зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
c2709dc7e9
Коммит
4ee92c8669
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче