@webjsdev/cli 0.10.11 → 0.10.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/mcp-docs.js +400 -0
- package/lib/mcp-source.js +244 -0
- package/lib/mcp.js +167 -18
- package/package.json +7 -2
- package/resources/AGENTS.md +404 -0
- package/resources/agent-docs/advanced.md +1090 -0
- package/resources/agent-docs/built-ins.md +367 -0
- package/resources/agent-docs/components.md +486 -0
- package/resources/agent-docs/configuration.md +207 -0
- package/resources/agent-docs/framework-dev.md +65 -0
- package/resources/agent-docs/lit-muscle-memory-gotchas.md +456 -0
- package/resources/agent-docs/metadata.md +334 -0
- package/resources/agent-docs/recipes.md +440 -0
- package/resources/agent-docs/service-worker.md +100 -0
- package/resources/agent-docs/ssr-partial-nav-design.md +214 -0
- package/resources/agent-docs/styling.md +235 -0
- package/resources/agent-docs/testing.md +372 -0
- package/resources/agent-docs/typescript.md +334 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# Lit muscle-memory gotchas
|
|
2
|
+
|
|
3
|
+
AI agents trained on lit will reach for patterns that look correct but
|
|
4
|
+
break webjs's SSR contract, reactivity model, or styling defaults. This
|
|
5
|
+
file catalogs those failures with the webjs-shaped fix for each.
|
|
6
|
+
|
|
7
|
+
The architectural disagreement underneath all of these. Lit is JS-first
|
|
8
|
+
(hydration is the API). Webjs is HTML-first (first paint is real HTML,
|
|
9
|
+
JS is opt-in per interactive behavior). Every gotcha below is
|
|
10
|
+
downstream of that one disagreement.
|
|
11
|
+
|
|
12
|
+
## Mental model. Progressive enhancement, JS opt-in per behavior not per component
|
|
13
|
+
|
|
14
|
+
Webjs is a progressive enhancement framework. Pages render as real HTML
|
|
15
|
+
on the server, and every web component renders to real HTML on the
|
|
16
|
+
server. With JavaScript disabled in the browser, the page is still
|
|
17
|
+
readable, `<a>` links still navigate, and `<form action method>`
|
|
18
|
+
submissions still hit server actions. Display-only custom elements
|
|
19
|
+
still render their server-produced HTML.
|
|
20
|
+
|
|
21
|
+
JavaScript is opt-in **per interactive behavior, not per component**.
|
|
22
|
+
This is the distinction most lit-shaped intuitions miss.
|
|
23
|
+
|
|
24
|
+
In lit and most modern frameworks, hydration is a per-component
|
|
25
|
+
decision. You decide whether a given component is interactive (and
|
|
26
|
+
therefore needs JS shipped and run) at the component boundary.
|
|
27
|
+
"Hydrate this island, skip that one."
|
|
28
|
+
|
|
29
|
+
In webjs, the granularity is different. Every component is server
|
|
30
|
+
rendered. JavaScript is requested **by the specific interactive holes
|
|
31
|
+
you write in the template**. A `@click=${...}` binding requests JS for
|
|
32
|
+
click handling. A `signal.set(...)` call (instance or module-scope)
|
|
33
|
+
requests JS for reactive updates. A property binding
|
|
34
|
+
`.data=${richObject}` requests JS for property hydration. A controller
|
|
35
|
+
like `Task` requests JS for that async behavior. A plain `<a href>`
|
|
36
|
+
does not request JS. A `<form action="...">` does not request JS. A
|
|
37
|
+
purely display-time component (no event listeners, no signal
|
|
38
|
+
mutations, no property bindings to hydrate) does not request JS.
|
|
39
|
+
|
|
40
|
+
A single component can mix both. A product card that shows
|
|
41
|
+
server-rendered title, price, image, and a "View" link
|
|
42
|
+
(no JS needed) plus an "Add to cart" button with a `@click`
|
|
43
|
+
(JS needed for that one behavior). The framework loads JS for the
|
|
44
|
+
component because of the `@click`, runs it, and the rest of the card
|
|
45
|
+
stays exactly as the server painted it. You do not pick a hydration
|
|
46
|
+
mode for the component. You write the markup, and the JS budget
|
|
47
|
+
follows from which interactive behaviors that markup uses.
|
|
48
|
+
|
|
49
|
+
Practical consequences for agents writing webjs code.
|
|
50
|
+
|
|
51
|
+
1. Never reach for `fetch()` plus a JS click handler when a `<form>`
|
|
52
|
+
plus a server action would do. The form is free (no JS), the
|
|
53
|
+
server action is typed and CSRF-protected, the result reaches the
|
|
54
|
+
page through normal navigation.
|
|
55
|
+
2. Never make first paint depend on hydration. If the user sees a
|
|
56
|
+
blank skeleton until JS runs, you wrote the feature wrong.
|
|
57
|
+
3. Never assume "this component needs JS" or "this component is
|
|
58
|
+
static" as a binary. Pick interactive primitives per behavior. A
|
|
59
|
+
shopping cart page can have ten components, eight of them adding
|
|
60
|
+
zero JS bytes, two of them adding the handlers they need.
|
|
61
|
+
4. When choosing between a server action invoked from `<form>` and a
|
|
62
|
+
client-side action invoked from `@click`, default to the form
|
|
63
|
+
unless the interaction genuinely needs client-only state
|
|
64
|
+
(optimistic UI, in-flight indicators tied to client state,
|
|
65
|
+
keyboard shortcuts).
|
|
66
|
+
|
|
67
|
+
## Use lit idioms, not vanilla DOM (the whole point of lit-style components)
|
|
68
|
+
|
|
69
|
+
webjs components are lit-shaped on purpose: the value is the declarative
|
|
70
|
+
DX (typed reactive props, signals, `html` templates, declarative
|
|
71
|
+
bindings), not raw DOM scripting. Reaching for vanilla web-component
|
|
72
|
+
muscle memory (`this.getAttribute`, `this.setAttribute`, `this.classList`,
|
|
73
|
+
`this.addEventListener`, `this.innerHTML`, `document.createElement`,
|
|
74
|
+
manual `observedAttributes` / `attributeChangedCallback`, manual
|
|
75
|
+
`customElements.define`) inside a component is the anti-pattern. Use the
|
|
76
|
+
lit form unless the vanilla API is genuinely unavoidable.
|
|
77
|
+
|
|
78
|
+
| Vanilla muscle memory | Lit-style webjs form |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `this.getAttribute('x')` / `this.hasAttribute('x')` for own config | a reactive prop: `static properties = { x: {...} }` + `declare x`, read `this.x` (the prop rides the `x` attribute) |
|
|
81
|
+
| `this.setAttribute('x', v)` / `removeAttribute` to reflect own state | a reactive prop with `reflect: true`, or for non-attribute state a `signal` |
|
|
82
|
+
| `state: true` reactive prop for internal state | a `signal` (instance signal in the constructor, or module-scope) |
|
|
83
|
+
| `this.classList.add/toggle(...)` on self | a `class=${...}` binding in `render()` |
|
|
84
|
+
| `this.innerHTML = ...` / `appendChild` / `document.createElement` | return the markup from `render()` as `` html`...` `` |
|
|
85
|
+
| `this.addEventListener('click', ...)` on own/child elements | a `@click=${...}` binding in the template |
|
|
86
|
+
| `this.querySelector(...)` to reach own rendered DOM | the `ref()` directive + `createRef()`, or read a `<form>` with `new FormData(form)` |
|
|
87
|
+
| manual `observedAttributes` + `attributeChangedCallback` | `static properties` (the framework derives both) |
|
|
88
|
+
| manual `customElements.define('x', C)` | `C.register('x')` |
|
|
89
|
+
|
|
90
|
+
Emitting an event with `this.dispatchEvent(new CustomEvent(...))` is the
|
|
91
|
+
correct lit form, not a vanilla smell. Reading form values with
|
|
92
|
+
`new FormData(e.currentTarget)` inside a `@submit` handler is also fine.
|
|
93
|
+
|
|
94
|
+
**When vanilla DOM is genuinely needed (these stay):**
|
|
95
|
+
|
|
96
|
+
- **Ancestor lookup in a compound component.** `this.closest('ui-tabs')`
|
|
97
|
+
to read a parent's state. There is no declarative lit equivalent. This
|
|
98
|
+
resolves at SSR too (tag-name selectors, against the SSR ancestor
|
|
99
|
+
chain), so a compound child's active/pressed state is correct in the
|
|
100
|
+
first server paint, not just after hydration. Host attributes the
|
|
101
|
+
child sets in `render()` (`this.dataset.* =`, `this.className =`,
|
|
102
|
+
`this.ariaPressed =`) reflect onto the SSR'd host tag. A class or
|
|
103
|
+
attribute selector still resolves to null server-side.
|
|
104
|
+
- **Slotted / projected content.** `this.querySelector(...)` reaching a
|
|
105
|
+
`<slot>`-projected child or a sibling sub-component the template does
|
|
106
|
+
not own. `ref()` only binds elements this component's own `render()`
|
|
107
|
+
creates, so it cannot reach slotted content.
|
|
108
|
+
- **Host attributes in light DOM.** A light-DOM `render()` template
|
|
109
|
+
cannot bind attributes or listeners on the host element itself, so a
|
|
110
|
+
component that must style or listen on its own host writes
|
|
111
|
+
`this.dataset.* =` / `this.className =` / `this.addEventListener` on
|
|
112
|
+
`this` in a lifecycle hook. Shadow-DOM components avoid this.
|
|
113
|
+
- **Global listeners.** `document` / `window` `addEventListener` for
|
|
114
|
+
click-away, global keys, resize, or reposition.
|
|
115
|
+
- **Reading another element's attribute.** `contentHost.getAttribute('side')`
|
|
116
|
+
reads a different element's config, not `this`.
|
|
117
|
+
- **Browser-only globals.** `localStorage`, `matchMedia`, `navigator`,
|
|
118
|
+
`document.documentElement` mutations (a theme toggle setting `<html>`),
|
|
119
|
+
clipboard. These belong in `connectedCallback` or an event handler.
|
|
120
|
+
- **Imperative focus.** `el.focus()` has no declarative form.
|
|
121
|
+
|
|
122
|
+
The rule of thumb: if a reactive prop, a signal, an `html` binding, or
|
|
123
|
+
`ref()` expresses it, use that. Reach for vanilla DOM only for the cases
|
|
124
|
+
above, where the platform offers nothing declarative.
|
|
125
|
+
|
|
126
|
+
This is a **convention, not a lint rule.** `webjs check` is reserved for
|
|
127
|
+
general correctness (SSR safety, server-only imports, erasable TS, and
|
|
128
|
+
so on), not for policing every vanilla call, which would be noisy and
|
|
129
|
+
poor DX. Use your judgment: prefer the lit form by default, and when a
|
|
130
|
+
vanilla API genuinely has no declarative equivalent (the cases above),
|
|
131
|
+
just use it.
|
|
132
|
+
|
|
133
|
+
## The SSR contract: the pre-render lifecycle plus `render()`
|
|
134
|
+
|
|
135
|
+
By design, the webjs SSR pipeline constructs the instance, applies
|
|
136
|
+
attributes, runs the **pre-render value-deriving hooks** (`willUpdate`,
|
|
137
|
+
then controllers' `hostUpdate`), reflects `reflect: true` properties,
|
|
138
|
+
and calls `instance.render()`. Nothing past render fires server-side.
|
|
139
|
+
Not `connectedCallback`, not `shouldUpdate`, not the `update` DOM
|
|
140
|
+
commit, not `firstUpdated`, not `updated`, not controllers'
|
|
141
|
+
`hostConnected` / `hostUpdated`. See
|
|
142
|
+
`packages/core/src/render-server.js` around line 357.
|
|
143
|
+
|
|
144
|
+
The mental model is one sentence. Code in the constructor, `willUpdate`,
|
|
145
|
+
and `render()` must avoid the genuinely browser-only surface (`document`,
|
|
146
|
+
`window`, `localStorage`, `navigator`, `querySelector`, layout reads),
|
|
147
|
+
though the attribute, event, and `attachInternals` methods are backed by
|
|
148
|
+
a server shim and are safe. Code in every other hook is client-only and
|
|
149
|
+
can freely use any browser API without an `isServer` guard.
|
|
150
|
+
|
|
151
|
+
The gotchas below are all violations of that rule.
|
|
152
|
+
|
|
153
|
+
## Patterns that produce visibly broken SSR
|
|
154
|
+
|
|
155
|
+
### 1. Fetching data in `connectedCallback` or `firstUpdated`
|
|
156
|
+
|
|
157
|
+
The lit pattern is to subscribe or fetch on connect, then update
|
|
158
|
+
state when the data arrives. In webjs the first paint is empty because
|
|
159
|
+
neither hook runs server-side. Content pops in after hydration, often
|
|
160
|
+
with a layout shift.
|
|
161
|
+
|
|
162
|
+
Fix. Fetch in the page function and pass the data as props or
|
|
163
|
+
attributes.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// app/users/[id]/page.ts (correct)
|
|
167
|
+
import { fetchUser } from '../../modules/users/queries/fetch-user.server.ts';
|
|
168
|
+
export default async function User({ params }) {
|
|
169
|
+
const user = await fetchUser(params.id);
|
|
170
|
+
return html`<user-card .user=${user}></user-card>`;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 2. Using `Task` for initial-paint data
|
|
175
|
+
|
|
176
|
+
Lit's canonical async pattern. The `Task` controller wires up a fetcher
|
|
177
|
+
that runs on host update. Controllers' `hostUpdate` does fire at SSR, but
|
|
178
|
+
`Task` deliberately does not auto-run server-side: it keeps its `INITIAL`
|
|
179
|
+
state and runs only on hydration, so no request fires during SSR. The
|
|
180
|
+
client then renders the resolved state, causing a flash.
|
|
181
|
+
|
|
182
|
+
`Task` is still useful for client-time async (interaction-triggered
|
|
183
|
+
mutations, polling, websocket reactions). For initial-paint data, fetch
|
|
184
|
+
in the page function instead.
|
|
185
|
+
|
|
186
|
+
### 3. Browser-only APIs in the constructor or `render()`
|
|
187
|
+
|
|
188
|
+
Calls like `window.matchMedia(...)`, `localStorage.getItem(...)`,
|
|
189
|
+
`navigator.userAgent`, `document.querySelector(...)` in the constructor
|
|
190
|
+
or render path crash SSR. The constructor is for pure-JS init
|
|
191
|
+
(defaults, method binding, instance fields). Browser APIs belong in
|
|
192
|
+
`connectedCallback` or later hooks (which are client-only by
|
|
193
|
+
construction).
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// wrong
|
|
197
|
+
constructor() {
|
|
198
|
+
super();
|
|
199
|
+
this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// right
|
|
203
|
+
constructor() {
|
|
204
|
+
super();
|
|
205
|
+
this.dark = false;
|
|
206
|
+
}
|
|
207
|
+
connectedCallback() {
|
|
208
|
+
super.connectedCallback();
|
|
209
|
+
this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
This applies only to the genuinely browser-only HTMLElement members on
|
|
214
|
+
`this` (`this.classList`, `this.querySelector(...)`,
|
|
215
|
+
`this.attachShadow(...)`, `this.getBoundingClientRect(...)`, `this.focus()`):
|
|
216
|
+
the SSR-time instance has no DOM, so they throw. The attribute methods
|
|
217
|
+
(`this.getAttribute` / `setAttribute` / `hasAttribute` / `toggleAttribute`),
|
|
218
|
+
the event methods (`addEventListener` / `removeEventListener` /
|
|
219
|
+
`dispatchEvent`), and `this.attachInternals()` ARE backed by a server shim,
|
|
220
|
+
so reading an attribute in `render()`, wiring a delegated listener in the
|
|
221
|
+
constructor, or reflecting a property during the SSR update cycle all work.
|
|
222
|
+
Reading attributes that drive render through a reactive property
|
|
223
|
+
(`static properties` + `declare`) is still the idiomatic path, but
|
|
224
|
+
`this.hasAttribute(...)` no longer crashes.
|
|
225
|
+
|
|
226
|
+
Two guards catch the browser-only cases. `webjs check` flags browser
|
|
227
|
+
globals and the still-unsupported HTMLElement members used in a constructor
|
|
228
|
+
or render body (the `no-browser-globals-in-render` rule). And if one slips
|
|
229
|
+
through, the SSR crash is actionable: the log names the offending member and
|
|
230
|
+
tells you to move it to `connectedCallback` or a lifecycle hook, instead of
|
|
231
|
+
a raw `document is not defined`.
|
|
232
|
+
|
|
233
|
+
### 4. Top-level imports of browser-only libraries
|
|
234
|
+
|
|
235
|
+
`import * as d3 from 'd3'`, `import Chart from 'chart.js'`, or any
|
|
236
|
+
library that touches `window` at import time. The page module loads on
|
|
237
|
+
the server during SSR, so the offending top-level access crashes.
|
|
238
|
+
|
|
239
|
+
Two fixes. Use a dynamic `import()` inside `connectedCallback` for
|
|
240
|
+
client-only behavior. Or wrap server-side work in a `.server.ts` file
|
|
241
|
+
if the library has both server and client uses.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
connectedCallback() {
|
|
245
|
+
super.connectedCallback();
|
|
246
|
+
import('chart.js').then(({ Chart }) => {
|
|
247
|
+
this.chart = new Chart(this.canvas, this.config);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Patterns that compile but silently break reactivity
|
|
253
|
+
|
|
254
|
+
### 5. Class-field initializers for reactive properties
|
|
255
|
+
|
|
256
|
+
This looks fine in TypeScript. It silently breaks the framework's
|
|
257
|
+
accessor.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// wrong (the initializer overwrites the framework accessor after super())
|
|
261
|
+
class StudentCard extends WebComponent {
|
|
262
|
+
static properties = { student: { type: Object } };
|
|
263
|
+
student: Student = { name: '', email: '' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// right
|
|
267
|
+
class StudentCard extends WebComponent {
|
|
268
|
+
static properties = { student: { type: Object } };
|
|
269
|
+
declare student: Student;
|
|
270
|
+
constructor() {
|
|
271
|
+
super();
|
|
272
|
+
this.student = { name: '', email: '' };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`webjs check` flags this via the `reactive-props-use-declare` rule, but
|
|
278
|
+
AI agents emit the broken form on autopilot. The convention check is
|
|
279
|
+
the safety net, not the primary defense. Authoring code should use
|
|
280
|
+
`declare` plus constructor defaults from the start.
|
|
281
|
+
|
|
282
|
+
### 6. The `@property()` decorator
|
|
283
|
+
|
|
284
|
+
Banned by framework invariant 10 (erasable TS). The replacement is
|
|
285
|
+
`static properties = { ... }` plus a matching `declare` for the typed
|
|
286
|
+
accessor, as shown above. Decorators are non-erasable, so they would
|
|
287
|
+
force the framework to depend on a build step.
|
|
288
|
+
|
|
289
|
+
## Patterns that produce different visual output
|
|
290
|
+
|
|
291
|
+
### 7. Expecting shadow DOM by default (and reaching for scoped CSS instead of Tailwind)
|
|
292
|
+
|
|
293
|
+
Lit components default to shadow DOM. `static styles = css` scoping
|
|
294
|
+
works automatically. Webjs defaults to light DOM. A `static styles`
|
|
295
|
+
block without `static shadow = true` does nothing useful (the framework
|
|
296
|
+
warns at runtime), and styles authored for the component bleed into
|
|
297
|
+
the global namespace.
|
|
298
|
+
|
|
299
|
+
This is also the **styling reflex** to unlearn, not just a config
|
|
300
|
+
default. Because lit scopes, the lit habit is to author scoped CSS
|
|
301
|
+
(`static styles = css\`\``) or an inline `<style>` with semantic class
|
|
302
|
+
names (`.hero`, `.feature`, `.card`) for every component. In a webjs
|
|
303
|
+
light-DOM component that CSS either does nothing (the scoped block) or
|
|
304
|
+
leaks globally (the inline `<style>` with bare class names). **The
|
|
305
|
+
webjs-shaped fix is Tailwind utilities, which apply directly in light
|
|
306
|
+
DOM and are webjs's strong styling default.** Reach for raw CSS only for
|
|
307
|
+
the short allowlist (design tokens, `@property` + `@keyframes`,
|
|
308
|
+
`::-webkit-scrollbar`, `prefers-reduced-motion`, complex `color-mix()` /
|
|
309
|
+
gradients); see `agent-docs/styling.md` for the full Tailwind-first rule
|
|
310
|
+
and that allowlist.
|
|
311
|
+
|
|
312
|
+
Three correct paths. Use Tailwind utilities in light DOM (the default,
|
|
313
|
+
and the answer for the vast majority of components). Or add
|
|
314
|
+
`static shadow = true` to the class and keep `static styles` (scoped CSS
|
|
315
|
+
genuinely belongs in a shadow root). Or, if authoring vanilla CSS in
|
|
316
|
+
light-DOM mode anyway, prefix every selector with the component tag, per
|
|
317
|
+
the styling invariant. When a utility bundle repeats across light-DOM
|
|
318
|
+
components, extract it into a `lib/utils/ui.ts` helper returning an
|
|
319
|
+
`` html`...` `` fragment, never a shared CSS class.
|
|
320
|
+
|
|
321
|
+
### 8. `<slot>` timing differs across DOM modes
|
|
322
|
+
|
|
323
|
+
Both modes accept `<slot>` syntax in templates and provide
|
|
324
|
+
`assignedNodes`, `assignedElements`, `slotchange`, named slots,
|
|
325
|
+
fallback content, and first-wins resolution. The public surface
|
|
326
|
+
aligns.
|
|
327
|
+
|
|
328
|
+
What differs is timing. In shadow DOM, slot projection is a browser
|
|
329
|
+
primitive that fires synchronously on parse. In light DOM, projection
|
|
330
|
+
is framework-driven (`packages/core/src/slot.js`) and observes
|
|
331
|
+
mutations via `MutationObserver`. Code that reads
|
|
332
|
+
`slot.assignedNodes()` synchronously in `connectedCallback` may see an
|
|
333
|
+
empty list in light DOM and a populated list in shadow DOM. Use
|
|
334
|
+
`slotchange` to react instead of reading synchronously.
|
|
335
|
+
|
|
336
|
+
## Lifecycle subtleties at SSR
|
|
337
|
+
|
|
338
|
+
### 9. `willUpdate` computing state for SSR (this now works)
|
|
339
|
+
|
|
340
|
+
This used to be a gotcha. The SSR pipeline now runs `willUpdate` (and
|
|
341
|
+
controllers' `hostUpdate`) before `render()`, so deriving render state
|
|
342
|
+
there is correct in the first paint:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
willUpdate(changedProperties) {
|
|
346
|
+
this.fullName = `${this.first} ${this.last}`;
|
|
347
|
+
}
|
|
348
|
+
render() {
|
|
349
|
+
return html`<p>${this.fullName}</p>`;
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The SSR HTML now shows `<p>Ada Lovelace</p>`. The value must still be a
|
|
354
|
+
pure function of constructor state plus applied attributes (no
|
|
355
|
+
browser-only APIs), since SSR has no DOM. What still does NOT run
|
|
356
|
+
server-side is the post-render and connection hooks (`update` commit,
|
|
357
|
+
`firstUpdated`, `updated`, `connectedCallback`, controllers'
|
|
358
|
+
`hostConnected` / `hostUpdated`), so state those compute is absent from
|
|
359
|
+
the first paint.
|
|
360
|
+
|
|
361
|
+
One tradeoff to know: overriding `willUpdate` is an interactivity
|
|
362
|
+
signal for the elision analyser, so a component that uses it (even
|
|
363
|
+
purely to derive SSR state) ships its JS to the browser and is never
|
|
364
|
+
elided. For a truly display-only component, prefer computing the value
|
|
365
|
+
inline in `render()` so the module can still be elided; reach for
|
|
366
|
+
`willUpdate` when the component is interactive anyway, or when the
|
|
367
|
+
derivation is shared across `render()` and a client hook.
|
|
368
|
+
|
|
369
|
+
### 10. `ContextProvider` for server-known data
|
|
370
|
+
|
|
371
|
+
Context providers in lit publish on connect via `hostConnected`. In
|
|
372
|
+
webjs SSR, `connectedCallback` does not run, so descendants that read
|
|
373
|
+
context during SSR see the default value (or undefined). On hydration
|
|
374
|
+
the provider connects and consumers re-render, causing a content
|
|
375
|
+
shift.
|
|
376
|
+
|
|
377
|
+
Rule of thumb. For data known on the server (session, user, theme,
|
|
378
|
+
locale, feature flags, A/B variants), pass it through props from the
|
|
379
|
+
page function rather than through context. Reserve `ContextProvider`
|
|
380
|
+
for client-time concerns (interaction state, focus management,
|
|
381
|
+
transient UI state).
|
|
382
|
+
|
|
383
|
+
## List rendering
|
|
384
|
+
|
|
385
|
+
### 11. Reordering a `.map()` list needs a keyed `repeat()`
|
|
386
|
+
|
|
387
|
+
A plain `.map()` list reconciles in place, matching lit-html's non-keyed
|
|
388
|
+
child-part behaviour. When one item's binding changes (a card flips its
|
|
389
|
+
`dragging` class on `@dragstart`, a row's input is edited), the framework
|
|
390
|
+
patches that item's existing nodes instead of rebuilding the whole list,
|
|
391
|
+
so DOM node identity survives. That is what makes native drag-and-drop,
|
|
392
|
+
focus, caret, text selection, scroll position, and uncontrolled input
|
|
393
|
+
value all survive an item-level update, no `repeat()` required. (This
|
|
394
|
+
used to be a real gotcha. Before the fix, any change to a `.map()`'s
|
|
395
|
+
output tore down and replaced every node, which silently aborted a
|
|
396
|
+
drag-in-progress and lost focus and input state.)
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
// Item updates preserve node identity. Drag-and-drop, focus, and
|
|
400
|
+
// input state all survive. No repeat() needed.
|
|
401
|
+
render() {
|
|
402
|
+
return html`<ul>${this.cards.map((c) => html`
|
|
403
|
+
<li class=${c.id === this.draggingId ? 'dragging' : 'idle'}
|
|
404
|
+
draggable="true"
|
|
405
|
+
@dragstart=${() => (this.draggingId = c.id)}>${c.text}</li>`)}</ul>`;
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
What plain `.map()` still does NOT do is **keyed reordering**.
|
|
410
|
+
Reconciliation is positional (by index): if the array is reordered or an
|
|
411
|
+
item is inserted/removed in the MIDDLE, index *i* is patched from the new
|
|
412
|
+
item at *i*, so the nodes stay put and their contents are rewritten
|
|
413
|
+
rather than the nodes themselves moving. For an item carrying live state
|
|
414
|
+
(a focused input, a playing media element, an in-flight CSS transition)
|
|
415
|
+
across a reorder, that state stays with the old position. When a list
|
|
416
|
+
**reorders** or splices in the middle and node identity must follow the
|
|
417
|
+
item, reach for the keyed directive, exactly as in lit:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { repeat } from '@webjsdev/core/directives';
|
|
421
|
+
render() {
|
|
422
|
+
return html`<ul>${repeat(this.cards, (c) => c.id, (c) => html`
|
|
423
|
+
<li>${c.text}</li>`)}</ul>`;
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Rule of thumb. Append-only or update-in-place list, where items keep
|
|
428
|
+
their position, plain `.map()` is fine and preserves identity. List that
|
|
429
|
+
**reorders or splices in the middle** and each item owns DOM state that
|
|
430
|
+
must move with it, use `repeat()` with a stable key.
|
|
431
|
+
|
|
432
|
+
## Quick reference
|
|
433
|
+
|
|
434
|
+
| Lit pattern | Webjs equivalent |
|
|
435
|
+
|---|---|
|
|
436
|
+
| Fetch in `connectedCallback` or `firstUpdated` | Fetch in the page function, pass as props |
|
|
437
|
+
| `Task` for initial-paint data | Page function fetch and pass as props |
|
|
438
|
+
| `Task` for client-time async | `Task` (no change, that's its job) |
|
|
439
|
+
| `window.X` or `document.X` in constructor or `render()` | Move to `connectedCallback` |
|
|
440
|
+
| Top-level `import` of browser-only library | Dynamic `import()` inside `connectedCallback` |
|
|
441
|
+
| `student: Student = { ... }` field initializer | `declare student: Student` plus constructor default |
|
|
442
|
+
| `@property()` decorator | `static properties = { ... }` plus `declare` |
|
|
443
|
+
| `static styles = css` / inline `<style>` with semantic class names in a light-DOM component | Tailwind utilities (the default); or `static shadow = true` for genuinely scoped CSS |
|
|
444
|
+
| Plain `.map()` for an interactive/stateful list | Works (reconciles in place, keeps node identity); use `repeat(items, key, t)` only when the list **reorders** |
|
|
445
|
+
| `willUpdate` for SSR-visible derived state | Works (runs at SSR); keep it a pure derivation |
|
|
446
|
+
| `this.hasAttribute` / `getAttribute` in `render()` | Works (server attribute shim) |
|
|
447
|
+
| `ContextProvider` for server-known data | Pass via props from the page function |
|
|
448
|
+
|
|
449
|
+
## When in doubt
|
|
450
|
+
|
|
451
|
+
If a pattern needs to influence the first paint, it has to be in the
|
|
452
|
+
constructor, the page function, or `render()`. If a pattern needs the
|
|
453
|
+
DOM, the event loop, or a browser API, it has to be in
|
|
454
|
+
`connectedCallback` or later. There is no third category. Anything
|
|
455
|
+
that violates this split either crashes SSR or produces a hydration
|
|
456
|
+
flash.
|