diff --git a/.agents/skills/webjs/references/muscle-memory-gotchas.md b/.agents/skills/webjs/references/muscle-memory-gotchas.md index 0f4e33d25..66d6d6df9 100644 --- a/.agents/skills/webjs/references/muscle-memory-gotchas.md +++ b/.agents/skills/webjs/references/muscle-memory-gotchas.md @@ -153,10 +153,6 @@ Lit defaults to shadow DOM, so `static styles = css` scopes automatically. WebJs In shadow DOM the browser projects slotted content natively before `firstUpdated`, so Lit muscle memory says `this.shadowRoot.querySelector('slot').assignedNodes()` is populated there. In light DOM the first projection lands one microtask AFTER the first render, so `firstUpdated` sees the `` element with an EMPTY `assignedNodes()`. The webjs-shaped fix: read assigned content from a `slotchange` listener (fires once projection lands, and on every later change), or wait a microtask. Every later read and every mutation-driven update behaves identically in both modes; only the first-render read differs. -### Naming a component method after a native DOM method (`remove`, `append`, `after`) - -Light-DOM slots ARE the native DOM slot API, so the framework instruments the native mutation methods (`append`, `prepend`, `before`, `after`, `replaceWith`, `replaceChildren`, `remove`, `appendChild`, `insertBefore`, `removeChild`, `replaceChild`) to keep slot projection live. A component method that shadows one of those names (for example a `remove()` handler on a list-row component, or an `append()` helper) overrides the instrumented method on the instance, so a native write that goes through it silently stops re-projecting. The webjs-shaped fix is a non-colliding name (`removeItem()`, `appendRow()`). Flagged by `no-shadowed-native-member`, which only fires on a real instance method at class-body depth, so a static member, a nested-object property, or a `static shadow = true` component (native shadow slots are not instrumented) is not flagged. - ### `:host { display: block }` on a light-DOM component A custom element is `display: inline` by default, so a block container collapses. In Lit you fix this with `:host { display: block }`, which works because Lit is shadow-DOM-first. A light-DOM WebJs component has no shadow root, so there is no `:host` to write. There is nothing to do: the framework already defaults every light-DOM host to `display: block` via a low-priority `@layer webjs-host` rule, overridable by any Tailwind utility (`class="flex"` wins). A shadow-DOM component (`static shadow = true`) still sets `:host { display: block }` in `static styles` itself, exactly like Lit. diff --git a/docs/app/docs/troubleshooting/page.ts b/docs/app/docs/troubleshooting/page.ts index 49ef3a251..7c564cd20 100644 --- a/docs/app/docs/troubleshooting/page.ts +++ b/docs/app/docs/troubleshooting/page.ts @@ -36,6 +36,11 @@ export default function Troubleshooting() {

Cause: you wrote a class-field initializer for a factory-declared reactive property (count: number = 0 or count = 0 alongside WebComponent({ count: Number })). Under modern class-field semantics that compiles to a define on this AFTER super(), which overwrites the framework's reactive accessor, so subsequent assignments bypass the update.

Fix: remove the class-field initializer and set the default by assigning in the constructor after super(). See Components. The reactive-props-no-class-field check rule flags this.

+

A component method named append, remove, or appendChild silently never runs

+

Symptom: a handler or helper you defined on a component (an append() click handler, a remove() method) does nothing when called, with no error thrown, and TypeScript did not complain.

+

Cause: WebJs instruments the native DOM mutation methods on every light-DOM host to keep the slot API live, and it installs those interceptors as own properties ON THE ELEMENT INSTANCE. An own instance property shadows a method defined on the class prototype, so the framework's interceptor wins and your same-named class method is never reached. The instrumented names are append, prepend, before, after, replaceWith, replaceChildren, remove, appendChild, insertBefore, removeChild, and replaceChild. TypeScript does not catch it because a shorter override (zero or one argument) is assignable to the native signature.

+

Fix: rename the member to a non-native name (appendRow(), removeItem()) and update its call sites. Only these native mutation members are affected; render() and the lifecycle hooks are meant to be overridden. The no-shadowed-native-member check rule catches this ahead of time (it flags only a real instance method, never a static member, a nested-object property, or a static shadow = true component, whose native shadow slots are not instrumented). See Components.

+

An error saying static properties is no longer supported

Symptom: a component throws at construction with static properties is no longer supported. Declare reactive properties via the factory instead.

Cause: the class body has a hand-written static properties = { ... } field. WebJs declares reactive properties only through the WebComponent({ ... }) base-class factory now, and the runtime throws on a direct static properties.