Skip to content

[Props] static props not inherited from parent class #104

Description

@DmitrySharabin

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions