@excom/renderable-element 0.1.0

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.
Files changed (40) hide show
  1. package/.rush/temp/chunked-rush-logs/renderable-element.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/renderable-element.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/renderable-element.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +3 -0
  14. package/config/rig.json +6 -0
  15. package/index.ts +638 -0
  16. package/package.json +48 -0
  17. package/rush-logs/renderable-element.apply-exports.cache.log +1 -0
  18. package/rush-logs/renderable-element.apply-exports.log +1 -0
  19. package/rush-logs/renderable-element.build_docs.cache.log +1 -0
  20. package/rush-logs/renderable-element.build_docs.log +1 -0
  21. package/rush-logs/renderable-element.build_package-metas.cache.log +1 -0
  22. package/rush-logs/renderable-element.build_package-metas.log +1 -0
  23. package/src/index.css +11 -0
  24. package/support/custom-elements.json +371 -0
  25. package/support/demos/host-iframe.html +26 -0
  26. package/support/demos/host-selector.html +29 -0
  27. package/support/demos/host-shadow.html +31 -0
  28. package/support/demos/persist-content.html +57 -0
  29. package/support/demos/render-event.html +66 -0
  30. package/support/dist-docs/renderable-element.md +499 -0
  31. package/support/docs/README.md +146 -0
  32. package/support/package-meta.json +206 -0
  33. package/support/tests/host-iframe.view.test.ts +22 -0
  34. package/support/tests/host-selector.view.test.ts +19 -0
  35. package/support/tests/host-shadow.view.test.ts +19 -0
  36. package/support/tests/persist-content.view.test.ts +31 -0
  37. package/support/tests/render-event.view.test.ts +24 -0
  38. package/support/tests/render-lifecycle.test.ts +961 -0
  39. package/support/tests/renderable-element.test.ts +222 -0
  40. package/tsconfig.json +5 -0
@@ -0,0 +1,499 @@
1
+ # renderable-element
2
+
3
+ Composition base for Neutron elements that defer rendering a `<template>`
4
+ until the right moment. It owns the load + render lifecycle so subclasses
5
+ only decide *when* to flip `is-active`. Used by `<include-content>`,
6
+ `<spa-route>`, and any custom element you compose yourself.
7
+
8
+ The demos below use `<include-content>` (the simplest concrete subclass)
9
+ to exercise behavior that comes straight from this mixin.
10
+
11
+
12
+ ```html
13
+ <p>Type into both inputs, then toggle them off and on. The right side
14
+ keeps its value because <code>persist-content</code> reuses the same
15
+ template node instead of cloning a fresh copy each render.</p>
16
+
17
+ <style>
18
+ .persist-demo {
19
+ display: grid;
20
+ grid-template-columns: 1fr 1fr;
21
+ gap: 12px;
22
+ }
23
+ .persist-demo include-content {
24
+ display: block;
25
+ border: 1px dashed currentColor;
26
+ padding: 8px;
27
+ margin-top: 4px;
28
+ }
29
+ </style>
30
+
31
+ <div class="persist-demo" id="persist-host">
32
+ <div>
33
+ <strong>Default (cloned each render):</strong>
34
+ <label>
35
+ <input type="checkbox" data-target="#cloned" checked />
36
+ Active
37
+ </label>
38
+ <include-content id="cloned" is-active>
39
+ <template>
40
+ <input type="text" placeholder="Type, then toggle off + on" />
41
+ </template>
42
+ </include-content>
43
+ </div>
44
+
45
+ <div>
46
+ <strong><code>persist-content</code>:</strong>
47
+ <label>
48
+ <input type="checkbox" data-target="#persisted" checked />
49
+ Active
50
+ </label>
51
+ <include-content id="persisted" is-active persist-content>
52
+ <template>
53
+ <input type="text" placeholder="Type, then toggle off + on" />
54
+ </template>
55
+ </include-content>
56
+ </div>
57
+ </div>
58
+
59
+ <script>
60
+ (() => {
61
+ document.querySelectorAll("#persist-host input[type=checkbox]").forEach((cb) => {
62
+ cb.addEventListener("change", () => {
63
+ document
64
+ .querySelector(cb.dataset.target)
65
+ .toggleAttribute("is-active", cb.checked);
66
+ });
67
+ });
68
+ })();
69
+ </script>
70
+ ```
71
+
72
+
73
+ ## Features
74
+
75
+ - **Template resolution** via `template-ref` in-document selectors or remote URLs
76
+ - **Prefetch strategies** `lazy` (default), `eager`, or `idle`
77
+ - **Configurable render host** light DOM, shadow, author iframe, or any selector
78
+ - **Cancelable render / unrender** parents can wrap updates in view transitions
79
+ - **Persistable content** keep live subtree state across unrender / render cycles
80
+ - **Ready coordination** `ready-on` + `delaying-ready` for paint-synced reveals
81
+
82
+ ## Installation
83
+
84
+
85
+ `@excom/renderable-element` v0.1.0
86
+
87
+ ```bash
88
+ pnpm add @excom/renderable-element
89
+ ```
90
+
91
+ ```bash
92
+ npm install @excom/renderable-element
93
+ ```
94
+
95
+ ```bash
96
+ yarn add @excom/renderable-element
97
+ ```
98
+
99
+ ### Import
100
+
101
+ ```ts
102
+ import { /* … */ } from "@excom/renderable-element";
103
+ ```
104
+
105
+
106
+
107
+ ## Usage
108
+
109
+ Compose `RenderableElement` into a Neutron class and toggle `isActive`
110
+ from whatever signal makes sense — a media query, a websocket message,
111
+ an experiment flag, etc. Everything else (template fetch, caching, host
112
+ resolution, lifecycle events) is inherited.
113
+
114
+ ```ts
115
+ import { Neutron } from "@excom/neutron";
116
+ import { RenderableElement } from "@excom/renderable-element";
117
+
118
+ export const MediaGated = Neutron.compose([
119
+ RenderableElement,
120
+ Neutron({
121
+ tag: "media-gated",
122
+ props: {
123
+ mediaQuery: String,
124
+ },
125
+ }),
126
+ ])
127
+ .onPropChanged("mediaQuery", (el, prev) => {
128
+ prev.mediaQuery && el._mql?.removeEventListener("change", el._sync);
129
+ if (!el.mediaQuery) return { isActive: false };
130
+ el._mql = window.matchMedia(el.mediaQuery);
131
+ el._sync = () => (el.isActive = el._mql.matches);
132
+ el._mql.addEventListener("change", el._sync);
133
+ el._sync();
134
+ });
135
+
136
+ MediaGated.define();
137
+ ```
138
+
139
+ ```html
140
+ <media-gated media-query="(min-width: 900px)">
141
+ <template>
142
+ <wide-screen-only></wide-screen-only>
143
+ </template>
144
+ </media-gated>
145
+ ```
146
+
147
+ Event names are prefixed with the concrete tag (shown as `{tag}-…` in the
148
+ API). For `<include-content>` that means `include-content-render`, etc.
149
+
150
+ ### API Reference
151
+
152
+
153
+ #### Attributes
154
+
155
+ | Name | Surface | Type | Default | Values | Description |
156
+ | --- | --- | --- | --- | --- | --- |
157
+ | `template-ref` | option | `string` | `":scope > template"` | `<CSS Selector>` \| `<URL>` | Source `<template>` — in-document selector or remote URL. Changing mid-flight aborts and reloads. Can use `:scope` to relatively select elements: e.g. `main:has(:scope) > template` |
158
+ | `bypass-cache` | option | `boolean` | | | Skip the in-memory response cache (URL `template-ref` only). |
159
+ | `pre-fetch` | option | `string` | `"lazy"` | `""` \| `"eager"` \| `"idle"` \| `"lazy"` | When to fetch the template, independent of when it renders. `""` aliases `eager`. |
160
+ | `persist-content` | option | `boolean` | | | Reuse the same live nodes across unrender / render (held on `_persistedTree`) so form values, scroll position, and subtree state survive toggles. |
161
+ | `host-ref` | option | `string` | | `"shadow"` \| `"iframe"` \| `<CSS Selector>` | Where rendered children land. Unset = this element's light DOM. `shadow` attaches an open shadow root. `iframe` paints into a child `<iframe data-render-host>` body (you supply the iframe — useful for sandboxed / third-party document isolation). Any other value is a portal selector. |
162
+ | `ready-on` | option | `string` | | `<Event Name>` | Event name that marks rendered children "ready". Until it fires, `delaying-ready` is set so CSS can hide the host for a coordinated paint / view transition. |
163
+ | `is-active` | hybrid | `boolean` | | | Master switch. Set to load (if needed) and render; unset to unrender. Drive from visibility, route match, hover, etc. |
164
+ | `is-loading` | state | `boolean` | | | Template fetch in flight. |
165
+ | `did-load` | state | `boolean` | | | Template resolved at least once. Stays set across `is-active` toggles so consumers know later paints are warm (URL refs reuse the shared fetch cache in kit-utils). Cleared when `template-ref` changes or `--reload` forces a fresh resolve. |
166
+ | `is-error` | state | `boolean` | | | Latest template fetch rejected (excluding abort). Fires with the `error` event. |
167
+ | `delaying-ready` | state | `boolean` | | | Between `render` and the matching `ready-on` event. Hook with CSS for coordinated paints / view transitions. |
168
+
169
+ #### Recognized Elements
170
+
171
+ | Relationship | Selector | Required | Description |
172
+ | --- | --- | --- | --- |
173
+ | `template` | child | no | Optional immediate `<template>` child used when `template-ref` is the default `":scope > template"`. Not required when `template-ref` points at a selector or URL elsewhere. |
174
+ | `iframe[data-render-host]` | child | no | Required when `host-ref="iframe"`. Content paints into `iframe.contentDocument.body`. Provide your own iframe (e.g. with `srcdoc`); the element will not create one. |
175
+
176
+ #### Fires
177
+
178
+ | Name | Type | Description |
179
+ | --- | --- | --- |
180
+ | `{tag}-render` | `RenderableRenderEvent` (`CustomEvent & { type: "{tag}-render"; detail: () => Promise<void>; bubbles: true; cancelable: true; composed: true }`) | Cancelable. Dispatched when the element becomes active and is about to place template content into the host. `event.detail` is a thunk that performs the load (if not already loaded) and renders the children, returning a Promise that resolves once the corresponding `ready-on` event fires (or immediately if `ready-on` is unset). The promise rejects if the element is torn down mid-flight (`startTeardown` while loading / `delaying-ready`). Call `preventDefault()` to defer rendering and invoke `event.detail()` later. |
181
+ | `{tag}-unrender` | `RenderableUnrenderEvent` (`CustomEvent & { type: "{tag}-unrender"; detail: () => void; bubbles: true; cancelable: true; composed: true }`) | Cancelable. Dispatched when the element becomes inactive and content is already painted. `event.detail` is a thunk that removes the rendered children. Call `preventDefault()` to defer the removal. Not fired when teardown cancels an in-flight load — that path emits `aborted` instead. |
182
+ | `{tag}-did-render` | `RenderableDidRenderEvent` (`CustomEvent & { type: "{tag}-did-render"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched after the template content has actually been placed into the host. |
183
+ | `{tag}-did-unrender` | `RenderableDidUnrenderEvent` (`CustomEvent & { type: "{tag}-did-unrender"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched after rendered children have been removed from the host. |
184
+ | `{tag}-error` | `RenderableErrorEvent` (`CustomEvent & { type: "{tag}-error"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the template promise rejects with anything other than an `AbortError`. |
185
+ | `{tag}-aborted` | `RenderableAbortedEvent` (`CustomEvent & { type: "{tag}-aborted"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched when an in-flight load / ready wait is canceled because `is-active` was unset (via `startTeardown`). |
186
+
187
+ #### Commands
188
+
189
+ | Command | Action |
190
+ | --- | --- |
191
+ | `--reload` | Aborts any in-flight fetch and re-resolves the template, bypassing the cache for URL refs (useful after remote content changes). |
192
+
193
+ #### Default actions
194
+
195
+ | Event | Default behavior (unless preventDefault() is called) |
196
+ | --- | --- |
197
+ | `{tag}-render` | Invokes `event.detail()` to load (if needed) and render the template into the host. |
198
+ | `{tag}-unrender` | Invokes `event.detail()` to remove rendered children from the host. |
199
+
200
+
201
+
202
+ ### Examples
203
+
204
+ #### Choosing a render host
205
+
206
+ Unset `host-ref` renders into the element's light DOM. `"shadow"` attaches
207
+ an open shadow root for style isolation:
208
+
209
+
210
+ ```html
211
+ <p>Two <code>&lt;include-content&gt;</code> elements rendering the same template into different hosts. The shadow-root version is style-isolated.</p>
212
+
213
+ <style>
214
+ .host-demo include-content {
215
+ display: block;
216
+ border: 1px dashed currentColor;
217
+ padding: 8px;
218
+ margin: 8px 0;
219
+ }
220
+ /* this rule cannot reach into a shadow root */
221
+ .host-demo h4 { color: rebeccapurple; }
222
+ </style>
223
+
224
+ <div class="host-demo">
225
+ <strong>Light DOM (default):</strong>
226
+ <include-content is-active>
227
+ <template>
228
+ <h4>I'm rebeccapurple</h4>
229
+ <p>Light-DOM children inherit the host page's CSS.</p>
230
+ </template>
231
+ </include-content>
232
+
233
+ <strong>Shadow DOM (<code>host-ref="shadow"</code>):</strong>
234
+ <include-content is-active host-ref="shadow">
235
+ <template>
236
+ <style>h4 { color: tomato; }</style>
237
+ <h4>I'm tomato — outer styles can't touch me</h4>
238
+ <p>Encapsulated inside an attached shadow root.</p>
239
+ </template>
240
+ </include-content>
241
+ </div>
242
+ ```
243
+
244
+
245
+ `"iframe"` paints into a child `<iframe data-render-host>` you provide —
246
+ sandbox styles / scripts / document context. Only nodes move: custom
247
+ elements upgrade there only if the iframe document loads their definitions:
248
+
249
+
250
+ ```html
251
+ <include-content is-active host-ref="iframe">
252
+ <template>
253
+ <style>
254
+ body { font-family: system-ui; padding: 12px; color: #333; }
255
+ h4 { color: navy; margin: 0 0 4px; }
256
+ </style>
257
+ <h4>Hello from inside the iframe</h4>
258
+ <p>Rendered into <code>iframe.contentDocument.body</code>.</p>
259
+ </template>
260
+ <iframe data-render-host
261
+ srcdoc="<!doctype html><html><body></body></html>"></iframe>
262
+ <style>
263
+ #demo-renderable-element-host-iframe > :first-child {
264
+ display: block;
265
+ border: 1px dashed currentColor;
266
+ padding: 8px;
267
+
268
+ > iframe {
269
+ width: 100%;
270
+ height: 140px;
271
+ border: none;
272
+ background: white;
273
+ }
274
+ }
275
+ </style>
276
+ </include-content>
277
+ ```
278
+
279
+
280
+ Any other value is a CSS selector — the template lands in whatever
281
+ element it resolves to:
282
+
283
+
284
+ ```html
285
+ <p>Any other <code>host-ref</code> value is treated as a CSS selector. The element renders its template into whatever the selector resolves to.</p>
286
+
287
+ <style>
288
+ .selector-demo {
289
+ display: grid;
290
+ grid-template-columns: 1fr 1fr;
291
+ gap: 12px;
292
+ }
293
+ .selector-demo .mount-point {
294
+ border: 1px dashed currentColor;
295
+ padding: 8px;
296
+ min-height: 80px;
297
+ }
298
+ </style>
299
+
300
+ <div class="selector-demo">
301
+ <div>
302
+ <strong>Source element:</strong>
303
+ <include-content is-active host-ref=".sidebar-mount">
304
+ <template>
305
+ <p>I render into <code>.sidebar-mount</code> →</p>
306
+ </template>
307
+ </include-content>
308
+ </div>
309
+ <div>
310
+ <strong>Mount target:</strong>
311
+ <div class="mount-point sidebar-mount"></div>
312
+ </div>
313
+ </div>
314
+ ```
315
+
316
+
317
+ #### Hooking render with view transitions
318
+
319
+ `render` and `unrender` are cancelable; `event.detail` is the update
320
+ thunk. A parent can `preventDefault()` and run the mutation inside
321
+ `document.startViewTransition()` — the same pattern `<spa-manager>`
322
+ uses to batch sibling routes.
323
+
324
+
325
+ ```html
326
+ <p>Toggle the checkbox to activate the element. The cancelable
327
+ <code>include-content-render</code> event is intercepted, the actual
328
+ DOM mutation is wrapped in <code>document.startViewTransition()</code>,
329
+ and a parent listener can defer or skip the render entirely.</p>
330
+
331
+ <style>
332
+ .render-event-demo {
333
+ border: 1px solid var(--border, #444);
334
+ padding: 12px;
335
+ border-radius: 4px;
336
+ }
337
+ .render-event-demo include-content {
338
+ display: block;
339
+ margin-top: 8px;
340
+ padding: 8px;
341
+ border: 1px dashed currentColor;
342
+ }
343
+ .render-event-demo include-content > article {
344
+ background: rgba(255, 200, 80, 0.15);
345
+ padding: 12px;
346
+ view-transition-name: render-demo-card;
347
+ }
348
+ ::view-transition-old(render-demo-card),
349
+ ::view-transition-new(render-demo-card) {
350
+ animation-duration: 350ms;
351
+ }
352
+ </style>
353
+
354
+ <div class="render-event-demo" id="render-event-host">
355
+ <label>
356
+ <input type="checkbox" id="toggle-active" />
357
+ Active
358
+ </label>
359
+
360
+ <include-content id="render-target">
361
+ <template>
362
+ <article>
363
+ <h4>Just rendered (with a view transition)</h4>
364
+ <p>The host swallowed the default render and ran it through <code>startViewTransition</code>.</p>
365
+ </article>
366
+ </template>
367
+ </include-content>
368
+ </div>
369
+
370
+ <script>
371
+ (() => {
372
+ const host = document.querySelector("#render-event-host");
373
+ const target = host.querySelector("#render-target");
374
+ const toggle = host.querySelector("#toggle-active");
375
+
376
+ target.addEventListener("include-content-render", (e) => {
377
+ if (!document.startViewTransition) return;
378
+ e.preventDefault();
379
+ document.startViewTransition(() => e.detail());
380
+ });
381
+ target.addEventListener("include-content-unrender", (e) => {
382
+ if (!document.startViewTransition) return;
383
+ e.preventDefault();
384
+ document.startViewTransition(() => e.detail());
385
+ });
386
+
387
+ toggle.addEventListener("change", () => {
388
+ target.toggleAttribute("is-active", toggle.checked);
389
+ });
390
+ })();
391
+ </script>
392
+ ```
393
+
394
+
395
+ The thunk's returned Promise resolves when the view is ready (immediately, or when `ready-on` fires). If `is-active` is unset while still loading / `delaying-ready`, teardown rejects that Promise and emits `aborted` instead of `unrender`.
396
+
397
+ Pair with `ready-on` so `delaying-ready` stays set until your transition
398
+ has committed:
399
+
400
+ ```html
401
+ <media-gated ready-on="my-app-paint" media-query="(min-width: 900px)">
402
+ <template>...</template>
403
+ </media-gated>
404
+ ```
405
+
406
+ ```css
407
+ media-gated[delaying-ready] {
408
+ display: none;
409
+ }
410
+ ```
411
+
412
+ #### Persisting content across cycles
413
+
414
+ Once a template resolves, `did-load` stays set so consumers know later
415
+ toggles are warm — URL `template-ref`s reuse the shared fetch cache in
416
+ kit-utils. Without `persist-content` (the default), each activation
417
+ re-resolves and imports a fresh clone — subtree state is lost on
418
+ unrender. With it, the same live nodes are held across toggles:
419
+
420
+
421
+ ```html
422
+ <p>Type into both inputs, then toggle them off and on. The right side
423
+ keeps its value because <code>persist-content</code> reuses the same
424
+ template node instead of cloning a fresh copy each render.</p>
425
+
426
+ <style>
427
+ .persist-demo {
428
+ display: grid;
429
+ grid-template-columns: 1fr 1fr;
430
+ gap: 12px;
431
+ }
432
+ .persist-demo include-content {
433
+ display: block;
434
+ border: 1px dashed currentColor;
435
+ padding: 8px;
436
+ margin-top: 4px;
437
+ }
438
+ </style>
439
+
440
+ <div class="persist-demo" id="persist-host">
441
+ <div>
442
+ <strong>Default (cloned each render):</strong>
443
+ <label>
444
+ <input type="checkbox" data-target="#cloned" checked />
445
+ Active
446
+ </label>
447
+ <include-content id="cloned" is-active>
448
+ <template>
449
+ <input type="text" placeholder="Type, then toggle off + on" />
450
+ </template>
451
+ </include-content>
452
+ </div>
453
+
454
+ <div>
455
+ <strong><code>persist-content</code>:</strong>
456
+ <label>
457
+ <input type="checkbox" data-target="#persisted" checked />
458
+ Active
459
+ </label>
460
+ <include-content id="persisted" is-active persist-content>
461
+ <template>
462
+ <input type="text" placeholder="Type, then toggle off + on" />
463
+ </template>
464
+ </include-content>
465
+ </div>
466
+ </div>
467
+
468
+ <script>
469
+ (() => {
470
+ document.querySelectorAll("#persist-host input[type=checkbox]").forEach((cb) => {
471
+ cb.addEventListener("change", () => {
472
+ document
473
+ .querySelector(cb.dataset.target)
474
+ .toggleAttribute("is-active", cb.checked);
475
+ });
476
+ });
477
+ })();
478
+ </script>
479
+ ```
480
+
481
+
482
+ #### Loading strategies
483
+
484
+ `pre-fetch` controls *when* the template is fetched, separately from
485
+ when it is rendered:
486
+
487
+ ```html
488
+ <!-- default: fetch on first activation -->
489
+ <my-el></my-el>
490
+
491
+ <!-- pre-warm immediately on attribute set -->
492
+ <my-el pre-fetch="eager"></my-el>
493
+
494
+ <!-- backfill on idle -->
495
+ <my-el pre-fetch="idle" template-ref="/fragments/hero.html"></my-el>
496
+ ```
497
+
498
+ Pair with `bypass-cache` for revalidation when the element activates
499
+ multiple times. Invoke the `--reload` command to force a refresh.
@@ -0,0 +1,146 @@
1
+ # renderable-element
2
+
3
+ Composition base for Neutron elements that defer rendering a `<template>`
4
+ until the right moment. It owns the load + render lifecycle so subclasses
5
+ only decide *when* to flip `is-active`. Used by `<include-content>`,
6
+ `<spa-route>`, and any custom element you compose yourself.
7
+
8
+ The demos below use `<include-content>` (the simplest concrete subclass)
9
+ to exercise behavior that comes straight from this mixin.
10
+
11
+ <include-content data-demo="persist-content"></include-content>
12
+
13
+ ## Features
14
+
15
+ - **Template resolution** via `template-ref` in-document selectors or remote URLs
16
+ - **Prefetch strategies** `lazy` (default), `eager`, or `idle`
17
+ - **Configurable render host** light DOM, shadow, author iframe, or any selector
18
+ - **Cancelable render / unrender** parents can wrap updates in view transitions
19
+ - **Persistable content** keep live subtree state across unrender / render cycles
20
+ - **Ready coordination** `ready-on` + `delaying-ready` for paint-synced reveals
21
+
22
+ ## Installation
23
+
24
+ <include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
25
+
26
+ ## Usage
27
+
28
+ Compose `RenderableElement` into a Neutron class and toggle `isActive`
29
+ from whatever signal makes sense — a media query, a websocket message,
30
+ an experiment flag, etc. Everything else (template fetch, caching, host
31
+ resolution, lifecycle events) is inherited.
32
+
33
+ ```ts
34
+ import { Neutron } from "@excom/neutron";
35
+ import { RenderableElement } from "@excom/renderable-element";
36
+
37
+ export const MediaGated = Neutron.compose([
38
+ RenderableElement,
39
+ Neutron({
40
+ tag: "media-gated",
41
+ props: {
42
+ mediaQuery: String,
43
+ },
44
+ }),
45
+ ])
46
+ .onPropChanged("mediaQuery", (el, prev) => {
47
+ prev.mediaQuery && el._mql?.removeEventListener("change", el._sync);
48
+ if (!el.mediaQuery) return { isActive: false };
49
+ el._mql = window.matchMedia(el.mediaQuery);
50
+ el._sync = () => (el.isActive = el._mql.matches);
51
+ el._mql.addEventListener("change", el._sync);
52
+ el._sync();
53
+ });
54
+
55
+ MediaGated.define();
56
+ ```
57
+
58
+ ```html
59
+ <media-gated media-query="(min-width: 900px)">
60
+ <template>
61
+ <wide-screen-only></wide-screen-only>
62
+ </template>
63
+ </media-gated>
64
+ ```
65
+
66
+ Event names are prefixed with the concrete tag (shown as `{tag}-…` in the
67
+ API). For `<include-content>` that means `include-content-render`, etc.
68
+
69
+ ### API Reference
70
+
71
+ <include-content is-active template-ref="/views/api-reference/api-reference.html"></include-content>
72
+
73
+ ### Examples
74
+
75
+ #### Choosing a render host
76
+
77
+ Unset `host-ref` renders into the element's light DOM. `"shadow"` attaches
78
+ an open shadow root for style isolation:
79
+
80
+ <include-content data-demo="host-shadow"></include-content>
81
+
82
+ `"iframe"` paints into a child `<iframe data-render-host>` you provide —
83
+ sandbox styles / scripts / document context. Only nodes move: custom
84
+ elements upgrade there only if the iframe document loads their definitions:
85
+
86
+ <include-content data-demo="host-iframe"></include-content>
87
+
88
+ Any other value is a CSS selector — the template lands in whatever
89
+ element it resolves to:
90
+
91
+ <include-content data-demo="host-selector"></include-content>
92
+
93
+ #### Hooking render with view transitions
94
+
95
+ `render` and `unrender` are cancelable; `event.detail` is the update
96
+ thunk. A parent can `preventDefault()` and run the mutation inside
97
+ `document.startViewTransition()` — the same pattern `<spa-manager>`
98
+ uses to batch sibling routes.
99
+
100
+ <include-content data-demo="render-event"></include-content>
101
+
102
+ The thunk's returned Promise resolves when the view is ready (immediately, or when `ready-on` fires). If `is-active` is unset while still loading / `delaying-ready`, teardown rejects that Promise and emits `aborted` instead of `unrender`.
103
+
104
+ Pair with `ready-on` so `delaying-ready` stays set until your transition
105
+ has committed:
106
+
107
+ ```html
108
+ <media-gated ready-on="my-app-paint" media-query="(min-width: 900px)">
109
+ <template>...</template>
110
+ </media-gated>
111
+ ```
112
+
113
+ ```css
114
+ media-gated[delaying-ready] {
115
+ display: none;
116
+ }
117
+ ```
118
+
119
+ #### Persisting content across cycles
120
+
121
+ Once a template resolves, `did-load` stays set so consumers know later
122
+ toggles are warm — URL `template-ref`s reuse the shared fetch cache in
123
+ kit-utils. Without `persist-content` (the default), each activation
124
+ re-resolves and imports a fresh clone — subtree state is lost on
125
+ unrender. With it, the same live nodes are held across toggles:
126
+
127
+ <include-content data-demo="persist-content"></include-content>
128
+
129
+ #### Loading strategies
130
+
131
+ `pre-fetch` controls *when* the template is fetched, separately from
132
+ when it is rendered:
133
+
134
+ ```html
135
+ <!-- default: fetch on first activation -->
136
+ <my-el></my-el>
137
+
138
+ <!-- pre-warm immediately on attribute set -->
139
+ <my-el pre-fetch="eager"></my-el>
140
+
141
+ <!-- backfill on idle -->
142
+ <my-el pre-fetch="idle" template-ref="/fragments/hero.html"></my-el>
143
+ ```
144
+
145
+ Pair with `bypass-cache` for revalidation when the element activates
146
+ multiple times. Invoke the `--reload` command to force a refresh.