Symptom
For a prop on an already-mounted element, mutating the prop while the element is disconnected drops the resulting propchange event — it is queued internally but never dispatched when the element reconnects.
class C extends NudeElement {
static props = { v: { type: Number, default: 0 } };
}
customElements.define("x-c", C);
let el = new C();
document.body.append(el);
el.addEventListener("propchange", e => console.log(e.name));
el.remove(); // disconnect
el.v = 5; // mutation lands in the queueing branch
await Promise.resolve(); // let the Computed microtask flush while disconnected
document.body.append(el); // reconnect
// expected: console logged "v"
// actual: nothing — queued event never drained
Root cause
firePropChangeEvent enters its queueing branch when the element is disconnected (plugins/props/util/Props.js#L74-L81):
if (element.isConnected && eventProps.prop.initialized) {
element.dispatchEvent?.(event);
}
else {
let queue = this.eventDispatchQueue.get(element) ?? [];
queue.push(event);
this.eventDispatchQueue.set(element, queue);
}
The queue is drained exactly once, inside Props.initializeFor (plugins/props/util/Props.js#L100-L109), and initializeFor is invoked only from the constructed lifecycle hook (plugins/props/index.js#L40-L42) — i.e. once per element, at construction time. The plugin registers no connected hook, so there is nothing to drain the queue when isConnected later flips back to true. Events that landed in the queueing branch after construction are permanently stuck in the eventDispatchQueue WeakMap.
Reproducible on
main and the props-runtime-tests branch. Surfaced as a skipped JS-first test in test/Props.js ("Queued propchange events drain on reconnect"). Pre-existing — not introduced by #79 / #91 / #97; the test was added in #99 specifically to track this case.
Suggested fix direction
Split a drainEventQueue(element) helper out of initializeFor and call it from a connected lifecycle hook (or connectedCallback, whichever fits the plugin contract — see how attributeChangedCallback is wired in plugins/props/index.js#L10-L14). Calling initializeFor itself on every reconnect would over-fire — it walks observed attributes and re-runs prop.initializeFor for every prop, which is mount-time work, not reconnect-time work.
Once fixed, un-skip the corresponding test in test/Props.js.
Symptom
For a prop on an already-mounted element, mutating the prop while the element is disconnected drops the resulting
propchangeevent — it is queued internally but never dispatched when the element reconnects.Root cause
firePropChangeEvententers its queueing branch when the element is disconnected (plugins/props/util/Props.js#L74-L81):The queue is drained exactly once, inside
Props.initializeFor(plugins/props/util/Props.js#L100-L109), andinitializeForis invoked only from theconstructedlifecycle hook (plugins/props/index.js#L40-L42) — i.e. once per element, at construction time. The plugin registers noconnectedhook, so there is nothing to drain the queue whenisConnectedlater flips back totrue. Events that landed in the queueing branch after construction are permanently stuck in theeventDispatchQueueWeakMap.Reproducible on
mainand theprops-runtime-testsbranch. Surfaced as a skipped JS-first test intest/Props.js("Queued propchange events drain on reconnect"). Pre-existing — not introduced by #79 / #91 / #97; the test was added in #99 specifically to track this case.Suggested fix direction
Split a
drainEventQueue(element)helper out ofinitializeForand call it from aconnectedlifecycle hook (orconnectedCallback, whichever fits the plugin contract — see howattributeChangedCallbackis wired in plugins/props/index.js#L10-L14). CallinginitializeForitself on every reconnect would over-fire — it walks observed attributes and re-runsprop.initializeForfor every prop, which is mount-time work, not reconnect-time work.Once fixed, un-skip the corresponding test in
test/Props.js.