@webjsdev/cli 0.10.12 → 0.10.13
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/README.md +1 -1
- package/bin/webjs.js +19 -13
- package/lib/create.js +24 -18
- package/package.json +3 -7
- package/templates/.claude.json +1 -1
- package/templates/AGENTS.md +17 -15
- package/templates/CONVENTIONS.md +5 -5
- package/lib/check-json.js +0 -47
- package/lib/mcp-docs.js +0 -400
- package/lib/mcp-source.js +0 -244
- package/lib/mcp.js +0 -557
- package/resources/AGENTS.md +0 -404
- package/resources/agent-docs/advanced.md +0 -1090
- package/resources/agent-docs/built-ins.md +0 -367
- package/resources/agent-docs/components.md +0 -486
- package/resources/agent-docs/configuration.md +0 -207
- package/resources/agent-docs/framework-dev.md +0 -65
- package/resources/agent-docs/lit-muscle-memory-gotchas.md +0 -456
- package/resources/agent-docs/metadata.md +0 -334
- package/resources/agent-docs/recipes.md +0 -440
- package/resources/agent-docs/service-worker.md +0 -100
- package/resources/agent-docs/ssr-partial-nav-design.md +0 -214
- package/resources/agent-docs/styling.md +0 -235
- package/resources/agent-docs/testing.md +0 -372
- package/resources/agent-docs/typescript.md +0 -334
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
# WebComponent deep-dive
|
|
2
|
-
|
|
3
|
-
## Property options in full detail
|
|
4
|
-
|
|
5
|
-
| Option | Type | Default | Meaning |
|
|
6
|
-
|---|---|---|---|
|
|
7
|
-
| `type` | `Number\|String\|Boolean\|Object\|Array` | `String` | Used by the default attribute converter |
|
|
8
|
-
| `reflect` | `boolean` | `false` | Property changes write back to the HTML attribute |
|
|
9
|
-
| `state` | `boolean` | `false` | Internal-only. No attribute, not in `observedAttributes` |
|
|
10
|
-
| `hasChanged` | `(newVal, oldVal) => boolean` | strict `!==` | Custom change detection |
|
|
11
|
-
| `converter` | `{ fromAttribute?, toAttribute? }` | type-based | Custom attribute ↔ property serialization |
|
|
12
|
-
|
|
13
|
-
Built-in constructors (`String`, `Number`, `Boolean`, `Array`, `Object`) feed
|
|
14
|
-
the default attribute coercion. For anything the default can't parse correctly
|
|
15
|
-
(Date, Map, Set, discriminated unions) supply a custom `converter`.
|
|
16
|
-
|
|
17
|
-
## Why `declare` is required in TypeScript
|
|
18
|
-
|
|
19
|
-
The framework installs reactive getter/setter on `this` inside the
|
|
20
|
-
constructor via `Object.defineProperty`. Without `declare`, TypeScript
|
|
21
|
-
emits `student = undefined` after `super()`, which under modern class-
|
|
22
|
-
field semantics uses `[[Define]]` to overwrite the accessor. Result:
|
|
23
|
-
`this.student = …` no longer goes through the setter, no `requestUpdate`,
|
|
24
|
-
no `hasChanged`, no reflect, and reactivity silently breaks.
|
|
25
|
-
|
|
26
|
-
The `.d.ts` overlay shipped with the framework makes every other class
|
|
27
|
-
member fully typed, so only the reactive properties need the `declare`
|
|
28
|
-
line, and only in TypeScript files.
|
|
29
|
-
|
|
30
|
-
## Lifecycle hooks (lit-aligned)
|
|
31
|
-
|
|
32
|
-
`WebComponent` ships lit's full reactive lifecycle. Every update cycle runs these hooks in order; each receives a `changedProperties` Map (`Map<string, oldValue>`, where keys are reactive-property names).
|
|
33
|
-
|
|
34
|
-
| # | Hook | When |
|
|
35
|
-
|---|---|---|
|
|
36
|
-
| 1 | `shouldUpdate(changedProperties)` | Return `false` to skip the update. Default `true`. |
|
|
37
|
-
| 2 | `willUpdate(changedProperties)` | Pre-render. Property assignments here fold into THIS cycle. |
|
|
38
|
-
| 3 | controllers' `hostUpdate()` | Pre-render controller hook |
|
|
39
|
-
| 4 | `update(changedProperties)` | Default calls `render()` + commits. Override to wrap or short-circuit (rare). |
|
|
40
|
-
| 5 | controllers' `hostUpdated()` | Post-render controller hook |
|
|
41
|
-
| 6 | `firstUpdated(changedProperties)` | Once, on the first render only |
|
|
42
|
-
| 7 | `updated(changedProperties)` | Every render commit. Right place for ad-hoc post-render DOM work. |
|
|
43
|
-
| 8 | `updateComplete` Promise resolves | `await el.updateComplete` to read post-render DOM in tests |
|
|
44
|
-
|
|
45
|
-
Assignments during `willUpdate` fold into the current cycle (no new render scheduled); assignments during `updated` or `firstUpdated` queue a fresh cycle. The framework gates this via an internal flag, so authors don't manage it.
|
|
46
|
-
|
|
47
|
-
The SSR pipeline runs the **pre-render value-deriving hooks** before `render()`: `willUpdate` (so derived state is in the first paint) and controllers' `hostUpdate`, then it reflects `reflect: true` properties to attributes. The rest stay **client-only** and SSR does not invoke them: `shouldUpdate`, the `update` DOM commit, `hostUpdated`, `updated`, `firstUpdated`, `connectedCallback`, `disconnectedCallback`. Set SSR-meaningful defaults in the constructor, derive SSR-visible state in `willUpdate`, and keep browser-only work (DOM queries, layout, localStorage, viewport) in `connectedCallback` / `firstUpdated`. A `Task` is the one controller whose `hostUpdate` does not act at SSR: it ships the `INITIAL` state and runs only on hydration, so no request fires server-side.
|
|
48
|
-
|
|
49
|
-
For component-local state, create an instance signal in the constructor and call `signal.set(...)` to mutate. The built-in `SignalWatcher` re-runs `render()` on the next microtask; the same lifecycle hooks fire as for reactive-property changes.
|
|
50
|
-
|
|
51
|
-
See [`/docs/lifecycle`](https://docs.webjs.com/docs/lifecycle) for per-hook usage examples.
|
|
52
|
-
|
|
53
|
-
## Display-only components are elided from the browser
|
|
54
|
-
|
|
55
|
-
A component that does no client-side work renders the same SSR'd HTML
|
|
56
|
-
whether or not its JavaScript ever reaches the browser. webjs detects
|
|
57
|
-
these statically and strips their import from the served source, so the
|
|
58
|
-
browser never downloads them (and their unique vendor dependencies drop
|
|
59
|
-
from the importmap). This is automatic, with no opt-in keyword and no
|
|
60
|
-
server/client split to reason about. A component stays elidable as long
|
|
61
|
-
as it has none of the following.
|
|
62
|
-
|
|
63
|
-
- An `@event` binding in a template (`@click=${...}`), or a native event-handler property (`.onclick=${...}`).
|
|
64
|
-
- A reactive property in `static properties` that is not `{ state: true }`. Attribute-driven or `.prop`-driven values are the channel a parent uses to push client updates.
|
|
65
|
-
- An overridden lifecycle hook (anything in the table above), as a method or an arrow class field.
|
|
66
|
-
- A `signal` / `computed` / `watch` / `Task` / `ref` / `live` / streaming directive imported from `@webjsdev/core`, OR a transitive import of a module that reads shared module-scope signal state.
|
|
67
|
-
- An `addController(...)` or `requestUpdate()` call.
|
|
68
|
-
- Any code that runs at module load. A display-only module's top level may only *declare* things (imports, the `WebComponent` class, `const` / `let` / `var`, pure initializers like `css\`...\``) and *register* the component (`X.register(...)` / `customElements.define(...)`). Any other top-level call, `new`, dynamic `import(...)`, or top-level `await` is client work and ships (a top-level `fetch('/track')`, `new WebSocket(...)`, `setTimeout(...)`, `someInit()`). This is checked structurally as an allowlist of safe top-level forms, not a denylist of global names, so a brand-new browser API is caught automatically with no code change. Code inside a method, `render()`, or an uninvoked function does not count (it does not run at load), nor do these words in rendered template text or a `.fetch` / `.location` member access.
|
|
69
|
-
- A rendered `<slot>`. Light-DOM slots rely on the client projection runtime, and proving a slot is purely native (shadow DOM) is beyond static analysis, so any `<slot>` ships.
|
|
70
|
-
- Being rendered or imported by a component that itself ships (an interactive parent can re-create the child on the client).
|
|
71
|
-
|
|
72
|
-
The analysis is deliberately conservative: anything it cannot prove
|
|
73
|
-
inert ships normally, so correctness never depends on it. The elidable
|
|
74
|
-
case in practice is a component with no inputs and no behavior: static
|
|
75
|
-
markup, or values seeded in the constructor. Note a slotted wrapper does
|
|
76
|
-
NOT qualify (the `<slot>` itself forces shipping per the list above).
|
|
77
|
-
|
|
78
|
-
**The one boundary the static model cannot see.** Elision proves a
|
|
79
|
-
component's own `render()` is inert; it does NOT prove that no *other*
|
|
80
|
-
client code observes the element's registration. An elided module never
|
|
81
|
-
loads, so its `customElements.define` never runs in the browser and the
|
|
82
|
-
tag stays an un-upgraded `HTMLElement`. That is invisible for a tag that
|
|
83
|
-
exists only as SSR'd markup, but it changes behavior if shipping client
|
|
84
|
-
code depends on the definition:
|
|
85
|
-
|
|
86
|
-
- `customElements.whenDefined('the-tag')` never resolves.
|
|
87
|
-
- reading an upgraded property or method off `document.querySelector('the-tag')` is `undefined` / throws.
|
|
88
|
-
- `el instanceof TheClass` is `false`.
|
|
89
|
-
- a CSS `the-tag:defined { … }` rule never matches.
|
|
90
|
-
|
|
91
|
-
**The three statically visible forms are now detected and force the
|
|
92
|
-
observed component to ship**: a literal `whenDefined('the-tag')`, a CSS
|
|
93
|
-
`the-tag:defined` selector, and `instanceof TheClass` (mapped back to the
|
|
94
|
-
tag via the component's class name) anywhere in a graph-reachable module
|
|
95
|
-
mark `the-tag`'s component as must-ship, so it is never elided. The bias
|
|
96
|
-
stays conservative: detection only ever forces MORE components to ship.
|
|
97
|
-
|
|
98
|
-
What remains an author-facing caveat is the part static analysis cannot
|
|
99
|
-
see: a tag built from a dynamic string (`whenDefined(\`x-\${name}\`)`), or
|
|
100
|
-
a `:defined` rule in an external stylesheet that is not part of the module
|
|
101
|
-
graph. If you observe a component that way, add an interactivity signal
|
|
102
|
-
(an `@event`, a non-`state` reactive property, or a lifecycle hook) so it
|
|
103
|
-
ships. In idiomatic webjs this is rare: a display-only element is
|
|
104
|
-
server-rendered to its final HTML and read as plain markup, and
|
|
105
|
-
`:defined` FOUC-hiding works against progressive enhancement (it would
|
|
106
|
-
hide content that already painted). But if you reach for those dynamic
|
|
107
|
-
patterns, treat the component as interactive.
|
|
108
|
-
|
|
109
|
-
The detection lists live in `packages/server/src/component-elision.js`
|
|
110
|
-
and are the single source of truth. They are kept in lockstep with the
|
|
111
|
-
lifecycle table above by `packages/server/test/elision/lifecycle-coverage.test.js`,
|
|
112
|
-
which fails if a new `WebComponent` hook is added without teaching the
|
|
113
|
-
analyser about it. If you add an interactivity feature to the framework,
|
|
114
|
-
update that file.
|
|
115
|
-
|
|
116
|
-
### Turning elision off
|
|
117
|
-
|
|
118
|
-
Elision is on by default. To disable it app-wide, set `elide` to `false`
|
|
119
|
-
under the `webjs` key in `package.json`:
|
|
120
|
-
|
|
121
|
-
```jsonc
|
|
122
|
-
{ "webjs": { "elide": false } }
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
With the switch off, every component and route module ships exactly as it
|
|
126
|
-
did before the feature existed (no import stripping, no dropped preloads,
|
|
127
|
-
the importmap keeps every vendor dep). The switch is pure opt-out, so any
|
|
128
|
-
value other than the literal `false`, or an absent key, leaves elision on.
|
|
129
|
-
Reach for it if the conservative analyser ever mis-elides a component, or
|
|
130
|
-
to A/B the wire-byte difference. Because the analyser biases toward
|
|
131
|
-
shipping, needing this should be rare.
|
|
132
|
-
|
|
133
|
-
There is also a `WEBJS_ELIDE` environment override that wins over the
|
|
134
|
-
`package.json` switch: `WEBJS_ELIDE=0` (also `false` / `off` / `no`) forces
|
|
135
|
-
elision off, `WEBJS_ELIDE=1` (`true` / `on` / `yes`) forces it on, and any
|
|
136
|
-
other value (or an unset variable) falls through to the `package.json`
|
|
137
|
-
switch. It is the deploy-time escape hatch (rule elision out while
|
|
138
|
-
debugging a suspected wrong-strip without editing committed code) and the
|
|
139
|
-
seam the differential elision test uses to render the same app on and off
|
|
140
|
-
in one process. Like the `package.json` switch, it is re-read on every
|
|
141
|
-
rebuild.
|
|
142
|
-
|
|
143
|
-
### The differential guard: elision never changes observable output
|
|
144
|
-
|
|
145
|
-
Elision's defining invariant is that removing the elided JS NEVER changes
|
|
146
|
-
what the user sees or can do (the SSR'd HTML is the progressive-enhancement
|
|
147
|
-
baseline; elision only drops JS that would have done nothing). Because the
|
|
148
|
-
analyser is heuristic and its long tail of inputs (comments, dynamic tag
|
|
149
|
-
strings, multi-line templates, vendor side-effects, future interactivity
|
|
150
|
-
surfaces) is open-ended, that invariant is verified DIFFERENTIALLY rather
|
|
151
|
-
than only by example: a test renders a corpus of routes with elision on and
|
|
152
|
-
off and asserts the observable output is identical, both at the SSR layer
|
|
153
|
-
(served HTML, modulo the boot script and modulepreload JS set) and in a
|
|
154
|
-
real browser after hydration (DOM and key interactions). The conservative
|
|
155
|
-
bias means a mistake almost always only over-ships (wastes bytes, ignored
|
|
156
|
-
by the diff); the dangerous direction (a needed module wrongly dropped)
|
|
157
|
-
changes post-hydration behaviour and fails the e2e diff loudly. This is the
|
|
158
|
-
guard that lets per-component elision stay a safe default rather than a
|
|
159
|
-
leap of faith, and it is what would have caught the comment-scanning (#179)
|
|
160
|
-
and cross-module-observation (#169) bug classes instantly. The test lives
|
|
161
|
-
at `packages/server/test/elision/differential-elision.test.js` (SSR layer)
|
|
162
|
-
and the `differential elision` cases in `test/e2e/e2e.test.mjs` (browser
|
|
163
|
-
layer).
|
|
164
|
-
|
|
165
|
-
## ReactiveControllers: composable lifecycle
|
|
166
|
-
|
|
167
|
-
```js
|
|
168
|
-
class FetchController {
|
|
169
|
-
constructor(host, url) {
|
|
170
|
-
this.host = host;
|
|
171
|
-
this.url = url;
|
|
172
|
-
this.data = null;
|
|
173
|
-
host.addController(this); // ← register
|
|
174
|
-
}
|
|
175
|
-
async hostConnected() {
|
|
176
|
-
this.data = await (await fetch(this.url)).json();
|
|
177
|
-
this.host.requestUpdate();
|
|
178
|
-
}
|
|
179
|
-
hostDisconnected() { /* cleanup */ }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
class MyEl extends WebComponent {
|
|
183
|
-
#users = new FetchController(this, '/api/users');
|
|
184
|
-
render() { return html`${this.#users.data?.length} users`; }
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Use controllers when the same lifecycle logic (fetch, timer, subscription,
|
|
189
|
-
resize observer) is needed in multiple unrelated components. The built-in
|
|
190
|
-
`Task`, `ContextProvider`, and `ContextConsumer` are all controllers.
|
|
191
|
-
|
|
192
|
-
## Light DOM (default) vs Shadow DOM (opt-in), full detail
|
|
193
|
-
|
|
194
|
-
Light DOM is the default because global CSS and Tailwind utility classes
|
|
195
|
-
apply directly, with no `::part`, no `:host`, no CSS-var plumbing, no
|
|
196
|
-
`adoptedStyleSheets` needed. The browser renders a plain element with
|
|
197
|
-
normal children, and hydration replaces SSR content in place.
|
|
198
|
-
|
|
199
|
-
| Use case | Mode | How |
|
|
200
|
-
|---|---|---|
|
|
201
|
-
| Global / Tailwind CSS, simple composition | **Light DOM** (default) | Just use `class="..."` in your `html\`...\`` template |
|
|
202
|
-
| Scoped styles via `static styles = css\`\`` | Shadow DOM | Set `static shadow = true`. `adoptedStyleSheets` + bare selectors are scoped |
|
|
203
|
-
| `<slot>` content projection | **Both** | Same `<slot>` / `<slot name="x">` syntax. Light DOM uses framework projection; shadow DOM uses native browser projection. Full spec parity in both modes (see "Slots" section below). |
|
|
204
|
-
| Third-party embeds needing isolation | Shadow DOM | CSS can't leak in or out |
|
|
205
|
-
|
|
206
|
-
Both modes are fully SSR'd (shadow DOM via Declarative Shadow DOM, light
|
|
207
|
-
DOM as direct HTML with a `<!--webjs-hydrate-->` marker) and hydrate
|
|
208
|
-
without flash on the client.
|
|
209
|
-
|
|
210
|
-
### Class-prefix rule for light-DOM components
|
|
211
|
-
|
|
212
|
-
If a light-DOM component authors its own custom CSS (a `<style>` block
|
|
213
|
-
inside `render()`, or an imported stylesheet), every class selector MUST
|
|
214
|
-
be prefixed with the component's tag name. Pick one of these two
|
|
215
|
-
patterns per component:
|
|
216
|
-
|
|
217
|
-
```ts
|
|
218
|
-
// Pattern A: BEM-ish class names prefixed with tag
|
|
219
|
-
class MyCard extends WebComponent {
|
|
220
|
-
render() {
|
|
221
|
-
return html`
|
|
222
|
-
<style>
|
|
223
|
-
.my-card__body { padding: 16px; }
|
|
224
|
-
.my-card__title { font-weight: 600; }
|
|
225
|
-
</style>
|
|
226
|
-
<div class="my-card__body">
|
|
227
|
-
<h3 class="my-card__title"><slot name="title"></slot></h3>
|
|
228
|
-
</div>
|
|
229
|
-
`;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Pattern B: descendant selector rooted at the tag
|
|
234
|
-
class MyCard extends WebComponent {
|
|
235
|
-
render() {
|
|
236
|
-
return html`
|
|
237
|
-
<style>
|
|
238
|
-
my-card .body { padding: 16px; }
|
|
239
|
-
my-card .title { font-weight: 600; }
|
|
240
|
-
</style>
|
|
241
|
-
<div class="body">
|
|
242
|
-
<h3 class="title"><slot name="title"></slot></h3>
|
|
243
|
-
</div>
|
|
244
|
-
`;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Prefer Tailwind utility classes first. They're unique by construction.
|
|
250
|
-
Drop down to custom CSS only when Tailwind can't express it.
|
|
251
|
-
|
|
252
|
-
### When to opt in to shadow DOM
|
|
253
|
-
|
|
254
|
-
Set `static shadow = true` when:
|
|
255
|
-
- You author styles via `static styles = css\`...\`` and want them
|
|
256
|
-
`adoptedStyleSheets`-scoped without a prefix discipline.
|
|
257
|
-
- You're publishing a component for third parties who won't have your
|
|
258
|
-
Tailwind build, and you need the embed to look right in any host.
|
|
259
|
-
- You want the browser's built-in `::slotted()` CSS selector for
|
|
260
|
-
styling projected children from inside the shadow tree.
|
|
261
|
-
|
|
262
|
-
Slots themselves are no longer a reason to opt into shadow DOM. The
|
|
263
|
-
same `<slot>` / `<slot name="x">` syntax works in light DOM with full
|
|
264
|
-
shadow-DOM spec parity (`assignedNodes`, `assignedElements`,
|
|
265
|
-
`assignedSlot`, `slotchange`, named slots, fallback content, first-wins
|
|
266
|
-
resolution). See the "Slots" section below.
|
|
267
|
-
|
|
268
|
-
`static styles` on a light-DOM component is silently ignored.
|
|
269
|
-
|
|
270
|
-
## Slots: full shadow-DOM parity in both DOM modes
|
|
271
|
-
|
|
272
|
-
webjs supports the entire shadow-DOM `<slot>` surface in light DOM. The
|
|
273
|
-
same `render()` template projects children identically whether your
|
|
274
|
-
component declares `static shadow = true` or leaves it at the default
|
|
275
|
-
`false`. Migrating between modes never requires a template rewrite.
|
|
276
|
-
|
|
277
|
-
### Syntax
|
|
278
|
-
|
|
279
|
-
```ts
|
|
280
|
-
class MyCard extends WebComponent {
|
|
281
|
-
// static shadow defaults to false. Either value works for everything
|
|
282
|
-
// below.
|
|
283
|
-
render() {
|
|
284
|
-
return html`
|
|
285
|
-
<header><slot name="header"></slot></header>
|
|
286
|
-
<main><slot></slot></main>
|
|
287
|
-
<footer><slot name="footer">no actions</slot></footer>
|
|
288
|
-
`;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
MyCard.register('my-card');
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Author markup:
|
|
295
|
-
|
|
296
|
-
```html
|
|
297
|
-
<my-card>
|
|
298
|
-
<h2 slot="header">Title</h2>
|
|
299
|
-
<p>Body content</p>
|
|
300
|
-
<p>More body content</p>
|
|
301
|
-
<button slot="footer">Save</button>
|
|
302
|
-
</my-card>
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
The `<h2>` projects into the `header` slot, both `<p>` elements into the
|
|
306
|
-
default slot in source order, and the `<button>` into the `footer` slot.
|
|
307
|
-
|
|
308
|
-
### Default slot
|
|
309
|
-
|
|
310
|
-
A `<slot>` without a `name` attribute receives all authored children
|
|
311
|
-
without a `slot=""` attribute. Text nodes, comments, and whitespace also
|
|
312
|
-
route to the default slot.
|
|
313
|
-
|
|
314
|
-
```ts
|
|
315
|
-
class Wrapper extends WebComponent {
|
|
316
|
-
render() { return html`<div><slot></slot></div>`; }
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
```html
|
|
321
|
-
<wrapper>
|
|
322
|
-
Plain text
|
|
323
|
-
<p>An element</p>
|
|
324
|
-
<!-- a comment -->
|
|
325
|
-
</wrapper>
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### Named slot
|
|
329
|
-
|
|
330
|
-
`<slot name="x">` receives authored children with `slot="x"`. A child
|
|
331
|
-
with `slot=""` (empty string) routes to the default slot, matching the
|
|
332
|
-
shadow-DOM spec.
|
|
333
|
-
|
|
334
|
-
### Fallback content
|
|
335
|
-
|
|
336
|
-
A slot's authored inner content is its fallback. If no children match
|
|
337
|
-
the slot, the fallback renders.
|
|
338
|
-
|
|
339
|
-
```ts
|
|
340
|
-
render() { return html`<slot name="actions">no actions</slot>`; }
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
When no `slot="actions"` child is provided, the slot shows "no actions".
|
|
344
|
-
When projection happens, the fallback is replaced by the projected
|
|
345
|
-
content.
|
|
346
|
-
|
|
347
|
-
### First-wins resolution
|
|
348
|
-
|
|
349
|
-
Multiple slots with the same `name` (or multiple default slots) are
|
|
350
|
-
permitted. Per shadow-DOM spec, the first one in document order receives
|
|
351
|
-
the assignment; subsequent same-named slots show their fallback content.
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
render() {
|
|
355
|
-
return html`
|
|
356
|
-
<slot name="title">Untitled</slot>
|
|
357
|
-
<slot name="title">never shown</slot>
|
|
358
|
-
`;
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### Dynamic slot name and child slot attribute
|
|
363
|
-
|
|
364
|
-
A slot's `name` attribute can be a template hole. Re-projection happens
|
|
365
|
-
automatically when the value changes. Likewise, a child's `slot=""`
|
|
366
|
-
attribute can change at runtime; the child re-routes to the new slot.
|
|
367
|
-
|
|
368
|
-
```ts
|
|
369
|
-
render() {
|
|
370
|
-
return html`<slot name=${this.section}></slot>`;
|
|
371
|
-
}
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### DOM API
|
|
375
|
-
|
|
376
|
-
Every shadow-DOM slot API is mirrored on light-DOM slots:
|
|
377
|
-
|
|
378
|
-
| API | Returns |
|
|
379
|
-
|---|---|
|
|
380
|
-
| `slot.assignedNodes(options?)` | Projected nodes in source order; empty array when slot shows fallback |
|
|
381
|
-
| `slot.assignedNodes({ flatten: true })` | Recursively unwraps nested forwarding slots to the leaf nodes |
|
|
382
|
-
| `slot.assignedElements(options?)` | Element-only filter of `assignedNodes` |
|
|
383
|
-
| `element.assignedSlot` | Returns the slot a child is projected into, or `null` |
|
|
384
|
-
| `slotchange` event | Fires on a slot when its assigned-node set actually changes (with equality detection to avoid no-op fires) |
|
|
385
|
-
|
|
386
|
-
The polyfills are gated on a `data-webjs-light` attribute that the
|
|
387
|
-
framework places on its slots, so the polyfill never interferes with
|
|
388
|
-
real shadow-DOM slots elsewhere on the page.
|
|
389
|
-
|
|
390
|
-
### SSR + hydration
|
|
391
|
-
|
|
392
|
-
Both modes are SSR'd:
|
|
393
|
-
|
|
394
|
-
- **Light DOM.** The server emits projected children directly inside
|
|
395
|
-
`<slot data-webjs-light data-projection="actual">` elements. Without
|
|
396
|
-
JavaScript, the page renders correctly because the projection is
|
|
397
|
-
baked into the HTML. On hydration the framework adopts the SSR-placed
|
|
398
|
-
Node references; DOM identity (event listeners, focus, scroll, input
|
|
399
|
-
values) survives the round-trip.
|
|
400
|
-
- **Shadow DOM.** The server emits Declarative Shadow DOM
|
|
401
|
-
(`<template shadowrootmode="open">…<slot>…</slot>…</template>`). The
|
|
402
|
-
browser opens the shadow root on parse and projects natively, again
|
|
403
|
-
without JavaScript.
|
|
404
|
-
|
|
405
|
-
### Compound components read their parent via `closest()` at SSR
|
|
406
|
-
|
|
407
|
-
A compound component (a tabs trigger, a toggle-group item) typically
|
|
408
|
-
derives its active/pressed state by walking to the parent and reading
|
|
409
|
-
its value:
|
|
410
|
-
|
|
411
|
-
```ts
|
|
412
|
-
get _tabs() { return this.closest('ui-tabs'); }
|
|
413
|
-
render() {
|
|
414
|
-
const active = this._tabs?.value === this.value;
|
|
415
|
-
this.dataset.state = active ? 'active' : 'inactive';
|
|
416
|
-
return html`<button data-state=${active ? 'active' : 'inactive'}><slot></slot></button>`;
|
|
417
|
-
}
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
This works in the **first server paint**, not only after hydration. The
|
|
421
|
-
SSR walker threads the chain of enclosing custom-element instances into
|
|
422
|
-
each instance, and the server element shim's `closest()` resolves a
|
|
423
|
-
parent over that chain (so `this.closest('ui-tabs').value` reads the
|
|
424
|
-
live parent property the walker already applied). Host IDL properties a
|
|
425
|
-
`render()` mutates on `this` (`this.dataset.*`, `this.className`,
|
|
426
|
-
`this.hidden`, `this.ariaPressed`, the rest of the `aria*` mixin)
|
|
427
|
-
reflect to the matching attribute on the SSR'd host tag, so the active
|
|
428
|
-
tab is marked before any JavaScript runs. The first client render
|
|
429
|
-
produces the identical state (the browser's real `closest()` against the
|
|
430
|
-
real DOM), so there is no hydration flash.
|
|
431
|
-
|
|
432
|
-
Limits:
|
|
433
|
-
|
|
434
|
-
- Only **tag-name selectors** resolve at SSR (`closest('ui-tabs')`). A
|
|
435
|
-
class, attribute, or descendant selector returns null server-side and
|
|
436
|
-
resolves on the client. That covers the compound-component pattern;
|
|
437
|
-
anything finer is client-only.
|
|
438
|
-
- The compound **parent** must be light DOM (the default, and what every
|
|
439
|
-
kit Tier-2 component uses). A shadow-DOM parent projects its children
|
|
440
|
-
through a native `<slot>`, and those slotted children are not threaded
|
|
441
|
-
the SSR ancestor chain, so their `closest(parent)` resolves to null in
|
|
442
|
-
the first server paint (it still resolves on the client after
|
|
443
|
-
hydration). Keep compound parents light DOM for a correct first paint.
|
|
444
|
-
- Genuine layout / live-DOM reads (`querySelector`, `classList`,
|
|
445
|
-
`attachShadow`, geometry) still throw at SSR, so keep them in
|
|
446
|
-
`connectedCallback` / `firstUpdated`.
|
|
447
|
-
|
|
448
|
-
### Slot inside conditionals and lists
|
|
449
|
-
|
|
450
|
-
A slot can live inside any `html\`\`` template fragment: conditional
|
|
451
|
-
ternaries, `${repeat()}` iterations, async `Task` results. When a slot
|
|
452
|
-
disappears (e.g., its containing template collapses), the projected
|
|
453
|
-
children move to a per-host pending map and re-attach with DOM identity
|
|
454
|
-
preserved when the slot reappears.
|
|
455
|
-
|
|
456
|
-
```ts
|
|
457
|
-
render() {
|
|
458
|
-
return html`
|
|
459
|
-
<div>
|
|
460
|
-
${this.expanded
|
|
461
|
-
? html`<section><slot></slot></section>`
|
|
462
|
-
: html`<i>collapsed</i>`}
|
|
463
|
-
</div>
|
|
464
|
-
`;
|
|
465
|
-
}
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
Toggling `this.expanded` between true and false preserves the projected
|
|
469
|
-
child Node references.
|
|
470
|
-
|
|
471
|
-
### Composition with Suspense
|
|
472
|
-
|
|
473
|
-
A slot composes naturally with `Suspense`. Authored children that
|
|
474
|
-
include `${Suspense({ fallback, children })}` project the fallback HTML
|
|
475
|
-
into the slot at SSR time; when the children promise resolves and
|
|
476
|
-
streams in, the `data-webjs-resolve` swap targets the
|
|
477
|
-
`<webjs-boundary>` element which lives inside the slot, updating the
|
|
478
|
-
slot's content in place.
|
|
479
|
-
|
|
480
|
-
## Helper methods
|
|
481
|
-
|
|
482
|
-
| Method | Purpose |
|
|
483
|
-
|---|---|
|
|
484
|
-
| `signal.set(v)` (instance signal) | Component-local reactive state; auto-tracked by SignalWatcher |
|
|
485
|
-
| `this.requestUpdate()` | Manually schedule a re-render (controllers) |
|
|
486
|
-
| `this.shadowRoot.querySelector(sel)` | Query shadow DOM (native API) |
|