diff --git a/.changeset/fe-887-use-property-init.md b/.changeset/fe-887-use-property-init.md new file mode 100644 index 0000000..3b31224 --- /dev/null +++ b/.changeset/fe-887-use-property-init.md @@ -0,0 +1,14 @@ +--- +"@pionjs/pion": patch +--- + +Fix: useProperty initialization sets default value even when preventDefault is called + +When a parent component uses `lift()` to listen to a property change event without +passing the property value to the child, `useProperty`'s initialization event was +`preventDefault`'d by `lift`, leaving the host property `undefined`. This caused crashes +in components like `cosmoz-omnitable` where `selectedItems` was never initialized. + +The `updater` method now accepts an `isInit` flag that bypasses the `preventDefault` veto +during initialization, ensuring the host always receives its default value. Subsequent +updates retain the full `lift`/`preventDefault` two-way binding semantics. \ No newline at end of file diff --git a/src/use-property.ts b/src/use-property.ts index f936259..5952e65 100644 --- a/src/use-property.ts +++ b/src/use-property.ts @@ -52,7 +52,7 @@ export const useProperty = hook( if (initialValue == null) return; - this.updater(initialValue); + this.updater(initialValue, true); } update(ignored: string, ignored2: T): StateTuple { @@ -82,10 +82,10 @@ export const useProperty = hook( return ev; } - updater(valueOrUpdater: NewState): void { + updater(valueOrUpdater: NewState, isInit = false): void { const [previousValue, value, updater] = this.resolve(valueOrUpdater); const ev = this.notify(value, updater); - if (ev.defaultPrevented) return; + if (!isInit && ev.defaultPrevented) return; if (Object.is(previousValue, value)) return; this.state.host[this.property] = value; } diff --git a/test/use-property.test.ts b/test/use-property.test.ts index e16a04b..5132666 100644 --- a/test/use-property.test.ts +++ b/test/use-property.test.ts @@ -391,6 +391,41 @@ describe("useProperty", () => { expect(span?.textContent).to.equal("a,b,c"); }); + it("initializes default value even when lift prevents default", async () => { + let parentSetter; + + function Parent() { + let [items, setItems] = useState(undefined); + parentSetter = setItems; + return html``; + } + + function Child() { + let [items, setItems] = useProperty("items", () => []); + return html`${items.length}`; + } + + customElements.define( + "use-property-lift-no-prop-parent", + component(Parent) + ); + customElements.define( + "use-property-lift-no-prop-child", + component(Child) + ); + + const el = await fixture( + html`` + ); + const child = el.shadowRoot?.firstElementChild as HTMLElement; + const span = child?.shadowRoot?.firstElementChild; + + expect(child?.items).to.deep.equal([]); + expect(span?.textContent).to.equal("0"); + }); + it("lift with direct value still works", async () => { let childSetter;