Skip to content

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

@DmitrySharabin

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions