@webjsdev/core 0.7.49 → 0.7.51
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/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +21 -21
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +1 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/attribute-reader.js +178 -0
- package/src/component/lifecycle.js +1481 -0
- package/src/component/reactive.js +171 -0
- package/src/component/server-element.js +248 -0
- package/src/component.d.ts +10 -0
- package/src/component.js +2 -1931
- package/src/html-entities.js +2179 -0
- package/src/render-client/parts.js +2007 -0
- package/src/render-client/reconciler.js +388 -0
- package/src/render-client/template-compiler.js +525 -0
- package/src/render-client.js +1 -2939
- package/src/render-server/attrs.js +160 -0
- package/src/render-server/dsd.js +445 -0
- package/src/render-server/html-scan.js +402 -0
- package/src/render-server/slots.js +213 -0
- package/src/render-server/stream.js +163 -0
- package/src/render-server/template-renderer.js +923 -0
- package/src/render-server/text.js +163 -0
- package/src/render-server.js +6 -2334
- package/src/router-client/anchors.js +44 -0
- package/src/router-client/boundaries.js +221 -0
- package/src/router-client/constants.js +187 -0
- package/src/router-client/diagnostics.js +377 -0
- package/src/router-client/dom-differ.js +709 -0
- package/src/router-client/dom-parse.js +150 -0
- package/src/router-client/events.js +139 -0
- package/src/router-client/fetch-apply.js +387 -0
- package/src/router-client/form-encoder.js +135 -0
- package/src/router-client/frames.js +329 -0
- package/src/router-client/head-merge.js +344 -0
- package/src/router-client/nav-error.js +105 -0
- package/src/router-client/navigator.js +781 -0
- package/src/router-client/prefetch.js +824 -0
- package/src/router-client/scroll.js +259 -0
- package/src/router-client/snapshot-cache.js +97 -0
- package/src/router-client/state.js +92 -0
- package/src/router-client/stream.js +206 -0
- package/src/router-client/swap.js +451 -0
- package/src/router-client/upgrade.js +261 -0
- package/src/router-client/view-transition.js +223 -0
- package/src/router-client.d.ts +10 -1
- package/src/router-client.js +75 -5345
- package/src/slot/assignment.js +304 -0
- package/src/slot/interception.js +558 -0
- package/src/slot/polyfills.js +365 -0
- package/src/slot/project.js +432 -0
- package/src/slot/sensors.js +303 -0
- package/src/slot/state.js +245 -0
- package/src/slot/symbols.js +85 -0
- package/src/slot.js +7 -2199
- package/src/webjs-config.d.ts +13 -2
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { isTemplate, MARKER } from '../html.js';
|
|
2
|
+
import {
|
|
3
|
+
reconcileFormAction, isBoundFormAction, ABSENT, assertSubmitterHasNoName, assertSubmitterType,
|
|
4
|
+
assertSubmitterHasNoValue, assertSubmitterHasNoStaticFormAction,
|
|
5
|
+
assertSubmitterHasNoFormAttribute, assertSingleSubmitterAction, resolveBoundSubmitterAttrs,
|
|
6
|
+
applyResolvedAttr, releaseSubmitterAttrs, assertConvergentSubmitter, formActionId,
|
|
7
|
+
assertIdentifiableAction, FORM_ACTION_FIELD,
|
|
8
|
+
} from '../form-action.js';
|
|
9
|
+
import { isLive } from '../directives.js';
|
|
10
|
+
import { RENDERING, SLOT_OWNER, SLOT_STATE, drainRendererBackstop, rescueAssignedNodes } from '../slot.js';
|
|
11
|
+
import { compile, submitterActionBindings, INSTANCE } from './template-compiler.js';
|
|
12
|
+
import {
|
|
13
|
+
applyPart, bindPart, currentRenderRoot, findSlotHost, setCurrentRenderRoot, updateInstance,
|
|
14
|
+
} from './parts.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render a value into a container, reusing DOM where possible.
|
|
18
|
+
*
|
|
19
|
+
* @param {unknown} value
|
|
20
|
+
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
21
|
+
*/
|
|
22
|
+
export function render(value, container) {
|
|
23
|
+
const host = /** @type any */ (container);
|
|
24
|
+
// Open the renderer-write window for the whole commit: every host-receiver
|
|
25
|
+
// write below (and in createInstance / updateInstance / clearInstance / all
|
|
26
|
+
// part commits they reach synchronously) then bypasses the slot interception
|
|
27
|
+
// that is patched onto a light host. This is the single discriminator
|
|
28
|
+
// between a renderer commit and an author write.
|
|
29
|
+
const prevRendering = host[RENDERING];
|
|
30
|
+
host[RENDERING] = true;
|
|
31
|
+
const prevRenderRoot = currentRenderRoot;
|
|
32
|
+
setCurrentRenderRoot(container);
|
|
33
|
+
try {
|
|
34
|
+
const prev = host[INSTANCE];
|
|
35
|
+
|
|
36
|
+
if (isTemplate(value)) {
|
|
37
|
+
const tr = /** @type {import('../html.js').TemplateResult} */ (value);
|
|
38
|
+
if (prev && prev.strings === tr.strings) {
|
|
39
|
+
updateInstance(prev, tr.values, reconcileFormActions);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (prev) clearInstance(prev, container);
|
|
43
|
+
|
|
44
|
+
// Light DOM hydration: if container has SSR content (marked by
|
|
45
|
+
// <!--webjs-hydrate-->), remove the marker and proceed with normal
|
|
46
|
+
// rendering. The content will be replaced with identical output -
|
|
47
|
+
// no visible flash because SSR and client render produce the same HTML.
|
|
48
|
+
const firstChild = container.firstChild;
|
|
49
|
+
if (firstChild && firstChild.nodeType === 8 && /** @type {Comment} */ (firstChild).data === 'webjs-hydrate') {
|
|
50
|
+
firstChild.remove();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pre-set the symbol to an explicit null BEFORE the commit,
|
|
54
|
+
// UNCONDITIONALLY: if createInstance throws after its replaceChildren
|
|
55
|
+
// (e.g. inside the slot-part apply loop), the finally-drain must see
|
|
56
|
+
// "rendered, no instance" (discard the commit's records), never
|
|
57
|
+
// "never rendered" (fold and corrupt) and never a STALE cleared prev
|
|
58
|
+
// instance on the template-swap path (whose bookends would misclassify
|
|
59
|
+
// the half-committed new roots as unowned and fold them).
|
|
60
|
+
host[INSTANCE] = null;
|
|
61
|
+
const inst = createInstance(tr, container);
|
|
62
|
+
host[INSTANCE] = inst;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Non-template value: treat as a single text child.
|
|
67
|
+
if (prev) clearInstance(prev, container);
|
|
68
|
+
host[INSTANCE] = null;
|
|
69
|
+
container.replaceChildren();
|
|
70
|
+
if (value == null || value === false || value === true) return;
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
for (const v of value) {
|
|
73
|
+
const text = document.createTextNode(String(v ?? ''));
|
|
74
|
+
container.appendChild(text);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
container.appendChild(document.createTextNode(String(value)));
|
|
79
|
+
} finally {
|
|
80
|
+
setCurrentRenderRoot(prevRenderRoot);
|
|
81
|
+
host[RENDERING] = prevRendering;
|
|
82
|
+
// Outermost window closing: drain this commit's childList records off the
|
|
83
|
+
// slot backstop (drainRendererBackstop processes them with a renderer-output
|
|
84
|
+
// skip when an instance exists, else discards), so the backstop never folds
|
|
85
|
+
// renderer output. Mirrors withRendererWrites, used by the async paths.
|
|
86
|
+
if (!prevRendering) drainRendererBackstop(host);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Converge every candidate bound form and submitter in this template, after all
|
|
92
|
+
* of its parts have committed. A no-op for the overwhelming majority of
|
|
93
|
+
* templates, which carry no `<form action=${...}>` at all and therefore no
|
|
94
|
+
* record.
|
|
95
|
+
*
|
|
96
|
+
* FORMS FIRST, submitters second, regardless of the order the records were
|
|
97
|
+
* collected in. Nothing reads an enclosing form's boundness any more (#1307
|
|
98
|
+
* made a bound submitter self-sufficient), but the ordering still makes a
|
|
99
|
+
* form's RELEASE run before its submitters reconcile, so it is kept rather
|
|
100
|
+
* than churned. See the note in the body.
|
|
101
|
+
*
|
|
102
|
+
* @param {FormActionRecord[] | null} formActions
|
|
103
|
+
* @param {BoundPart[]} bound
|
|
104
|
+
* @param {unknown[]} values
|
|
105
|
+
*/
|
|
106
|
+
function reconcileFormActions(formActions, bound, values) {
|
|
107
|
+
if (!formActions) return;
|
|
108
|
+
// Forms first, then submitters. The original motivation was a boundness read
|
|
109
|
+
// that is gone with #1307, but the ordering still makes a form's RELEASE run
|
|
110
|
+
// before its submitters reconcile, so it is kept rather than churned.
|
|
111
|
+
for (const pass of [true, false]) {
|
|
112
|
+
for (const rec of formActions) {
|
|
113
|
+
if (rec.isForm !== pass) continue;
|
|
114
|
+
// With more than one action hole, the BOUND one decides, whichever
|
|
115
|
+
// position it is written in. Picking `actionIdxs[0]` blindly would send
|
|
116
|
+
// `<form action=${'/url'} action=${boundFn}>` down the release path and
|
|
117
|
+
// ship the broken form SSR refuses outright.
|
|
118
|
+
let idx = rec.actionIdxs[0];
|
|
119
|
+
const targetAttr = rec.isForm ? 'action' : 'formaction';
|
|
120
|
+
for (const i of rec.actionIdxs) {
|
|
121
|
+
if (isBoundFormAction(resolveHoleValue(values[i]), targetAttr, rec.tag)) { idx = i; break; }
|
|
122
|
+
}
|
|
123
|
+
const part = bound[idx];
|
|
124
|
+
if (!part || !part.el) continue;
|
|
125
|
+
const val = resolveHoleValue(values[idx]);
|
|
126
|
+
if (rec.isForm) {
|
|
127
|
+
reconcileFormAction(
|
|
128
|
+
/** @type any */ (part.el),
|
|
129
|
+
val,
|
|
130
|
+
effectiveFormAttr(rec.methodParts, rec.staticMethod, values),
|
|
131
|
+
effectiveFormAttr(rec.enctypeParts, rec.staticEnctype, values),
|
|
132
|
+
rec,
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
// What SSR would have emitted for this pass. An attribute hole always
|
|
136
|
+
// emits (even `name=${null}`, as `name=""`); a boolean hole emits only
|
|
137
|
+
// when truthy. Both identity channels ask the same question, through
|
|
138
|
+
// one predicate, so they cannot drift apart again.
|
|
139
|
+
const emits = (parts) => parts.some((np) => (np.kind === 'bool'
|
|
140
|
+
? !!resolveHoleValue(values[np.i])
|
|
141
|
+
: true));
|
|
142
|
+
reconcileSubmitterAction(
|
|
143
|
+
/** @type any */ (part.el), val, rec,
|
|
144
|
+
emits(rec.nameParts), emits(rec.valueParts),
|
|
145
|
+
effectiveFormAttr(rec.methodParts, rec.staticMethod, values),
|
|
146
|
+
effectiveFormAttr(rec.enctypeParts, rec.staticEnctype, values),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Remove the identity channel previously injected into a submitter.
|
|
155
|
+
*
|
|
156
|
+
* Only ours is removed. `submitterActionBindings` remembers the elements this
|
|
157
|
+
* renderer stamped, and the `name` check covers an SSR'd button meeting its
|
|
158
|
+
* template for the first time on hydration. A button that never carried a
|
|
159
|
+
* binding is left alone, so an author's own `name` / `value` survives a
|
|
160
|
+
* re-render that happens to pass a non-action value.
|
|
161
|
+
*
|
|
162
|
+
* @param {Element} el
|
|
163
|
+
*/
|
|
164
|
+
function releaseSubmitterAction(el) {
|
|
165
|
+
const injected = submitterActionBindings.has(el) || el.getAttribute('name') === FORM_ACTION_FIELD;
|
|
166
|
+
if (!injected) return;
|
|
167
|
+
if (el.getAttribute('name') === FORM_ACTION_FIELD) el.removeAttribute('name');
|
|
168
|
+
el.removeAttribute('value');
|
|
169
|
+
submitterActionBindings.delete(el);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Converge a live submitter on what SSR emitted for `<button formaction=${fn}>`
|
|
174
|
+
* (#1207, #1307): the `formaction` attribute gone, the identity in the button's
|
|
175
|
+
* own `name` / `value` pair, and the submission the framework supplies in its
|
|
176
|
+
* own `formmethod` / `formenctype`.
|
|
177
|
+
*
|
|
178
|
+
* There is no enclosing-form question here any more. It used to be the hard
|
|
179
|
+
* part of this function, asked once per element and BEST EFFORT because a
|
|
180
|
+
* fragment reconciling detached (a submitter inside a `repeat()` or an array
|
|
181
|
+
* item, whose form lives in the parent template) has no answer to give. With
|
|
182
|
+
* the submission attributes now on the button itself (#1307) the button needs
|
|
183
|
+
* nothing from the form, so the question stopped mattering and the whole
|
|
184
|
+
* asymmetry with SSR went with it.
|
|
185
|
+
*
|
|
186
|
+
* @param {Element} el
|
|
187
|
+
* @param {unknown} value
|
|
188
|
+
* @param {FormActionRecord} rec
|
|
189
|
+
* @param {boolean} emitsName whether SSR would emit a `name` attribute this pass
|
|
190
|
+
* @param {boolean} emitsValue whether SSR would emit a `value` attribute this pass
|
|
191
|
+
* @param {string | typeof ABSENT} authoredFormMethod what the template supplies for `formmethod`
|
|
192
|
+
* @param {string | typeof ABSENT} authoredFormEnctype what the template supplies for `formenctype`
|
|
193
|
+
*/
|
|
194
|
+
function reconcileSubmitterAction(
|
|
195
|
+
el, value, rec, emitsName, emitsValue, authoredFormMethod, authoredFormEnctype,
|
|
196
|
+
) {
|
|
197
|
+
const id = typeof value === 'function' ? formActionId(value) : null;
|
|
198
|
+
if (!id) {
|
|
199
|
+
// A function that was MEANT as an action still refuses, so a button never
|
|
200
|
+
// silently submits the form's action instead of its own.
|
|
201
|
+
if (typeof value === 'function') assertIdentifiableAction(null, el.localName);
|
|
202
|
+
releaseSubmitterAction(el);
|
|
203
|
+
// The submission attributes the framework supplied go with the identity, or
|
|
204
|
+
// a released button keeps a `formmethod` / `formenctype` SSR does not emit
|
|
205
|
+
// for the same template (#1307).
|
|
206
|
+
releaseSubmitterAttrs(el, authoredFormMethod, authoredFormEnctype, rec.propAttrs);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Template-shaped refusals first, from the compiled record, because the live
|
|
210
|
+
// element may already carry this renderer's own `name` / `value`.
|
|
211
|
+
assertSingleSubmitterAction(rec.duplicateAction, el.localName);
|
|
212
|
+
assertConvergentSubmitter(rec.propAttrs, el.localName);
|
|
213
|
+
if (rec.staticAction) assertSubmitterHasNoStaticFormAction(el.localName);
|
|
214
|
+
if (rec.authoredValue || emitsValue) assertSubmitterHasNoValue(el.localName);
|
|
215
|
+
if (rec.authoredName || emitsName) {
|
|
216
|
+
// Judged on the PART, not on what it resolved to this pass. `name=${null}`
|
|
217
|
+
// leaves no attribute here while SSR emits `name=""` beside the identity,
|
|
218
|
+
// so reading the live value back returned '' and waved through a template
|
|
219
|
+
// SSR refuses. The template supplying a `name` channel at all is the
|
|
220
|
+
// conflict, whatever today's value happens to be.
|
|
221
|
+
assertSubmitterHasNoName(el.getAttribute('name') || FORM_ACTION_FIELD, el.localName, false);
|
|
222
|
+
}
|
|
223
|
+
if (rec.authoredForm || el.hasAttribute('form')) assertSubmitterHasNoFormAttribute(el.localName);
|
|
224
|
+
assertSubmitterType(el.localName, el.getAttribute('type'));
|
|
225
|
+
|
|
226
|
+
// The submission decision, made from the TEMPLATE rather than by reading the
|
|
227
|
+
// DOM back, exactly as `reconcileFormAction` makes it for a bound form. The
|
|
228
|
+
// distinction the DOM cannot preserve is the same one: `?formmethod=${false}`
|
|
229
|
+
// and `formmethod=${null}` both leave no attribute, and SSR resolves them to
|
|
230
|
+
// opposite answers.
|
|
231
|
+
const resolved = resolveBoundSubmitterAttrs(
|
|
232
|
+
el.localName, authoredFormMethod, authoredFormEnctype,
|
|
233
|
+
);
|
|
234
|
+
el.removeAttribute('formaction');
|
|
235
|
+
el.setAttribute('name', FORM_ACTION_FIELD);
|
|
236
|
+
el.setAttribute('value', id);
|
|
237
|
+
// AFTER the identity, matching SSR's byte order: the identity replaces the
|
|
238
|
+
// `formaction=` hole in place and the submission pair is appended at the `>`.
|
|
239
|
+
// The differential parity suite compares these two renderers byte for byte,
|
|
240
|
+
// so the order is part of the contract, not a detail.
|
|
241
|
+
applyResolvedAttr(el, 'formmethod', authoredFormMethod, resolved.formMethod);
|
|
242
|
+
applyResolvedAttr(el, 'formenctype', authoredFormEnctype, resolved.formEnctype);
|
|
243
|
+
submitterActionBindings.set(el, id);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* ================================================================
|
|
247
|
+
* Instance lifecycle
|
|
248
|
+
* ================================================================ */
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Resolve what SSR would have emitted for one attribute of a candidate form.
|
|
254
|
+
*
|
|
255
|
+
* The per-kind rules mirror `render-server.js` exactly, which is the whole
|
|
256
|
+
* point: a boolean hole emits nothing when falsy (`if (val) out += name+'=""'`)
|
|
257
|
+
* while an attribute hole emits an EMPTY value for null (`String(val ?? '')`).
|
|
258
|
+
* Those two produce the same DOM and opposite verdicts, so only the template
|
|
259
|
+
* can tell them apart.
|
|
260
|
+
*
|
|
261
|
+
* @param {FormAttrPart[]} attrParts
|
|
262
|
+
* @param {string | null} staticValue
|
|
263
|
+
* @param {unknown[]} values
|
|
264
|
+
* @returns {string | typeof ABSENT}
|
|
265
|
+
*/
|
|
266
|
+
function effectiveFormAttr(attrParts, staticValue, values) {
|
|
267
|
+
for (const p of attrParts) {
|
|
268
|
+
if (p.kind === 'bool') return resolveHoleValue(values[p.i]) ? '' : ABSENT;
|
|
269
|
+
if (p.kind === 'attr') {
|
|
270
|
+
const v = resolveHoleValue(values[p.i]);
|
|
271
|
+
return v == null ? '' : String(v);
|
|
272
|
+
}
|
|
273
|
+
// A mixed attribute is the concatenation of its static pieces and EVERY one
|
|
274
|
+
// of its holes, so the anchor's own value is only part of the answer.
|
|
275
|
+
const statics = p.statics || [];
|
|
276
|
+
const group = p.group || [];
|
|
277
|
+
let out = statics[0] || '';
|
|
278
|
+
for (let j = 0; j < group.length; j++) {
|
|
279
|
+
out += String(resolveHoleValue(values[group[j]]) ?? '');
|
|
280
|
+
out += statics[j + 1] || '';
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
return staticValue == null ? ABSENT : staticValue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Unwrap a hole's value the same way `applyPart` does, so the reconcile judges
|
|
289
|
+
* what was actually committed. `live()` in particular wraps its value, and
|
|
290
|
+
* reading the wrapper would make a bound action look like a plain object.
|
|
291
|
+
*
|
|
292
|
+
* @param {unknown} v
|
|
293
|
+
* @returns {unknown}
|
|
294
|
+
*/
|
|
295
|
+
function resolveHoleValue(v) {
|
|
296
|
+
return isLive(v) ? /** @type any */ (v).value : v;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {import('../html.js').TemplateResult} tr
|
|
301
|
+
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
302
|
+
*/
|
|
303
|
+
function createInstance(tr, container) {
|
|
304
|
+
const { templateEl, parts, formActions } = compile(tr);
|
|
305
|
+
const frag = /** @type DocumentFragment */ (templateEl.content.cloneNode(true));
|
|
306
|
+
// Bookend markers bound the instance so we can tear it down cleanly.
|
|
307
|
+
const startNode = document.createComment(`${MARKER}s`);
|
|
308
|
+
const endNode = document.createComment(`${MARKER}e`);
|
|
309
|
+
|
|
310
|
+
const bound = parts.map((p) => bindPart(p, frag));
|
|
311
|
+
const lastValues = [];
|
|
312
|
+
for (let i = 0; i < tr.values.length; i++) {
|
|
313
|
+
applyPart(bound[i], tr.values[i], undefined, tr.values, reconcileFormActions);
|
|
314
|
+
lastValues.push(tr.values[i]);
|
|
315
|
+
}
|
|
316
|
+
reconcileFormActions(formActions, bound, tr.values);
|
|
317
|
+
|
|
318
|
+
/** @type any */ (container).replaceChildren(startNode, ...frag.childNodes, endNode);
|
|
319
|
+
|
|
320
|
+
// Slot parts have no value-hole to drive applyPart from the loop above.
|
|
321
|
+
// Apply them once now that the fragment is inserted into the live
|
|
322
|
+
// container, so each slot can locate its host by walking parents and
|
|
323
|
+
// schedule the first projection through slot.js. Stamp each slot with the
|
|
324
|
+
// host whose TEMPLATE produced it (this container), so a FORWARDED slot
|
|
325
|
+
// (rendered here but nested inside a child component) routes to this host
|
|
326
|
+
// rather than to the child the structural walk would pick. Only an element
|
|
327
|
+
// container is a host; a fragment/shadow container leaves the structural
|
|
328
|
+
// path in place.
|
|
329
|
+
//
|
|
330
|
+
// These MUST be the symbols slot.js created. They are `Symbol(...)`, which
|
|
331
|
+
// is unique per call, not `Symbol.for(...)`, which looks one up in the
|
|
332
|
+
// global registry by string. Reaching for the registry produces a different
|
|
333
|
+
// symbol that no host carries, so `ownerHost` is always null, the stamp
|
|
334
|
+
// never lands, and a forwarded slot silently routes to the wrong host.
|
|
335
|
+
const ownerHost =
|
|
336
|
+
/** @type {any} */ (container).nodeType === 1 && /** @type {any} */ (container)[SLOT_STATE]
|
|
337
|
+
? container
|
|
338
|
+
: null;
|
|
339
|
+
for (const part of bound) {
|
|
340
|
+
if (part.kind === 'slot') {
|
|
341
|
+
if (ownerHost && part.slotEl) /** @type {any} */ (part.slotEl)[SLOT_OWNER] = ownerHost;
|
|
342
|
+
applyPart(part, undefined, undefined, [], reconcileFormActions);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { strings: tr.strings, bound, lastValues, startNode, endNode };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @param {TemplateInstance} inst
|
|
351
|
+
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
352
|
+
*/
|
|
353
|
+
function clearInstance(inst, container) {
|
|
354
|
+
// Dispose event listeners on event parts, unbind active refs on
|
|
355
|
+
// element parts, and rescue any projected children sitting inside
|
|
356
|
+
// slot parts so they survive teardown of a collapsing conditional
|
|
357
|
+
// fragment.
|
|
358
|
+
for (const p of inst.bound) {
|
|
359
|
+
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher);
|
|
360
|
+
if (p.kind === 'element') {
|
|
361
|
+
// Guarded for the same reason as the sibling in `disposeInstance`, and
|
|
362
|
+
// the stakes are higher here: this is the container-level teardown
|
|
363
|
+
// `render()` runs before a template swap, so a throw skips the rest of
|
|
364
|
+
// this loop AND the `replaceChildren()` below, leaving the old DOM in
|
|
365
|
+
// place with `host[INSTANCE]` never reassigned. Since `lastTarget` is
|
|
366
|
+
// cleared only after the write, every later swap of that container
|
|
367
|
+
// then throws at the same part, permanently.
|
|
368
|
+
const prev = /** @type any */ (p).lastTarget;
|
|
369
|
+
if (prev) {
|
|
370
|
+
if (typeof prev === 'function') {
|
|
371
|
+
try { prev(undefined); } catch {}
|
|
372
|
+
} else if (typeof prev === 'object') {
|
|
373
|
+
try { prev.value = undefined; } catch {}
|
|
374
|
+
}
|
|
375
|
+
/** @type any */ (p).lastTarget = undefined;
|
|
376
|
+
/** @type any */ (p).__lastEl = undefined;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (p.kind === 'slot') {
|
|
380
|
+
// Detach record-owned children before the teardown disposes the
|
|
381
|
+
// slot subtree; the record keeps the refs, so a re-created slot
|
|
382
|
+
// re-places the SAME nodes (children are values, #1015).
|
|
383
|
+
const host = findSlotHost(p.slotEl);
|
|
384
|
+
if (host) rescueAssignedNodes(host, p.slotEl);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** @type any */ (container).replaceChildren();
|
|
388
|
+
}
|