Repro
let el = document.createElement("my-element");
el.setAttribute("foo", "bar"); // attribute set while disconnected
document.body.append(el);
console.log(el.foo); // expected: "bar", actual: undefined
Cause
Props#attributeChanged early-returns when !element.isConnected:
attributeChanged (element, name, oldValue) {
if (!element.isConnected || element.ignoredAttributes.has(name)) {
// We process attributes all at once when the element is connected
return;
}
...
}
The comment promises "we process attributes all at once when the element is connected" — but no such re-scan exists on the connected lifecycle. Props#connected only drains the dispatch queue; it doesn't walk observedAttributes. So the attribute write is observed by the browser (via attributeChangedCallback), routed to Props#attributeChanged, bailed on, and never replayed.
The parser-upgrade path works because isConnected is true during constructor (the element is already in the document by the time customElements.define upgrades it). Only the createElement → setAttribute → append path is affected.
Possible fixes
- Drop the
!isConnected guard. Let attributeChanged proceed even when disconnected; the queue + connected() drain already handle deferred dispatch.
- Scan
observedAttributes in Props#connected. Mirror what initializeFor does, but only for attributes whose corresponding prop hasn't been set yet.
(1) is simpler and likely correct given the post-PR-#102 architecture (queue + drain handles disconnected elements).
Pre-existing
This bug exists on main and was preserved (not introduced) by #102. Surfaced during code review of that PR.
Repro
Cause
Props#attributeChangedearly-returns when!element.isConnected:The comment promises "we process attributes all at once when the element is connected" — but no such re-scan exists on the
connectedlifecycle.Props#connectedonly drains the dispatch queue; it doesn't walkobservedAttributes. So the attribute write is observed by the browser (viaattributeChangedCallback), routed toProps#attributeChanged, bailed on, and never replayed.The parser-upgrade path works because
isConnectedis true during constructor (the element is already in the document by the timecustomElements.defineupgrades it). Only thecreateElement → setAttribute → appendpath is affected.Possible fixes
!isConnectedguard. LetattributeChangedproceed even when disconnected; the queue +connected()drain already handle deferred dispatch.observedAttributesinProps#connected. Mirror whatinitializeFordoes, but only for attributes whose corresponding prop hasn't been set yet.(1) is simpler and likely correct given the post-PR-#102 architecture (queue + drain handles disconnected elements).
Pre-existing
This bug exists on
mainand was preserved (not introduced) by #102. Surfaced during code review of that PR.