Skip to content

fix(webui-framework): @attr property reflection#304

Merged
mcritzjam merged 3 commits into
mainfrom
mmansour/webui-framewrk-observation
May 19, 2026
Merged

fix(webui-framework): @attr property reflection#304
mcritzjam merged 3 commits into
mainfrom
mmansour/webui-framewrk-observation

Conversation

@mohamedmansour
Copy link
Copy Markdown
Contributor

The bug was that @attr looked reactive from the component template, but it was not fully the same reactive primitive as @observable.

Property writes updated the backing field and could refresh template bindings, but they did not reflect back to the host DOM attribute. @attr also was not registered in the observable-name registry, so targeted updates, SSR state seeding, and setState() treated it differently from @observable.

host.label = "bob";

Before this fix, that assignment left the host markup stale:

<test-attr label="Status"></test-attr>

This broke attribute selectors, external DOM reads, serialization, and parent code that expects @attr to be observable state plus DOM attribute synchronization.

The fix makes @attr reuse the observable registration path and adds guarded property-to-attribute reflection. The reflection guard prevents a DOM write from re-entering attributeChangedCallback and converting non-string backing values into strings.

host.count = 5;
// stays number-backed while reflecting count="5"

It also syncs pre-connect @attr property values after mount so client-created elements preserve values assigned before appendChild().

Tests now cover default and custom attr names, boolean attr reflection, DOM-to-property updates, setState(), type-preserving reflection, and pre-connect assignment.

The bug was that `@attr` looked reactive from the component template, but it was not fully the same reactive primitive as `@observable`.

Property writes updated the backing field and could refresh template bindings, but they did not reflect back to the host DOM attribute. `@attr` also was not registered in the observable-name registry, so targeted updates, SSR state seeding, and `setState()` treated it differently from `@observable`.

```ts
host.label = "bob";
```

Before this fix, that assignment left the host markup stale:

```html
<test-attr label="Status"></test-attr>
```

This broke attribute selectors, external DOM reads, serialization, and parent code that expects `@attr` to be observable state plus DOM attribute synchronization.

The fix makes `@attr` reuse the observable registration path and adds guarded property-to-attribute reflection. The reflection guard prevents a DOM write from re-entering `attributeChangedCallback` and converting non-string backing values into strings.

```ts
host.count = 5;
// stays number-backed while reflecting count="5"
```

It also syncs pre-connect `@attr` property values after mount so client-created elements preserve values assigned before `appendChild()`.

Tests now cover default and custom attr names, boolean attr reflection, DOM-to-property updates, `setState()`, type-preserving reflection, and pre-connect assignment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mohamedmansour mohamedmansour requested review from a team and akroshg May 18, 2026 23:57
mohamedmansour and others added 2 commits May 18, 2026 17:35
The review found that subclassed WebUI elements did not inherit decorator metadata from their base classes. That meant inherited `@attr` properties could appear in `observedAttributes` while `attributeChangedCallback`, `setState()`, SSR state seeding, targeted updates, and mount-time attribute sync only saw metadata registered directly on the subclass.

```ts
class BaseCard extends WebUIElement {
  @attr baseLabel = "";
}

class ProductCard extends BaseCard {
  @attr label = "";
}
```

Before this fix, `base-label` changes on `<product-card>` were not routed to `baseLabel`, and inherited observable names were missing from the subclass registry.

The fix resolves observable and attr metadata through the constructor chain and copies inherited maps when a subclass registers its own decorators. That keeps lookup cost bounded to subclass setup or rare inheritance fallback, while direct classes still use the same WeakMap-backed hot path.

Tests now cover inherited `@observable` names and inherited `@attr` metadata for DOM-to-property and property-to-attribute synchronization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mcritzjam mcritzjam merged commit 386ebaa into main May 19, 2026
21 checks passed
@mcritzjam mcritzjam deleted the mmansour/webui-framewrk-observation branch May 19, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants