Bug 1496425 - Provide a mechanism for Custom Elements to delay connectedCallback until after DOMContentLoaded;r=paolo

There are two reasons for this:
1) It's faster than running the connectedCallback in the middle of document parse, at least for
   <radiogroups> in about:preferences
2) It provides a construction sequence more similar to XBL, so the translation from XBL <constructor>
   to CE connectedCallback is more likely to be correct. This is because when there is markup like:
       <parent-ce><child-ce></child-ce></parent-ce>
   the parent-ce node is empty during the first connectedCallback. If we wait for DOMContentLoaded
   then the parent-ce has the child-ce node below it.

Differential Revision: https://phabricator.services.mozilla.com/D7944

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Brian Grinstead 2018-10-08 21:17:39 +00:00
Родитель c2709dc7e9
Коммит 4ee92c8669
4 изменённых файлов: 158 добавлений и 0 удалений

Просмотреть файл

@ -13,10 +13,53 @@
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
// The listener of DOMContentLoaded must be set on window, rather than
// document, because the window can go away before the event is fired.
// In that case, we don't want to initialize anything, otherwise we
// may be leaking things because they will never be destroyed after.
let gIsDOMContentLoaded = false;
const gElementsPendingConnection = new Set();
window.addEventListener("DOMContentLoaded", () => {
gIsDOMContentLoaded = true;
for (let element of gElementsPendingConnection) {
try {
if (element.isConnected) {
element.connectedCallback();
}
} catch (ex) { console.error(ex); }
}
gElementsPendingConnection.clear();
}, { once: true, capture: true });
const gXULDOMParser = new DOMParser();
gXULDOMParser.forceEnableXULXBL();
class MozXULElement extends XULElement {
/**
* Sometimes an element may not want to run connectedCallback logic during
* parse. This could be because we don't want to initialize the element before
* the element's contents have been fully parsed, or for performance reasons.
* If you'd like to opt-in to this, then add this to the beginning of your
* `connectedCallback` and `disconnectedCallback`:
*
* if (this.delayConnectedCallback()) { return }
*
* And this at the beginning of your `attributeChangedCallback`
*
* if (!this.isConnectedAndReady) { return; }
*/
delayConnectedCallback() {
if (gIsDOMContentLoaded) {
return false;
}
gElementsPendingConnection.add(this);
return true;
}
get isConnectedAndReady() {
return gIsDOMContentLoaded && this.isConnected;
}
/**
* Allows eager deterministic construction of XUL elements with XBL attached, by
* parsing an element tree and returning a DOM fragment to be inserted in the

Просмотреть файл

@ -105,6 +105,7 @@ skip-if = toolkit == "cocoa"
[test_closemenu_attribute.xul]
[test_contextmenu_list.xul]
[test_custom_element_base.xul]
[test_custom_element_delay_connection.xul]
[test_deck.xul]
[test_dialogfocus.xul]
[test_editor_for_input_with_autocomplete.html]

Просмотреть файл

@ -0,0 +1,110 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
<window title="Custom Element Base Delayed Connected"
onload="runTests();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<!-- test results are displayed in the html:body -->
<body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
<script type="application/javascript"><![CDATA[
let nativeDOMContentLoadedFired = false;
document.addEventListener("DOMContentLoaded", () => {
nativeDOMContentLoadedFired = true;
}, { once: true });
// To test `delayConnectedCallback` and `isConnectedAndReady` we have to run this before
// DOMContentLoaded, which is why this is done in a separate script that runs
// immediately and not in `runTests`.
let delayedConnectionPromise = new Promise(resolve => {
let numSkippedAttributeChanges = 0;
let numDelayedConnections = 0;
let numDelayedDisconnections = 0;
let finishedWaitingForDOMReady = false;
// Register this custom element before DOMContentLoaded has fired and before it's parsed in
// the markup:
customElements.define("delayed-connection", class DelayedConnection extends MozXULElement {
static get observedAttributes() { return ["foo"]; }
attributeChangedCallback() {
ok(!this.isConnectedAndReady,
"attributeChangedCallback fires before isConnectedAndReady");
ok(!nativeDOMContentLoadedFired,
"attributeChangedCallback fires before nativeDOMContentLoadedFired");
numSkippedAttributeChanges++;
}
connectedCallback() {
if (this.delayConnectedCallback()) {
ok(!finishedWaitingForDOMReady,
"connectedCallback with delayConnectedCallback fires before finishedWaitingForDOMReady");
ok(!this.isConnectedAndReady,
"connectedCallback with delayConnectedCallback fires before isConnectedAndReady");
ok(!nativeDOMContentLoadedFired,
"connectedCallback with delayConnectedCallback fires before nativeDOMContentLoadedFired");
numDelayedConnections++;
return;
}
ok(!finishedWaitingForDOMReady,
"connectedCallback only fires once when DOM is ready");
ok(this.isConnectedAndReady,
"isConnectedAndReady during connectedCallback");
ok(!nativeDOMContentLoadedFired,
"delayed connectedCallback fires before nativeDOMContentLoadedFired");
is(numSkippedAttributeChanges, 2,
"Correct number of skipped attribute changes");
is(numDelayedConnections, 2,
"Correct number of delayed connections");
is(numDelayedDisconnections, 1,
"Correct number of delated disconnections");
finishedWaitingForDOMReady = true;
resolve();
}
disconnectedCallback() {
ok(this.delayConnectedCallback(),
"disconnectedCallback while DOM not ready");
is(numDelayedDisconnections, 0,
"disconnectedCallback fired only once");
numDelayedDisconnections++;
}
});
});
// This should be called after the element is parsed below this.
function mutateDelayedConnection() {
// Fire connectedCallback and attributeChangedCallback twice before DOMContentLoaded
// fires. The first connectedCallback is due to the parse and the second due to re-appending.
let delayedConnection = document.querySelector("delayed-connection");
delayedConnection.setAttribute("foo", "bar");
delayedConnection.remove();
delayedConnection.setAttribute("foo", "bat");
document.documentElement.append(delayedConnection);
}
]]>
</script>
<delayed-connection></delayed-connection>
<!-- test code goes here -->
<script type="application/javascript"><![CDATA[
SimpleTest.waitForExplicitFinish();
mutateDelayedConnection();
async function runTests() {
info("Waiting for delayed connection to fire");
ok(nativeDOMContentLoadedFired,
"nativeDOMContentLoadedFired is true in runTests");
await delayedConnectionPromise;
SimpleTest.finish();
}
]]>
</script>
</window>

Просмотреть файл

@ -112,6 +112,10 @@ class MozRadiogroup extends MozBaseControl {
}
connectedCallback() {
if (this.delayConnectedCallback()) {
return;
}
this.init();
if (!this.value) {
this.selectedIndex = 0;