@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.
- package/.rush/temp/chunked-rush-logs/renderable-element.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/renderable-element.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/renderable-element.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +638 -0
- package/package.json +48 -0
- package/rush-logs/renderable-element.apply-exports.cache.log +1 -0
- package/rush-logs/renderable-element.apply-exports.log +1 -0
- package/rush-logs/renderable-element.build_docs.cache.log +1 -0
- package/rush-logs/renderable-element.build_docs.log +1 -0
- package/rush-logs/renderable-element.build_package-metas.cache.log +1 -0
- package/rush-logs/renderable-element.build_package-metas.log +1 -0
- package/src/index.css +11 -0
- package/support/custom-elements.json +371 -0
- package/support/demos/host-iframe.html +26 -0
- package/support/demos/host-selector.html +29 -0
- package/support/demos/host-shadow.html +31 -0
- package/support/demos/persist-content.html +57 -0
- package/support/demos/render-event.html +66 -0
- package/support/dist-docs/renderable-element.md +499 -0
- package/support/docs/README.md +146 -0
- package/support/package-meta.json +206 -0
- package/support/tests/host-iframe.view.test.ts +22 -0
- package/support/tests/host-selector.view.test.ts +19 -0
- package/support/tests/host-shadow.view.test.ts +19 -0
- package/support/tests/persist-content.view.test.ts +31 -0
- package/support/tests/render-event.view.test.ts +24 -0
- package/support/tests/render-lifecycle.test.ts +961 -0
- package/support/tests/renderable-element.test.ts +222 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
package/config/rig.json
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { AbortableElement } from "@excom/abortable-element";
|
|
2
|
+
import { KitLogger } from "@excom/kit-logger";
|
|
3
|
+
import { requestIdleCb } from "@excom/kit-shims";
|
|
4
|
+
import {
|
|
5
|
+
replaceNonTemplateChildren,
|
|
6
|
+
resolveTemplateContent,
|
|
7
|
+
selectOne,
|
|
8
|
+
} from "@excom/kit-utils";
|
|
9
|
+
import { ConstructorType, Neutron, TEvent } from "@excom/neutron";
|
|
10
|
+
|
|
11
|
+
export type RenderThunk = () => Promise<void>;
|
|
12
|
+
export type UnrenderThunk = () => void;
|
|
13
|
+
|
|
14
|
+
export type RenderableRenderEvent = TEvent & {
|
|
15
|
+
type: "{tag}-render";
|
|
16
|
+
detail: RenderThunk;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RenderableUnrenderEvent = TEvent & {
|
|
20
|
+
type: "{tag}-unrender";
|
|
21
|
+
detail: UnrenderThunk;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RenderableDidRenderEvent = TEvent & {
|
|
25
|
+
type: "{tag}-did-render";
|
|
26
|
+
detail: void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type RenderableDidUnrenderEvent = TEvent & {
|
|
30
|
+
type: "{tag}-did-unrender";
|
|
31
|
+
detail: void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type RenderableErrorEvent = TEvent & {
|
|
35
|
+
type: "{tag}-error";
|
|
36
|
+
detail: void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type RenderableAbortedEvent = TEvent & {
|
|
40
|
+
type: "{tag}-aborted";
|
|
41
|
+
detail: void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const IFRAME_HOST_ATTR = "data-render-host";
|
|
45
|
+
/** Iframes waiting on `load` so their body can be `renderHost`. */
|
|
46
|
+
const pendingIframeLoads = new WeakSet<HTMLIFrameElement>();
|
|
47
|
+
|
|
48
|
+
// Must stay `async` even when `resolveTemplateContent` is sync.
|
|
49
|
+
const initTemplatePromise = async (
|
|
50
|
+
element,
|
|
51
|
+
opts: { forceBypassCache?: boolean } = {}
|
|
52
|
+
) => {
|
|
53
|
+
const { templateRef, bypassCache, abortController } = element;
|
|
54
|
+
// Snapshot this signal: `doAbort` rotates a fresh controller onto the
|
|
55
|
+
// element, so don't read `element.abortController` after await.
|
|
56
|
+
const { signal } = abortController;
|
|
57
|
+
try {
|
|
58
|
+
return await resolveTemplateContent(templateRef, {
|
|
59
|
+
scope: element,
|
|
60
|
+
bypassCache: bypassCache || opts.forceBypassCache,
|
|
61
|
+
reqInit: { signal },
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Preserve aborts so disconnect / reload teardown is quiet
|
|
65
|
+
if (error?.name === "AbortError" || signal.aborted) {
|
|
66
|
+
throw error?.name === "AbortError"
|
|
67
|
+
? error
|
|
68
|
+
: Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Failed to find template: ${templateRef}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface PromiseObject {
|
|
75
|
+
promise: Promise<void>;
|
|
76
|
+
resolve: (value: void) => void;
|
|
77
|
+
reject: (reason?: any) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const makePromiseObject = () => {
|
|
81
|
+
const readyPromiseObject = {} as unknown as PromiseObject;
|
|
82
|
+
readyPromiseObject.promise = new Promise((resolve, reject) => {
|
|
83
|
+
readyPromiseObject.resolve = resolve;
|
|
84
|
+
readyPromiseObject.reject = reject;
|
|
85
|
+
});
|
|
86
|
+
return readyPromiseObject;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Composition base that resolves a `<template>` reference, fetches its
|
|
91
|
+
* content (eagerly, on idle, lazily, or on demand), and renders / unrenders
|
|
92
|
+
* those children into a configurable host (the element's own light DOM, an
|
|
93
|
+
* attached shadow root, a child `<iframe data-render-host>`, or any
|
|
94
|
+
* selector-resolved element). Not a registered element on its own (tag is
|
|
95
|
+
* intentionally `noop-tag`) — compose it via
|
|
96
|
+
* `Neutron.compose([RenderableElement, ...])` and the consumer element
|
|
97
|
+
* inherits every attribute, state, and event declared below.
|
|
98
|
+
*
|
|
99
|
+
* Subclasses only need to toggle `is-active` to drive the lifecycle:
|
|
100
|
+
* setting it triggers loading (if needed) and renders the template;
|
|
101
|
+
* unsetting it unrenders. Templates are resolved via
|
|
102
|
+
* `resolveTemplateContent`, so `template-ref` accepts in-document selectors
|
|
103
|
+
* (`":scope > template"`, the default) as well as URLs to remote
|
|
104
|
+
* templates. URL responses are memoized in kit-utils (`TEMPLATES`);
|
|
105
|
+
* `did-load` reflects that a resolve succeeded so consumers can treat
|
|
106
|
+
* later toggles as warm. `persist-content` is separate: it keeps the
|
|
107
|
+
* live rendered tree in `_persistedTree` across unrender / render.
|
|
108
|
+
*
|
|
109
|
+
* The `render` and `unrender` events are dispatched as cancelable defaults:
|
|
110
|
+
* listeners can `preventDefault()` to take over the actual DOM update by
|
|
111
|
+
* calling `event.detail()` themselves (handy for orchestrating view
|
|
112
|
+
* transitions or animations from a parent like `<spa-manager>`).
|
|
113
|
+
*
|
|
114
|
+
* @fires {tag}-render - Cancelable. Dispatched when the element becomes
|
|
115
|
+
* active and is about to place template content into the host.
|
|
116
|
+
* `event.detail` is a thunk that performs the load (if not already
|
|
117
|
+
* loaded) and renders the children, returning a Promise that resolves
|
|
118
|
+
* once the corresponding `ready-on` event fires (or immediately if
|
|
119
|
+
* `ready-on` is unset). The promise rejects if the element is torn
|
|
120
|
+
* down mid-flight (`startTeardown` while loading / `delaying-ready`).
|
|
121
|
+
* Call `preventDefault()` to defer rendering and invoke
|
|
122
|
+
* `event.detail()` later.
|
|
123
|
+
* @type RenderableRenderEvent
|
|
124
|
+
* @fires {tag}-unrender - Cancelable. Dispatched when the element becomes
|
|
125
|
+
* inactive and content is already painted. `event.detail` is a thunk
|
|
126
|
+
* that removes the rendered children. Call `preventDefault()` to defer
|
|
127
|
+
* the removal. Not fired when teardown cancels an in-flight load —
|
|
128
|
+
* that path emits `aborted` instead.
|
|
129
|
+
* @type RenderableUnrenderEvent
|
|
130
|
+
* @fires {tag}-did-render - Dispatched after the template content has
|
|
131
|
+
* actually been placed into the host.
|
|
132
|
+
* @type RenderableDidRenderEvent
|
|
133
|
+
* @fires {tag}-did-unrender - Dispatched after rendered children have been
|
|
134
|
+
* removed from the host.
|
|
135
|
+
* @type RenderableDidUnrenderEvent
|
|
136
|
+
* @fires {tag}-error - Dispatched when the template promise rejects with
|
|
137
|
+
* anything other than an `AbortError`.
|
|
138
|
+
* @type RenderableErrorEvent
|
|
139
|
+
* @fires {tag}-aborted - Dispatched when an in-flight load / ready wait is
|
|
140
|
+
* canceled because `is-active` was unset (via `startTeardown`).
|
|
141
|
+
* @type RenderableAbortedEvent
|
|
142
|
+
*
|
|
143
|
+
* @default-action {tag}-render - Invokes `event.detail()` to load (if
|
|
144
|
+
* needed) and render the template into the host.
|
|
145
|
+
* @default-action {tag}-unrender - Invokes `event.detail()` to remove
|
|
146
|
+
* rendered children from the host.
|
|
147
|
+
* @command --reload - Aborts any in-flight fetch and re-resolves the
|
|
148
|
+
* template, bypassing the cache for URL refs (useful after remote content
|
|
149
|
+
* changes).
|
|
150
|
+
*
|
|
151
|
+
* @child ?template - Optional immediate `<template>` child used when
|
|
152
|
+
* `template-ref` is the default `":scope > template"`. Not required when
|
|
153
|
+
* `template-ref` points at a selector or URL elsewhere.
|
|
154
|
+
* @child ?iframe[data-render-host] - Required when `host-ref="iframe"`.
|
|
155
|
+
* Content paints into `iframe.contentDocument.body`. Provide your own
|
|
156
|
+
* iframe (e.g. with `srcdoc`); the element will not create one.
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
export const RenderableElement = Neutron.compose([
|
|
160
|
+
AbortableElement,
|
|
161
|
+
Neutron({
|
|
162
|
+
tag: "noop-tag",
|
|
163
|
+
events: {
|
|
164
|
+
["render"]: {
|
|
165
|
+
prefixWithTag: true,
|
|
166
|
+
},
|
|
167
|
+
["unrender"]: {
|
|
168
|
+
prefixWithTag: true,
|
|
169
|
+
},
|
|
170
|
+
["did-render"]: {
|
|
171
|
+
prefixWithTag: true,
|
|
172
|
+
},
|
|
173
|
+
["did-unrender"]: {
|
|
174
|
+
prefixWithTag: true,
|
|
175
|
+
},
|
|
176
|
+
error: {
|
|
177
|
+
prefixWithTag: true,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
props: {
|
|
181
|
+
/**
|
|
182
|
+
* @option
|
|
183
|
+
* Source `<template>` — in-document selector or remote URL.
|
|
184
|
+
* Changing mid-flight aborts and reloads. Can use `:scope` to
|
|
185
|
+
* relatively select elements: e.g. `main:has(:scope) > template`
|
|
186
|
+
* @default :scope > template
|
|
187
|
+
* @values <CSS Selector> | <URL>
|
|
188
|
+
*/
|
|
189
|
+
templateRef: {
|
|
190
|
+
type: String,
|
|
191
|
+
defaultValue: () => ":scope > template",
|
|
192
|
+
},
|
|
193
|
+
/**
|
|
194
|
+
* @option
|
|
195
|
+
* Skip the in-memory response cache (URL `template-ref` only).
|
|
196
|
+
*/
|
|
197
|
+
bypassCache: Boolean,
|
|
198
|
+
/**
|
|
199
|
+
* @option
|
|
200
|
+
* When to fetch the template, independent of when it renders.
|
|
201
|
+
* `""` aliases `eager`.
|
|
202
|
+
* @default lazy
|
|
203
|
+
*/
|
|
204
|
+
preFetch: {
|
|
205
|
+
type: String,
|
|
206
|
+
isValid: (value: string) =>
|
|
207
|
+
// lazy (default): fetch only when needed.
|
|
208
|
+
// empty string aliases eager
|
|
209
|
+
["", "eager", "idle", "lazy"].includes(value),
|
|
210
|
+
defaultValue: () => "lazy",
|
|
211
|
+
},
|
|
212
|
+
/**
|
|
213
|
+
* @option
|
|
214
|
+
* Reuse the same live nodes across unrender / render (held on
|
|
215
|
+
* `_persistedTree`) so form values, scroll position, and subtree
|
|
216
|
+
* state survive toggles.
|
|
217
|
+
*/
|
|
218
|
+
persistContent: Boolean,
|
|
219
|
+
/**
|
|
220
|
+
* @option
|
|
221
|
+
* Where rendered children land. Unset = this element's light DOM.
|
|
222
|
+
* `shadow` attaches an open shadow root. `iframe` paints into a
|
|
223
|
+
* child `<iframe data-render-host>` body (you supply the iframe —
|
|
224
|
+
* useful for sandboxed / third-party document isolation). Any
|
|
225
|
+
* other value is a portal selector.
|
|
226
|
+
* @values shadow | iframe | <CSS Selector>
|
|
227
|
+
*/
|
|
228
|
+
hostRef: String,
|
|
229
|
+
/**
|
|
230
|
+
* @option
|
|
231
|
+
* Event name that marks rendered children "ready". Until it fires,
|
|
232
|
+
* `delaying-ready` is set so CSS can hide the host for a
|
|
233
|
+
* coordinated paint / view transition.
|
|
234
|
+
* @values <Event Name>
|
|
235
|
+
*/
|
|
236
|
+
readyOn: String,
|
|
237
|
+
/**
|
|
238
|
+
* @option
|
|
239
|
+
* @state
|
|
240
|
+
* Master switch. Set to load (if needed) and render; unset to
|
|
241
|
+
* unrender. Drive from visibility, route match, hover, etc.
|
|
242
|
+
*/
|
|
243
|
+
isActive: Boolean,
|
|
244
|
+
/**
|
|
245
|
+
* @state
|
|
246
|
+
* Template fetch in flight.
|
|
247
|
+
*/
|
|
248
|
+
isLoading: Boolean,
|
|
249
|
+
/**
|
|
250
|
+
* @state
|
|
251
|
+
* Template resolved at least once. Stays set across `is-active`
|
|
252
|
+
* toggles so consumers know later paints are warm (URL refs reuse
|
|
253
|
+
* the shared fetch cache in kit-utils). Cleared when
|
|
254
|
+
* `template-ref` changes or `--reload` forces a fresh resolve.
|
|
255
|
+
*/
|
|
256
|
+
didLoad: Boolean,
|
|
257
|
+
/**
|
|
258
|
+
* @state
|
|
259
|
+
* Latest template fetch rejected (excluding abort). Fires with
|
|
260
|
+
* the `error` event.
|
|
261
|
+
*/
|
|
262
|
+
isError: Boolean,
|
|
263
|
+
/**
|
|
264
|
+
* @state
|
|
265
|
+
* Between `render` and the matching `ready-on` event. Hook with
|
|
266
|
+
* CSS for coordinated paints / view transitions.
|
|
267
|
+
*/
|
|
268
|
+
delayingReady: Boolean,
|
|
269
|
+
// private state
|
|
270
|
+
templatePromise: Promise as unknown as ConstructorType<
|
|
271
|
+
Promise<HTMLTemplateElement>
|
|
272
|
+
>,
|
|
273
|
+
/* Live tree kept only with `persist-content`. Cleared on template
|
|
274
|
+
change / reload. Without persist, each paint re-resolves (URL hits
|
|
275
|
+
`TEMPLATES`) and `importNode`s a fresh clone. */
|
|
276
|
+
_persistedTree: Object as unknown as ConstructorType<Node | null>,
|
|
277
|
+
/* Current render-host (null until set up, or while an iframe host
|
|
278
|
+
is still loading). Null falls back to the element. Not an attribute. */
|
|
279
|
+
renderHost: Object as unknown as ConstructorType<
|
|
280
|
+
Element | ShadowRoot | null
|
|
281
|
+
>,
|
|
282
|
+
readyPromiseObject: Object as unknown as ConstructorType<PromiseObject>,
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
])
|
|
286
|
+
.defineMethods({
|
|
287
|
+
setCanceledState: () => [
|
|
288
|
+
{ doAbort: [] },
|
|
289
|
+
{
|
|
290
|
+
templatePromise: null,
|
|
291
|
+
isLoading: false,
|
|
292
|
+
didLoad: false,
|
|
293
|
+
isError: false,
|
|
294
|
+
delayingReady: false,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
/**
|
|
298
|
+
* Resolve `hostRef` to a render-host:
|
|
299
|
+
* - unset/empty -> null (caller uses the element)
|
|
300
|
+
* - "shadow" -> element.shadowRoot (attaches on first call)
|
|
301
|
+
* - "iframe" -> child `iframe[data-render-host]` body (null if
|
|
302
|
+
* missing or still loading; load sets `renderHost`
|
|
303
|
+
* and paints)
|
|
304
|
+
* - any other str -> selectOne(value, { scope: element })
|
|
305
|
+
*/
|
|
306
|
+
resolveRenderHost: (element) => {
|
|
307
|
+
const ref = element.hostRef as string | undefined;
|
|
308
|
+
if (!ref) return { returns: null };
|
|
309
|
+
if (ref === "shadow") {
|
|
310
|
+
return {
|
|
311
|
+
returns: element.shadowRoot ?? element.attachShadow({ mode: "open" }),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (ref === "iframe") {
|
|
315
|
+
const iframe = element.querySelector(
|
|
316
|
+
`:scope > iframe[${IFRAME_HOST_ATTR}]`
|
|
317
|
+
) as HTMLIFrameElement | null;
|
|
318
|
+
if (!iframe) return { returns: null };
|
|
319
|
+
const takeBody = () => iframe.contentDocument?.body ?? null;
|
|
320
|
+
const body = takeBody();
|
|
321
|
+
if (body) return { returns: body };
|
|
322
|
+
/* Author iframe not ready: wait for load, then paint. Parser-created
|
|
323
|
+
`srcdoc` iframes may have already fired `load` before upgrade, so
|
|
324
|
+
re-probe on a microtask too. */
|
|
325
|
+
if (!pendingIframeLoads.has(iframe)) {
|
|
326
|
+
pendingIframeLoads.add(iframe);
|
|
327
|
+
let settled = false;
|
|
328
|
+
const onReady = () => {
|
|
329
|
+
const readyBody = takeBody();
|
|
330
|
+
if (!readyBody) return;
|
|
331
|
+
if (settled) return;
|
|
332
|
+
settled = true;
|
|
333
|
+
pendingIframeLoads.delete(iframe);
|
|
334
|
+
iframe.removeEventListener("load", onReady);
|
|
335
|
+
if (element.hostRef !== "iframe" || !iframe.isConnected) return;
|
|
336
|
+
element.renderHost = readyBody;
|
|
337
|
+
if (!element.isActive) return;
|
|
338
|
+
// @ts-expect-error TODO defineMethods
|
|
339
|
+
if (element._persistedTree) element.renderChildren();
|
|
340
|
+
// @ts-expect-error TODO defineMethods
|
|
341
|
+
else element.attemptLoad();
|
|
342
|
+
};
|
|
343
|
+
iframe.addEventListener("load", onReady);
|
|
344
|
+
queueMicrotask(onReady);
|
|
345
|
+
}
|
|
346
|
+
return { returns: null };
|
|
347
|
+
}
|
|
348
|
+
return { returns: selectOne(ref, { scope: element }) };
|
|
349
|
+
},
|
|
350
|
+
renderChildren: (element, resolved?: Node) => {
|
|
351
|
+
const source = resolved ?? element._persistedTree;
|
|
352
|
+
if (!source) return;
|
|
353
|
+
// Host not ready yet (e.g. iframe still loading). Its load handler
|
|
354
|
+
// retriggers this call.
|
|
355
|
+
if (element.hostRef && !element.renderHost) return;
|
|
356
|
+
const host = (element.renderHost ?? element) as Element;
|
|
357
|
+
const children = [
|
|
358
|
+
element.persistContent
|
|
359
|
+
? source
|
|
360
|
+
: (document.importNode(source, true) as Element),
|
|
361
|
+
];
|
|
362
|
+
if (replaceNonTemplateChildren(host, children as Node[])) {
|
|
363
|
+
return [
|
|
364
|
+
{
|
|
365
|
+
tryCompleteReady: ["renderChildren"],
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
emits: [["did-render"]],
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
unrenderChildren: (element) => {
|
|
374
|
+
if (element.hostRef && !element.renderHost) return;
|
|
375
|
+
const host = (element.renderHost ?? element) as Element;
|
|
376
|
+
if (replaceNonTemplateChildren(host, [])) {
|
|
377
|
+
return [
|
|
378
|
+
/* Drop the non-persist source; `didLoad` stays so a warm
|
|
379
|
+
re-resolve is available. With `persist-content`, keep
|
|
380
|
+
`_persistedTree` (detached live nodes). */
|
|
381
|
+
!element.persistContent && { _persistedTree: null },
|
|
382
|
+
{
|
|
383
|
+
emits: [["did-unrender"]],
|
|
384
|
+
},
|
|
385
|
+
];
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
// @ts-ignore TODO defineMethods
|
|
389
|
+
startTeardown: ({ isLoading, delayingReady, unrenderChildren }) => {
|
|
390
|
+
return isLoading || delayingReady
|
|
391
|
+
? [
|
|
392
|
+
{ setCanceledState: [] },
|
|
393
|
+
{ emit: ["aborted"] },
|
|
394
|
+
{ tryCompleteReady: ["startTeardown", "reject"] },
|
|
395
|
+
]
|
|
396
|
+
: {
|
|
397
|
+
emit: [
|
|
398
|
+
/* Sync, but order still matters for listening parents. */
|
|
399
|
+
"unrender",
|
|
400
|
+
// Sync: do not return a promise
|
|
401
|
+
{ detail: unrenderChildren },
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
// @ts-ignore TODO defineMethods
|
|
406
|
+
setupRenderHost: ({ resolveRenderHost }) => ({
|
|
407
|
+
renderHost: resolveRenderHost(),
|
|
408
|
+
}),
|
|
409
|
+
// Clear the current host's children on a `hostRef` retarget so the
|
|
410
|
+
// next paint can retarget without a full teardown.
|
|
411
|
+
clearHostChildren: (element) => {
|
|
412
|
+
const host = (element.renderHost ?? element) as Element;
|
|
413
|
+
if (replaceNonTemplateChildren(host, [])) {
|
|
414
|
+
return {
|
|
415
|
+
emits: [["did-unrender"]],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
attemptLoad: (element, opts: { forceBypassCache?: boolean } = {}) => {
|
|
420
|
+
const { templatePromise, isLoading, _persistedTree } = element;
|
|
421
|
+
return [
|
|
422
|
+
!templatePromise &&
|
|
423
|
+
!isLoading &&
|
|
424
|
+
!_persistedTree && {
|
|
425
|
+
isLoading: true,
|
|
426
|
+
isError: false,
|
|
427
|
+
templatePromise: initTemplatePromise(element, opts),
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
},
|
|
431
|
+
attemptPreFetch: (element, opts: { forceBypassCache?: boolean } = {}) => {
|
|
432
|
+
// @ts-ignore TODO defineMethods
|
|
433
|
+
const { preFetch, isActive, attemptLoad } = element;
|
|
434
|
+
// Eager / empty `preFetch`, or the element is active.
|
|
435
|
+
if (["eager", ""].includes(preFetch as string) || isActive)
|
|
436
|
+
return { attemptLoad: [] };
|
|
437
|
+
else if (preFetch === "idle") {
|
|
438
|
+
const cb = () => {
|
|
439
|
+
/* Re-read `preFetch` off the element; the captured value may be
|
|
440
|
+
stale. `cancelIdleCallback` would be nicer, but Safari lacks it. */
|
|
441
|
+
if (element.preFetch === "idle") {
|
|
442
|
+
attemptLoad(opts);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
// Fallback: wait until after the next paint
|
|
446
|
+
requestIdleCb(cb, 17);
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
changeTemplate: (
|
|
450
|
+
{ templatePromise, isActive },
|
|
451
|
+
opts: { forceBypassCache?: boolean } = {}
|
|
452
|
+
) => [
|
|
453
|
+
templatePromise && {
|
|
454
|
+
setCanceledState: [],
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
didLoad: false,
|
|
458
|
+
_persistedTree: null,
|
|
459
|
+
isError: false,
|
|
460
|
+
},
|
|
461
|
+
// Bypass the in-memory template cache so reload sees the latest
|
|
462
|
+
// source (matters when `templateRef` is a URL).
|
|
463
|
+
isActive ? { attemptLoad: [opts] } : { attemptPreFetch: [opts] },
|
|
464
|
+
],
|
|
465
|
+
readyContent: () => ({
|
|
466
|
+
delayingReady: false,
|
|
467
|
+
}),
|
|
468
|
+
// @ts-ignore TODO defineMethods
|
|
469
|
+
startReady: ({ readyOn, readyContent, readyPromiseObject }) => {
|
|
470
|
+
if (readyPromiseObject) return false;
|
|
471
|
+
const newReadyPromiseObject = makePromiseObject();
|
|
472
|
+
if (readyOn) {
|
|
473
|
+
// Ignore abort rejects; `readyContent` only runs on success
|
|
474
|
+
newReadyPromiseObject!.promise.then(readyContent, () => {});
|
|
475
|
+
}
|
|
476
|
+
return [
|
|
477
|
+
readyOn && { delayingReady: true },
|
|
478
|
+
{ readyPromiseObject: newReadyPromiseObject },
|
|
479
|
+
];
|
|
480
|
+
},
|
|
481
|
+
isResponsibleForReady: ({ readyOn }, caller: string | Event) => {
|
|
482
|
+
if (caller === "startTeardown") {
|
|
483
|
+
return { returns: true };
|
|
484
|
+
} else if (readyOn && caller instanceof Event) {
|
|
485
|
+
return { returns: true };
|
|
486
|
+
} else if (!readyOn && caller === "renderChildren") {
|
|
487
|
+
return { returns: true };
|
|
488
|
+
}
|
|
489
|
+
return { returns: false };
|
|
490
|
+
},
|
|
491
|
+
tryCompleteReady: (
|
|
492
|
+
// @ts-ignore TODO defineMethods
|
|
493
|
+
{ readyPromiseObject, isResponsibleForReady },
|
|
494
|
+
caller: string | Event,
|
|
495
|
+
method: "reject" | any
|
|
496
|
+
) => {
|
|
497
|
+
if (!readyPromiseObject || !isResponsibleForReady(caller)) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
/* `readyOn` can fire more than once. Pass `"reject"` on mid-flight
|
|
501
|
+
teardown so waiters (e.g. spa-manager) don't treat the abort as ready. */
|
|
502
|
+
if (method === "reject") {
|
|
503
|
+
// Attach before reject so abort isn't an unhandled rejection when
|
|
504
|
+
// no parent is awaiting the ready promise yet
|
|
505
|
+
readyPromiseObject.promise.catch(() => {});
|
|
506
|
+
readyPromiseObject.reject?.();
|
|
507
|
+
} else {
|
|
508
|
+
readyPromiseObject.resolve?.();
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
readyPromiseObject: null,
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
// @ts-ignore TODO defineMethods
|
|
515
|
+
renderCallback: ({ isActive, _persistedTree, tryCompleteReady }) =>
|
|
516
|
+
/* Re-check `isActive`: the consumer may call this later, and the
|
|
517
|
+
load may have been canceled (`isActive` false) in the meantime. */
|
|
518
|
+
isActive
|
|
519
|
+
? [_persistedTree ? { renderChildren: [] } : { attemptLoad: [] }]
|
|
520
|
+
: /* Torn down before a deferred render thunk ran (a parent such as
|
|
521
|
+
`<spa-manager>` batches these). Reject the ready promise rather
|
|
522
|
+
than dropping it: the thunk already handed it to the batch, and
|
|
523
|
+
an unsettled promise would hang the batch until its timeout. */
|
|
524
|
+
{ tryCompleteReady: ["startTeardown", "reject"] },
|
|
525
|
+
firePromiseEvent: (
|
|
526
|
+
{ readyPromiseObject },
|
|
527
|
+
eventName: string,
|
|
528
|
+
callback: () => void | Promise<void>
|
|
529
|
+
) => ({
|
|
530
|
+
emit: [
|
|
531
|
+
eventName,
|
|
532
|
+
{
|
|
533
|
+
detail: () => {
|
|
534
|
+
queueMicrotask(() => {
|
|
535
|
+
callback();
|
|
536
|
+
});
|
|
537
|
+
return readyPromiseObject?.promise;
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
}),
|
|
542
|
+
})
|
|
543
|
+
.onPropChanged("readyOn", ({ readyOn, tryCompleteReady }, previous) => [
|
|
544
|
+
previous.readyOn && {
|
|
545
|
+
removeListener: [previous.readyOn, tryCompleteReady],
|
|
546
|
+
},
|
|
547
|
+
readyOn && {
|
|
548
|
+
addListener: [readyOn, tryCompleteReady],
|
|
549
|
+
},
|
|
550
|
+
])
|
|
551
|
+
.onPropSet("preFetch", () => ({
|
|
552
|
+
attemptPreFetch: [],
|
|
553
|
+
}))
|
|
554
|
+
// Reset when `templateRef` is removed or changed to a different truthy value
|
|
555
|
+
.onPropChanged("templateRef", () => ({
|
|
556
|
+
changeTemplate: [],
|
|
557
|
+
}))
|
|
558
|
+
.onCommand("--reload", () => ({
|
|
559
|
+
changeTemplate: [{ forceBypassCache: true }],
|
|
560
|
+
}))
|
|
561
|
+
/*
|
|
562
|
+
* Retarget when `hostRef` is set, changed, or unset. Order: clear the
|
|
563
|
+
* old host's children (so a kept host, element / shadow / iframe body /
|
|
564
|
+
* portal, isn't left stale), resolve the new host, then re-render.
|
|
565
|
+
*
|
|
566
|
+
* `persist-content` re-places `_persistedTree`. Otherwise `attemptLoad`
|
|
567
|
+
* re-resolves (URL refs hit the shared fetch cache).
|
|
568
|
+
*
|
|
569
|
+
* An unreadied author iframe: `resolveRenderHost` returns null; its
|
|
570
|
+
* load handler paints once the body exists.
|
|
571
|
+
*/
|
|
572
|
+
.onPropChanged("hostRef", ({ isActive, _persistedTree }) => [
|
|
573
|
+
/* Clear the previous host (light DOM / shadow / iframe body / portal)
|
|
574
|
+
before resolving the new one. Author iframes stay; only their body
|
|
575
|
+
children are cleared via `renderHost`. */
|
|
576
|
+
isActive && { clearHostChildren: [] },
|
|
577
|
+
{ setupRenderHost: [] },
|
|
578
|
+
isActive && (_persistedTree ? { renderChildren: [] } : { attemptLoad: [] }),
|
|
579
|
+
])
|
|
580
|
+
.onPromiseResolved(
|
|
581
|
+
"templatePromise",
|
|
582
|
+
({ isActive, persistContent }, result) => [
|
|
583
|
+
{
|
|
584
|
+
// Keep the live tree only when `persist-content` asks. Non-persist
|
|
585
|
+
// paints get `resolved` as a method arg below.
|
|
586
|
+
_persistedTree: persistContent ? result.templatePromise : null,
|
|
587
|
+
templatePromise: null,
|
|
588
|
+
isLoading: false,
|
|
589
|
+
didLoad: true,
|
|
590
|
+
isError: false,
|
|
591
|
+
},
|
|
592
|
+
isActive && { renderChildren: [result.templatePromise] },
|
|
593
|
+
]
|
|
594
|
+
)
|
|
595
|
+
.onPromiseRejected("templatePromise", ({ localName }, result) => {
|
|
596
|
+
if (result.templatePromise?.name !== "AbortError") {
|
|
597
|
+
KitLogger.error(
|
|
598
|
+
`${localName}: template load failed`,
|
|
599
|
+
result.templatePromise
|
|
600
|
+
);
|
|
601
|
+
return {
|
|
602
|
+
templatePromise: null,
|
|
603
|
+
isLoading: false,
|
|
604
|
+
didLoad: false,
|
|
605
|
+
isError: true,
|
|
606
|
+
emit: ["error"],
|
|
607
|
+
};
|
|
608
|
+
} else {
|
|
609
|
+
KitLogger.debug("templatePromise was aborted");
|
|
610
|
+
}
|
|
611
|
+
})
|
|
612
|
+
.onPropUnset("isActive", () => ({ startTeardown: [] }))
|
|
613
|
+
.onPropSet("isActive", ({ renderCallback }) => [
|
|
614
|
+
// Re-resolve the host on activate so a late-ready author iframe is
|
|
615
|
+
// picked up even if `host-ref` was set before its body existed.
|
|
616
|
+
{ setupRenderHost: [] },
|
|
617
|
+
{ startReady: [] },
|
|
618
|
+
{ firePromiseEvent: ["render", renderCallback] },
|
|
619
|
+
])
|
|
620
|
+
/* Abort in-flight template work on disconnect so test teardown / SPA
|
|
621
|
+
unmounts don't throw "Failed to find template" after the light-DOM
|
|
622
|
+
source is gone. */
|
|
623
|
+
.onDisconnected(
|
|
624
|
+
({ templatePromise, isMoving }) =>
|
|
625
|
+
!isMoving &&
|
|
626
|
+
templatePromise && {
|
|
627
|
+
setCanceledState: [],
|
|
628
|
+
}
|
|
629
|
+
)
|
|
630
|
+
.onEventDefault("render", (_, { detail }) => {
|
|
631
|
+
detail();
|
|
632
|
+
})
|
|
633
|
+
.onEventDefault("unrender", (_, { detail }) => {
|
|
634
|
+
detail();
|
|
635
|
+
})
|
|
636
|
+
.onError(({ localName }, error) => {
|
|
637
|
+
KitLogger.error(`${localName}:`, error);
|
|
638
|
+
});
|