Symptom
With a class hierarchy where both Parent and Child declare static props, Parent's props are never installed — no accessor descriptors on Parent.prototype, no Props instance on Parent.
class Parent extends NudeElement {
static props = { a: { type: String } };
}
class Child extends Parent {
static props = { c: { type: String } };
}
customElements.define("x-child", Child);
let el = new Child();
el.a = "value"; // sets a data property; no parsing, no propchange
console.log(el.a); // "value" — but Parent's accessor never ran
Object.getOwnPropertyDescriptor(Parent.prototype, "a"); // undefined
// `props` here is the internal symbol the plugin uses to attach the
// per-class Props instance — exported from `src/plugins/props/index.js`.
Parent[props]; // undefined
Root cause
The setup hook in src/plugins/props/index.js#L26-L30 only checks Object.hasOwn(this, "props"):
setup () {
if (Object.hasOwn(this, "props")) {
this.defineProps();
}
},
hooks-common's setup cascade runs once with this = most-derived class only. So when Child is the most-derived, only Child's static props is processed: Child.defineProps() is called, which lazily creates Child[props] and installs accessor descriptors on Child.prototype. Parent's static props is never processed — its accessor descriptors are never installed on Parent.prototype, and Parent[props] is never created.
The // TODO how does this work if attributeChangedCallback is inherited? at index.js#L9 suggests this territory was known to be unfinished.
Reproducible on
main and props-batched-drain. Pre-existing — the static-only-on-most-derived semantic of setup predates the recent props refactors. Latent in production because no consumer in nude-element or color-elements declares static props on a non-leaf class (every color-element component extends ColorElement, which has zero props of its own).
Suggested fix direction
Walk the class chain in setup. Either:
- Local fix: have the props
setup hook walk ancestors via getSupers(this, NudeElement) and call defineProps for each ancestor that has its own static props. Each class still gets its own Props instance; descriptors land on the correct prototypes.
- Broader fix: change
hooks-common's setup cascade to run for each class in the chain. Bigger blast radius.
Related
Once this is fixed, a second issue surfaces: with both Parent's and Child's Props active, multi-prop synchronous writes spanning both classes will fire propsupdate (and updated()) twice with partial Maps, not once with the merged set. Each Props instance owns an independent #eventDispatchQueue / #scheduleDrain. That's a separate concern about per-class vs element-level drain coordination — worth tracking but not a blocker for this fix.
Symptom
With a class hierarchy where both Parent and Child declare
static props, Parent's props are never installed — no accessor descriptors onParent.prototype, noPropsinstance on Parent.Root cause
The
setuphook insrc/plugins/props/index.js#L26-L30only checksObject.hasOwn(this, "props"):hooks-common'ssetupcascade runs once withthis= most-derived class only. So whenChildis the most-derived, onlyChild'sstatic propsis processed:Child.defineProps()is called, which lazily createsChild[props]and installs accessor descriptors onChild.prototype. Parent'sstatic propsis never processed — its accessor descriptors are never installed onParent.prototype, andParent[props]is never created.The
// TODO how does this work if attributeChangedCallback is inherited?atindex.js#L9suggests this territory was known to be unfinished.Reproducible on
mainandprops-batched-drain. Pre-existing — the static-only-on-most-derived semantic ofsetuppredates the recent props refactors. Latent in production because no consumer innude-elementorcolor-elementsdeclaresstatic propson a non-leaf class (every color-element component extendsColorElement, which has zero props of its own).Suggested fix direction
Walk the class chain in
setup. Either:setuphook walk ancestors viagetSupers(this, NudeElement)and calldefinePropsfor each ancestor that has its ownstatic props. Each class still gets its ownPropsinstance; descriptors land on the correct prototypes.hooks-common'ssetupcascade to run for each class in the chain. Bigger blast radius.Related
Once this is fixed, a second issue surfaces: with both Parent's and Child's
Propsactive, multi-prop synchronous writes spanning both classes will firepropsupdate(andupdated()) twice with partial Maps, not once with the merged set. EachPropsinstance owns an independent#eventDispatchQueue/#scheduleDrain. That's a separate concern about per-class vs element-level drain coordination — worth tracking but not a blocker for this fix.