Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .agents/skills/webjs/references/muscle-memory-gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<slot>` 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.
Expand Down
5 changes: 5 additions & 0 deletions docs/app/docs/troubleshooting/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export default function Troubleshooting() {
<p><strong>Cause:</strong> you wrote a class-field initializer for a factory-declared reactive property (<code>count: number = 0</code> or <code>count = 0</code> alongside <code>WebComponent({ count: Number })</code>). Under modern class-field semantics that compiles to a define on <code>this</code> AFTER <code>super()</code>, which overwrites the framework's reactive accessor, so subsequent assignments bypass the update.</p>
<p><strong>Fix:</strong> remove the class-field initializer and set the default by assigning in the <code>constructor</code> after <code>super()</code>. See <a href="/docs/components">Components</a>. The <code>reactive-props-no-class-field</code> check rule flags this.</p>

<h2>A component method named <code>append</code>, <code>remove</code>, or <code>appendChild</code> silently never runs</h2>
<p><strong>Symptom:</strong> a handler or helper you defined on a component (an <code>append()</code> click handler, a <code>remove()</code> method) does nothing when called, with no error thrown, and TypeScript did not complain.</p>
<p><strong>Cause:</strong> 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 <code>append</code>, <code>prepend</code>, <code>before</code>, <code>after</code>, <code>replaceWith</code>, <code>replaceChildren</code>, <code>remove</code>, <code>appendChild</code>, <code>insertBefore</code>, <code>removeChild</code>, and <code>replaceChild</code>. TypeScript does not catch it because a shorter override (zero or one argument) is assignable to the native signature.</p>
<p><strong>Fix:</strong> rename the member to a non-native name (<code>appendRow()</code>, <code>removeItem()</code>) and update its call sites. Only these native mutation members are affected; <code>render()</code> and the lifecycle hooks are meant to be overridden. The <code>no-shadowed-native-member</code> check rule catches this ahead of time (it flags only a real instance method, never a static member, a nested-object property, or a <code>static shadow = true</code> component, whose native shadow slots are not instrumented). See <a href="/docs/components">Components</a>.</p>

<h2>An error saying <code>static properties</code> is no longer supported</h2>
<p><strong>Symptom:</strong> a component throws at construction with <code>static properties is no longer supported. Declare reactive properties via the factory instead</code>.</p>
<p><strong>Cause:</strong> the class body has a hand-written <code>static properties = { ... }</code> field. WebJs declares reactive properties only through the <code>WebComponent({ ... })</code> base-class factory now, and the runtime throws on a direct <code>static properties</code>.</p>
Expand Down
Loading