Run custom element upgrades through real constructors
This commit is contained in:
1 parent
af5ae70d31
commit
9bacacc60d
2 files changed
+179
-19
No files matched your search
Vendored
+52
-19
@@ -1035,6 +1035,13 @@ function _eventTargetDispatch(target, event) {
|
||||
return !event.defaultPrevented;
|
||||
}
|
||||
|
||||
// During custom-element upgrade, HTMLElement's constructor must return the
|
||||
// already-existing element being upgraded. A class constructor cannot be
|
||||
// invoked with `.call(existingElement)`, so the registry and Element
|
||||
// constructor coordinate through the same construction-stack shape used by
|
||||
// browser custom-element implementations.
|
||||
const _customElementConstructionStack = [];
|
||||
|
||||
class Node {
|
||||
static ELEMENT_NODE = 1;
|
||||
static ATTRIBUTE_NODE = 2;
|
||||
@@ -1746,7 +1753,21 @@ globalThis.NamedNodeMap = NamedNodeMap;
|
||||
|
||||
class Element extends Node {
|
||||
constructor(nid) {
|
||||
super(nid);
|
||||
const entry = _customElementConstructionStack[_customElementConstructionStack.length - 1];
|
||||
const matchesUpgrade = entry && new.target === entry.constructor;
|
||||
const upgrading = matchesUpgrade && !entry.constructed ? entry.element : null;
|
||||
super(upgrading ? upgrading._nid : nid);
|
||||
if (matchesUpgrade && entry.constructed) {
|
||||
throw new TypeError("Custom element is already being constructed");
|
||||
}
|
||||
if (upgrading) {
|
||||
// Keep an already-constructed marker on the stack until the outer class
|
||||
// constructor returns. Recursive `new`/`super` calls for the same
|
||||
// definition must not steal the element currently being upgraded.
|
||||
entry.constructed = true;
|
||||
Object.setPrototypeOf(upgrading, new.target.prototype);
|
||||
return upgrading;
|
||||
}
|
||||
this._style = _styleProxy(new CSSStyleDeclaration(this));
|
||||
}
|
||||
// Element wrappers always back a nodeType-1 node (_wrap/_wrapEl only build an
|
||||
@@ -3340,15 +3361,24 @@ class Document extends Node {
|
||||
return _makeXPathResult(type, _xpathFindNodes(expression, contextNode || this));
|
||||
}
|
||||
createElement(t) {
|
||||
const el = _wrapEl(+_dom("create_element", t.toLowerCase()));
|
||||
if (el && t.toLowerCase() === 'template') {
|
||||
const localName = String(t).toLowerCase();
|
||||
const el = _wrapEl(+_dom("create_element", localName));
|
||||
if (el && localName === 'template') {
|
||||
el._templateContent = this.createDocumentFragment();
|
||||
el._templateContent._fragmentContext = 'template';
|
||||
}
|
||||
const definition = globalThis.customElements?._registry?.get(localName);
|
||||
if (el && definition) globalThis.customElements._upgradeElement(el, definition);
|
||||
return el;
|
||||
}
|
||||
createElementNS(ns, t) {
|
||||
const el = this.createElement(t);
|
||||
const namespace = ns == null ? null : String(ns);
|
||||
if (namespace === "http://www.w3.org/1999/xhtml") {
|
||||
const el = this.createElement(t);
|
||||
if (el) el._ns = namespace;
|
||||
return el;
|
||||
}
|
||||
const el = _wrapEl(+_dom("create_element", String(t).toLowerCase()));
|
||||
if (el) el._ns = ns;
|
||||
return el;
|
||||
}
|
||||
@@ -5920,25 +5950,28 @@ class CustomElementRegistry {
|
||||
if (el.__customUpgraded) return;
|
||||
el.__customUpgraded = true;
|
||||
try {
|
||||
// Web Components spec: copy own props from the prototype onto the
|
||||
// element. JS-side classes define behavior via methods on the
|
||||
// prototype; we don't truly swap prototypes (Element is shared),
|
||||
// so attach the prototype methods directly to the instance.
|
||||
const proto = cls.prototype;
|
||||
for (const key of Object.getOwnPropertyNames(proto)) {
|
||||
if (key === 'constructor') continue;
|
||||
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
||||
if (desc) Object.defineProperty(el, key, desc);
|
||||
// Upgrade preserves object identity but installs the definition's
|
||||
// prototype before running its class constructor. HTMLElement's
|
||||
// constructor consumes this entry and returns `el`, so derived class
|
||||
// fields and constructor-side state initialize on the real DOM wrapper.
|
||||
const constructionEntry = { element: el, constructor: cls, constructed: false };
|
||||
_customElementConstructionStack.push(constructionEntry);
|
||||
let constructed;
|
||||
try {
|
||||
constructed = Reflect.construct(cls, []);
|
||||
} finally {
|
||||
const pending = _customElementConstructionStack.lastIndexOf(constructionEntry);
|
||||
if (pending !== -1) _customElementConstructionStack.splice(pending, 1);
|
||||
}
|
||||
if (constructed !== el) {
|
||||
throw new TypeError("Custom element constructor did not produce the element being upgraded");
|
||||
}
|
||||
// Run constructor-side init on the element. Real custom elements
|
||||
// run the class constructor, but Element instances aren't a `cls`
|
||||
// subclass here; calling `.call(el)` runs whatever init logic the
|
||||
// class defines without needing a new allocation.
|
||||
try { cls.call(el); } catch (e) {}
|
||||
if (typeof el.connectedCallback === 'function' && globalThis.document?.contains?.(el)) {
|
||||
try { el.connectedCallback(); } catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
el.__customUpgradeFailed = true;
|
||||
}
|
||||
}
|
||||
get(name) { return this._registry.get(name); }
|
||||
getName(cls) {
|
||||
|
||||
@@ -1897,6 +1897,133 @@ mod tests {
|
||||
assert_eq!(ok, serde_json::json!("OK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_element_upgrade_runs_class_constructor_on_existing_element() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<html><body><svelte-like id="component"></svelte-like></body></html>"#,
|
||||
);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const before = document.getElementById("component");
|
||||
class SvelteLike extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.$$s = [];
|
||||
this.attachShadow({ mode: "open" });
|
||||
}
|
||||
connectedCallback() {
|
||||
for (const subscription of this.$$s) subscription();
|
||||
this.$$s.push(() => {});
|
||||
this.shadowRoot.textContent = "ready";
|
||||
}
|
||||
}
|
||||
customElements.define("svelte-like", SvelteLike);
|
||||
return [
|
||||
document.getElementById("component") === before,
|
||||
before instanceof SvelteLike,
|
||||
before.constructor === SvelteLike,
|
||||
before.$$s.length,
|
||||
before.shadowRoot && before.shadowRoot.textContent
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
"ready"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_element_synchronously_constructs_an_existing_definition() {
|
||||
let mut rt = setup_runtime("<html><body></body></html>");
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const testStart = true;
|
||||
class CreatedLater extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.constructorState = ["initialized"];
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.textContent = "constructed";
|
||||
}
|
||||
connectedCallback() {
|
||||
this.constructorState.push("connected");
|
||||
}
|
||||
}
|
||||
customElements.define("created-later", CreatedLater);
|
||||
const element = document.createElement("created-later");
|
||||
const foreign = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg", "created-later"
|
||||
);
|
||||
return [
|
||||
element instanceof CreatedLater,
|
||||
element.constructor === CreatedLater,
|
||||
element.localName,
|
||||
element.constructorState,
|
||||
element.shadowRoot && element.shadowRoot.textContent,
|
||||
element.isConnected,
|
||||
foreign instanceof CreatedLater
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
true,
|
||||
true,
|
||||
"created-later",
|
||||
["initialized"],
|
||||
"constructed",
|
||||
false,
|
||||
false
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn throwing_custom_element_constructor_marks_upgrade_failed_without_connecting() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<html><body><throws-during-upgrade id="target"></throws-during-upgrade></body></html>"#,
|
||||
);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
let constructorCalls = 0;
|
||||
let connectedCalls = 0;
|
||||
class ThrowsDuringUpgrade extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
constructorCalls++;
|
||||
throw new Error("expected constructor failure");
|
||||
}
|
||||
connectedCallback() {
|
||||
connectedCalls++;
|
||||
}
|
||||
}
|
||||
customElements.define("throws-during-upgrade", ThrowsDuringUpgrade);
|
||||
const element = document.getElementById("target");
|
||||
customElements.upgrade(document);
|
||||
return [
|
||||
constructorCalls,
|
||||
connectedCalls,
|
||||
element.__customUpgradeFailed === true
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!([1, 0, true]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_document_title() {
|
||||
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");
|
||||
|
||||
Reference in new issue
Block a user