@webjsdev/core 0.8.0 → 0.9.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/README.md +5 -0
- package/dist/webjs-core-browser.js +6 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +20 -19
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -6
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +14 -9
- package/index.d.ts +22 -10
- package/index.js +17 -9
- package/package.json +15 -2
- package/src/action-effects-client.d.ts +5 -0
- package/src/action-effects-client.js +84 -0
- package/src/action-seed-client.js +5 -5
- package/src/asset-url.js +12 -13
- package/src/async-directive.d.ts +18 -0
- package/src/async-directive.js +88 -0
- package/src/attribute-reader.js +197 -23
- package/src/binding-prefixes.js +0 -12
- package/src/component/lifecycle.js +760 -209
- package/src/component/reactive.js +2 -10
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +151 -41
- package/src/context.d.ts +94 -18
- package/src/context.js +341 -217
- package/src/converters.d.ts +7 -0
- package/src/converters.js +38 -0
- package/src/css.d.ts +32 -6
- package/src/css.js +178 -14
- package/src/directive.d.ts +58 -0
- package/src/directive.js +119 -0
- package/src/directives.d.ts +38 -1
- package/src/directives.js +158 -1
- package/src/form-action.d.ts +13 -0
- package/src/form-action.js +139 -4
- package/src/hot.js +23 -0
- package/src/html.d.ts +4 -0
- package/src/html.js +35 -3
- package/src/is-server.d.ts +2 -0
- package/src/is-server.js +18 -0
- package/src/jsx-guard.js +130 -0
- package/src/metadata.d.ts +22 -3
- package/src/nav.d.ts +4 -1
- package/src/nav.js +44 -11
- package/src/optimistic.js +2 -2
- package/src/refresh-header.js +52 -0
- package/src/registry.js +10 -1
- package/src/render-client/adopt-slots.js +8 -3
- package/src/render-client/parts.js +745 -31
- package/src/render-client/reconciler.js +56 -11
- package/src/render-client/template-compiler.js +139 -44
- package/src/render-client.d.ts +5 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +34 -0
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +61 -8
- package/src/render-server/served-attrs.js +1 -2
- package/src/render-server/walk.js +135 -3
- package/src/render-server.d.ts +3 -0
- package/src/render-server.js +2 -0
- package/src/repeat.d.ts +7 -2
- package/src/repeat.js +20 -6
- package/src/router-client/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +86 -5
- package/src/router-client/navigator.js +45 -9
- package/src/router-client/prefetch.js +3 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/swap.js +4 -1
- package/src/routes.d.ts +75 -18
- package/src/serialize.d.ts +2 -0
- package/src/serialize.js +35 -4
- package/src/signal.d.ts +7 -0
- package/src/signal.js +26 -6
- package/src/slot/polyfills.js +3 -3
- package/src/slot.js +4 -8
- package/src/static-html.d.ts +22 -0
- package/src/static-html.js +144 -0
- package/src/task.d.ts +63 -20
- package/src/task.js +326 -293
- package/src/webjs-config.d.ts +1 -8
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
assertNotFunctionActionAttr, assertNotFunctionReflectedActionProp, isBoundFormAction,
|
|
6
6
|
} from '../form-action.js';
|
|
7
7
|
import { isRepeat } from '../repeat.js';
|
|
8
|
+
import { isDirectiveResult, PartType } from '../directive.js';
|
|
9
|
+
import { isSignal } from '../signal.js';
|
|
8
10
|
import {
|
|
9
11
|
isUnsafeHTML, isLive, isKeyed, isGuard, isTemplateContent, isRef, isCache, isUntil, isAsyncAppend, isAsyncReplace, isWatch
|
|
10
12
|
} from '../directives.js';
|
|
@@ -34,6 +36,29 @@ export function setCurrentRenderRoot(root) {
|
|
|
34
36
|
currentRenderRoot = root;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Where the child part being committed renders, while a fresh template's
|
|
41
|
+
* parts still sit in their detached fragment. A directive reading
|
|
42
|
+
* `part.parentNode` in `update()` gets this, lit's "logical DOM parent", rather
|
|
43
|
+
* than the fragment the clone is built in.
|
|
44
|
+
* @type {Node | null}
|
|
45
|
+
*/
|
|
46
|
+
let logicalParent = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The logical parent for a marker's DOM parent: itself, unless it is the
|
|
50
|
+
* detached fragment a template clone is built in (not a shadow root and not
|
|
51
|
+
* the fragment being rendered into).
|
|
52
|
+
* @param {Node | null} node
|
|
53
|
+
* @returns {Node | null}
|
|
54
|
+
*/
|
|
55
|
+
function logicalParentOf(node) {
|
|
56
|
+
if (node && node.nodeType === 11 && !(/** @type any */ (node)).host && node !== currentRenderRoot) {
|
|
57
|
+
return logicalParent ?? currentRenderRoot ?? node;
|
|
58
|
+
}
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
/**
|
|
38
63
|
* Open the renderer-write window on a light-DOM host while `fn` commits into
|
|
39
64
|
* it, so the host's patched slot-interception methods delegate to native and
|
|
@@ -180,7 +205,7 @@ export function bindPart(p, root) {
|
|
|
180
205
|
if (p.kind === 'attr-mixed') return { kind: 'attr-mixed', el, name: p.name || '', statics: p.statics || [], group: p.group || [] };
|
|
181
206
|
if (p.kind === 'prop') return { kind: 'prop', el, name: p.name || '' };
|
|
182
207
|
if (p.kind === 'bool') return { kind: 'bool', el, name: p.name || '' };
|
|
183
|
-
if (p.kind === 'element') return { kind: 'element', el };
|
|
208
|
+
if (p.kind === 'element') return { kind: 'element', el, refHost: currentRenderHost };
|
|
184
209
|
if (p.kind === 'slot') {
|
|
185
210
|
const slotEl = /** @type {HTMLSlotElement} */ (el);
|
|
186
211
|
// Defer fallback-strip and SLOT_FALLBACK_FRAG installation to apply
|
|
@@ -193,19 +218,451 @@ export function bindPart(p, root) {
|
|
|
193
218
|
throw new Error(`unknown part kind ${/** @type any */(p).kind}`);
|
|
194
219
|
}
|
|
195
220
|
|
|
221
|
+
/* ================================================================
|
|
222
|
+
* Custom directives (`@webjsdev/core/directive`, lit's directive API).
|
|
223
|
+
*
|
|
224
|
+
* One directive INSTANCE lives per part (per piece of an interpolated
|
|
225
|
+
* attribute, under its own slot) for as long as the same class renders
|
|
226
|
+
* there, in a record `{ ctor, inst, child }`. `child` holds the record of a
|
|
227
|
+
* directive the first one RETURNED, so a directive composing another keeps
|
|
228
|
+
* both instances. A record is replaced (and an async one disconnected) when a
|
|
229
|
+
* different class, or a plain value, takes the part over.
|
|
230
|
+
* ================================================================ */
|
|
231
|
+
|
|
232
|
+
/** Per-host set of live async-directive records, toggled with the host. */
|
|
233
|
+
const ASYNC_DIRECTIVES = Symbol.for('webjs.asyncDirectives');
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The lit-shaped part handed to `Directive#update()`.
|
|
237
|
+
* @param {any} part
|
|
238
|
+
* @param {number} [piece] the piece index, for a piece of an interpolated attribute
|
|
239
|
+
*/
|
|
240
|
+
function directivePartFacade(part, piece) {
|
|
241
|
+
const options = { host: part.__eventHost ?? part.eventHost ?? currentRenderHost };
|
|
242
|
+
switch (part.kind) {
|
|
243
|
+
case 'child':
|
|
244
|
+
return {
|
|
245
|
+
type: PartType.CHILD,
|
|
246
|
+
options,
|
|
247
|
+
get parentNode() { return logicalParentOf(part.marker.parentNode); },
|
|
248
|
+
get endNode() { return part.marker; },
|
|
249
|
+
get startNode() {
|
|
250
|
+
const c = part.child;
|
|
251
|
+
const first = c && (c.startNode || (Array.isArray(c) ? c[0] : undefined));
|
|
252
|
+
return first ? first.previousSibling : part.marker.previousSibling;
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
case 'attr':
|
|
256
|
+
case 'attr-mixed':
|
|
257
|
+
return {
|
|
258
|
+
type: PartType.ATTRIBUTE,
|
|
259
|
+
options,
|
|
260
|
+
element: part.el,
|
|
261
|
+
name: part.name,
|
|
262
|
+
tagName: part.el.tagName,
|
|
263
|
+
strings: part.kind === 'attr-mixed' ? part.statics : undefined,
|
|
264
|
+
piece,
|
|
265
|
+
};
|
|
266
|
+
case 'prop':
|
|
267
|
+
return { type: PartType.PROPERTY, options, element: part.el, name: part.name, tagName: part.el.tagName };
|
|
268
|
+
case 'bool':
|
|
269
|
+
return { type: PartType.BOOLEAN_ATTRIBUTE, options, element: part.el, name: part.name, tagName: part.el.tagName };
|
|
270
|
+
case 'event':
|
|
271
|
+
return { type: PartType.EVENT, options, element: part.el, name: part.name, tagName: part.el.tagName };
|
|
272
|
+
default:
|
|
273
|
+
return { type: PartType.ELEMENT, options, element: part.el };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Report a directive record disconnected (async ones run `disconnected()`)
|
|
279
|
+
* and drop it from its host's registry, with every record it chained to.
|
|
280
|
+
* @param {any} rec
|
|
281
|
+
*/
|
|
282
|
+
function disconnectDirectiveRecord(rec) {
|
|
283
|
+
for (let r = rec; r; r = r.child) {
|
|
284
|
+
r.released = true;
|
|
285
|
+
if (typeof r.inst._$webjsSetConnected === 'function') {
|
|
286
|
+
r.inst._$webjsCommit = undefined;
|
|
287
|
+
try { r.inst._$webjsSetConnected(false); } catch (err) {
|
|
288
|
+
console.error('[webjs] an async directive\'s disconnected() threw:', err);
|
|
289
|
+
}
|
|
290
|
+
r.host?.[ASYNC_DIRECTIVES]?.delete(r);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Register an async directive record with its host so it follows the host's
|
|
297
|
+
* connection. The host gets ONE reactive controller, on its first async
|
|
298
|
+
* directive, that flips every registered record.
|
|
299
|
+
* @param {any} host
|
|
300
|
+
* @param {any} rec
|
|
301
|
+
*/
|
|
302
|
+
function registerAsyncDirective(host, rec) {
|
|
303
|
+
if (!host || typeof host.addController !== 'function') return;
|
|
304
|
+
let set = host[ASYNC_DIRECTIVES];
|
|
305
|
+
if (!set) {
|
|
306
|
+
const records = (set = host[ASYNC_DIRECTIVES] = new Set());
|
|
307
|
+
host.addController({
|
|
308
|
+
hostConnected() { for (const r of [...records]) r.inst._$webjsSetConnected(true); },
|
|
309
|
+
hostDisconnected() { for (const r of [...records]) r.inst._$webjsSetConnected(false); },
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
set.add(rec);
|
|
313
|
+
rec.host = host;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Resolve a value that may be a custom directive result into what it renders,
|
|
318
|
+
* keeping the directive instances on `holder[key]` (and their `child` chain).
|
|
319
|
+
*
|
|
320
|
+
* @param {any} part
|
|
321
|
+
* @param {any} holder the object the record lives on (the part, or its piece map)
|
|
322
|
+
* @param {string | number} key
|
|
323
|
+
* @param {unknown} value
|
|
324
|
+
* @param {(v: unknown) => void} commit applies a value `setValue()` pushes
|
|
325
|
+
* @param {boolean} releaseTop when the value is NOT a directive, whether to
|
|
326
|
+
* release a record left from an earlier render (false while a wrapper
|
|
327
|
+
* directive such as keyed / guard / cache passes its inner value on)
|
|
328
|
+
* @param {number} [piece]
|
|
329
|
+
* @returns {unknown}
|
|
330
|
+
*/
|
|
331
|
+
function resolveDirectiveChain(part, holder, key, value, commit, releaseTop, piece) {
|
|
332
|
+
if (part.__settingValue) return value;
|
|
333
|
+
let parent = holder;
|
|
334
|
+
/** @type {string | number} */
|
|
335
|
+
let slot = key;
|
|
336
|
+
let depth = 0;
|
|
337
|
+
while (isDirectiveResult(value)) {
|
|
338
|
+
if (++depth > 100) throw new Error('[webjs] a custom directive kept returning directives (more than 100 levels deep).');
|
|
339
|
+
const d = /** @type {any} */ (value);
|
|
340
|
+
let rec = parent[slot];
|
|
341
|
+
if (!rec || rec.ctor !== d.ctor) {
|
|
342
|
+
if (rec) disconnectDirectiveRecord(rec);
|
|
343
|
+
const facade = directivePartFacade(part, piece);
|
|
344
|
+
/** @type {any} */
|
|
345
|
+
const f = facade;
|
|
346
|
+
const inst = new d.ctor({ type: f.type, name: f.name, tagName: f.tagName, strings: f.strings });
|
|
347
|
+
rec = parent[slot] = { ctor: d.ctor, inst, facade, child: undefined, host: undefined, released: false };
|
|
348
|
+
if (typeof inst._$webjsSetConnected === 'function') {
|
|
349
|
+
// Record the owning component while its render is on the stack, the
|
|
350
|
+
// only moment it is knowable, so a `setValue()` commit throw reaches
|
|
351
|
+
// that component's renderError() (the `watch` rule).
|
|
352
|
+
if (currentRenderRoot) part.__commitOwner = boundaryOwnerOf(currentRenderRoot);
|
|
353
|
+
if (part.kind !== 'child') part.__eventHost ??= currentRenderHost;
|
|
354
|
+
const own = rec;
|
|
355
|
+
// A container `render()` disconnected (lit's `RootPart.setConnected`)
|
|
356
|
+
// starts the directives it creates disconnected, as lit does.
|
|
357
|
+
if (currentRenderRoot?.[ROOT_DISCONNECTED]) inst.isConnected = false;
|
|
358
|
+
// So does a component rendering while it is out of the document: its
|
|
359
|
+
// directives start disconnected and are reconnected with it.
|
|
360
|
+
const owner = part.__eventHost ?? currentRenderHost;
|
|
361
|
+
if (owner && owner.isConnected === false && typeof owner.addController === 'function') inst.isConnected = false;
|
|
362
|
+
inst._$webjsCommit = (v) => {
|
|
363
|
+
if (own.released) return;
|
|
364
|
+
try {
|
|
365
|
+
// A directive `setValue()` hands on resolves UNDER this one, so its
|
|
366
|
+
// instance lives as long as this directive does (lit passes the
|
|
367
|
+
// directive as the parent to `_$setValue`).
|
|
368
|
+
commitOutOfBand(part, () => {
|
|
369
|
+
const resolved = resolveDirectiveChain(part, own, 'child', v, commit, true, piece);
|
|
370
|
+
if (resolved !== noChange) commit(resolved);
|
|
371
|
+
});
|
|
372
|
+
} catch (err) {
|
|
373
|
+
reportOutOfBandCommitError(part, err);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
registerAsyncDirective(part.__eventHost ?? currentRenderHost, rec);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
value = rec.inst.update(rec.facade, d.values);
|
|
380
|
+
parent = rec;
|
|
381
|
+
slot = 'child';
|
|
382
|
+
}
|
|
383
|
+
// A directive that answers `noChange` keeps everything below it, a nested
|
|
384
|
+
// directive's instance included, as lit's `resolveDirective` returns early.
|
|
385
|
+
if (parent[slot] && value !== noChange && (depth > 0 || releaseTop)) {
|
|
386
|
+
disconnectDirectiveRecord(parent[slot]);
|
|
387
|
+
parent[slot] = undefined;
|
|
388
|
+
}
|
|
389
|
+
return value;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Set on a container whose `render()` result was disconnected through
|
|
394
|
+
* `setConnected(false)`.
|
|
395
|
+
*/
|
|
396
|
+
export const ROOT_DISCONNECTED = Symbol('webjs.rootDisconnected');
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Report every async directive in a rendered template instance connected or
|
|
400
|
+
* disconnected, in template order, the walk lit's `RootPart.setConnected`
|
|
401
|
+
* makes. `disconnected()` / `reconnected()` run once per transition.
|
|
402
|
+
* @param {any} inst
|
|
403
|
+
* @param {boolean} connected
|
|
404
|
+
*/
|
|
405
|
+
export function setInstanceDirectivesConnected(inst, connected) {
|
|
406
|
+
if (!inst || !inst.bound) return;
|
|
407
|
+
for (const p of inst.bound) setPartDirectivesConnected(p, connected);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Connect or disconnect the directives one part holds, and those in what it
|
|
412
|
+
* rendered, an array item that is a child part of its own included.
|
|
413
|
+
* @param {any} p
|
|
414
|
+
* @param {boolean} connected
|
|
415
|
+
*/
|
|
416
|
+
function setPartDirectivesConnected(p, connected) {
|
|
417
|
+
/** @param {any} rec */
|
|
418
|
+
const flip = (rec) => {
|
|
419
|
+
for (let r = rec; r; r = r.child) {
|
|
420
|
+
if (typeof r.inst._$webjsSetConnected === 'function') r.inst._$webjsSetConnected(connected);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
flip(p.__directive);
|
|
424
|
+
if (p.__pieceDirectives) for (const k of Object.keys(p.__pieceDirectives)) flip(p.__pieceDirectives[k]);
|
|
425
|
+
const c = p.kind === 'child' ? p.child : null;
|
|
426
|
+
if (!c) return;
|
|
427
|
+
if (c.kind === 'repeat') for (const i of c.map.values()) setInstanceDirectivesConnected(i, connected);
|
|
428
|
+
else if (c.kind === 'array') {
|
|
429
|
+
for (const it of c.items) {
|
|
430
|
+
if (it.type === 'tpl') setInstanceDirectivesConnected(it.inst, connected);
|
|
431
|
+
else if (it.type === 'part') setPartDirectivesConnected(it.sub, connected);
|
|
432
|
+
}
|
|
433
|
+
} else if ('strings' in c) setInstanceDirectivesConnected(c, connected);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Release every directive record a bound part holds (its own and, for an
|
|
438
|
+
* interpolated attribute, each piece's), and recurse into a child part's
|
|
439
|
+
* rendered template so a nested directive is released with the region that
|
|
440
|
+
* is going away.
|
|
441
|
+
* @param {any} p
|
|
442
|
+
*/
|
|
443
|
+
function releasePartDirectives(p) {
|
|
444
|
+
// A part going away drops its signal subscriptions too (a bare signal or a
|
|
445
|
+
// `watch()` in it), so a later change cannot commit into detached nodes.
|
|
446
|
+
if (p.__sigSub) teardownSignalPart(p);
|
|
447
|
+
if (p.__watchSub) teardownWatch(p);
|
|
448
|
+
if (p.__directive) { disconnectDirectiveRecord(p.__directive); p.__directive = undefined; }
|
|
449
|
+
if (p.__pieceDirectives) {
|
|
450
|
+
for (const k of Object.keys(p.__pieceDirectives)) disconnectDirectiveRecord(p.__pieceDirectives[k]);
|
|
451
|
+
p.__pieceDirectives = undefined;
|
|
452
|
+
}
|
|
453
|
+
if (p.kind === 'child' && p.child) releaseRegionDirectives(p.child);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Release the directives inside a child part's rendered content, whatever
|
|
458
|
+
* shape it took (a template instance, a repeat map, a positional array).
|
|
459
|
+
* @param {any} c
|
|
460
|
+
*/
|
|
461
|
+
function releaseRegionDirectives(c) {
|
|
462
|
+
if (!c) return;
|
|
463
|
+
if (c.kind === 'repeat') { for (const inst of c.map.values()) releaseInstanceDirectives(inst); return; }
|
|
464
|
+
if (c.kind === 'array') {
|
|
465
|
+
for (const item of c.items) {
|
|
466
|
+
if (item.type === 'tpl') releaseInstanceDirectives(item.inst);
|
|
467
|
+
else if (item.type === 'part') releasePartDirectives(item.sub);
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if ('strings' in c) releaseInstanceDirectives(c);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Release every directive record in a template instance's parts. Exported for
|
|
476
|
+
* the top-level reconciler, which swaps a component's whole template.
|
|
477
|
+
* @param {any} inst
|
|
478
|
+
*/
|
|
479
|
+
export function releaseInstanceDirectives(inst) {
|
|
480
|
+
if (!inst || !inst.bound) return;
|
|
481
|
+
for (const p of inst.bound) releasePartDirectives(p);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Whether a value only WRAPS another one the part renders (so its directive record must survive). */
|
|
485
|
+
function isWrapperDirective(value) {
|
|
486
|
+
return isKeyed(value) || isGuard(value) || isCache(value) || isLive(value);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Settled values of the promises `until` has seen, so a re-render that
|
|
490
|
+
* passes an already-settled promise commits its value at once rather than
|
|
491
|
+
* flashing the placeholder (Lit's until keeps the same per-part memory). */
|
|
492
|
+
const settledUntil = new WeakMap();
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* `until(...args)` outside a child position: the value to commit now (the
|
|
496
|
+
* first non-promise argument, or the value of a higher-priority promise that
|
|
497
|
+
* has already settled), with each higher-priority promise still pending
|
|
498
|
+
* scheduled to commit through `onLater` when it settles, while `isCurrent()`.
|
|
499
|
+
* `has` is false when every argument is a pending promise, which commits
|
|
500
|
+
* nothing, as Lit's `noChange`.
|
|
501
|
+
* @param {unknown[]} args
|
|
502
|
+
* @param {() => boolean} isCurrent
|
|
503
|
+
* @param {(v: unknown) => void} onLater
|
|
504
|
+
* @returns {{ has: boolean, value?: unknown }}
|
|
505
|
+
*/
|
|
506
|
+
function untilValueNow(args, isCurrent, onLater) {
|
|
507
|
+
const isThenable = (/** @type any */ x) => !!x && typeof x.then === 'function';
|
|
508
|
+
const syncIdx = args.findIndex((x) => !isThenable(x));
|
|
509
|
+
let limit = syncIdx < 0 ? args.length : syncIdx;
|
|
510
|
+
/** @type {{ has: boolean, value?: unknown }} */
|
|
511
|
+
let now = syncIdx < 0 ? { has: false } : { has: true, value: args[syncIdx] };
|
|
512
|
+
for (let i = 0; i < limit; i++) {
|
|
513
|
+
if (settledUntil.has(/** @type object */ (args[i]))) {
|
|
514
|
+
now = { has: true, value: settledUntil.get(/** @type object */ (args[i])) };
|
|
515
|
+
limit = i;
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
let best = limit;
|
|
520
|
+
for (let i = 0; i < limit; i++) {
|
|
521
|
+
const p = /** @type object */ (args[i]);
|
|
522
|
+
Promise.resolve(p).then((v) => {
|
|
523
|
+
settledUntil.set(p, v);
|
|
524
|
+
if (!isCurrent() || i >= best) return;
|
|
525
|
+
best = i;
|
|
526
|
+
onLater(v);
|
|
527
|
+
}, () => {});
|
|
528
|
+
}
|
|
529
|
+
return now;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* @param {unknown} prev
|
|
534
|
+
* @param {unknown} deps
|
|
535
|
+
*/
|
|
536
|
+
function sameGuardDeps(prev, deps) {
|
|
537
|
+
if (prev === undefined) return false;
|
|
538
|
+
if (Array.isArray(prev) && Array.isArray(deps)) {
|
|
539
|
+
return prev.length === deps.length && deps.every((d, i) => Object.is(d, prev[i]));
|
|
540
|
+
}
|
|
541
|
+
return Object.is(prev, deps);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* One piece of an interpolated attribute through the same unwrap a whole
|
|
546
|
+
* attribute gets (#1686): `guard`, `keyed`, `cache`, and `until`, whose
|
|
547
|
+
* later promise rewrites just this piece. A piece with nothing to commit yet
|
|
548
|
+
* keeps the text it last committed (empty on a first render), as `noChange`.
|
|
549
|
+
* @param {any} part
|
|
550
|
+
* @param {number} j
|
|
551
|
+
* @param {unknown} piece
|
|
552
|
+
* @param {string[]} committed
|
|
553
|
+
* @param {{ statics: string[], group: number[] }} mp
|
|
554
|
+
*/
|
|
555
|
+
function mixedPieceValue(part, j, piece, committed, mp) {
|
|
556
|
+
const guards = (part.__pieceGuardDeps ??= []);
|
|
557
|
+
const untils = (part.__pieceUntil ??= []);
|
|
558
|
+
untils[j] = null;
|
|
559
|
+
if (isGuard(piece)) {
|
|
560
|
+
const deps = /** @type any */ (piece).deps;
|
|
561
|
+
if (sameGuardDeps(guards[j], deps)) return committed[j] ?? '';
|
|
562
|
+
guards[j] = deps;
|
|
563
|
+
piece = /** @type any */ (piece).fn();
|
|
564
|
+
} else {
|
|
565
|
+
guards[j] = undefined;
|
|
566
|
+
}
|
|
567
|
+
while (isKeyed(piece) || isCache(piece)) piece = /** @type any */ (piece).value;
|
|
568
|
+
if (!isUntil(piece)) return piece;
|
|
569
|
+
const token = {};
|
|
570
|
+
untils[j] = token;
|
|
571
|
+
const now = untilValueNow(/** @type any */ (piece).args, () => untils[j] === token, (v) => {
|
|
572
|
+
if (v === nothing) { part.el.removeAttribute(part.name); return; }
|
|
573
|
+
assertNotFunctionActionAttr(v, part.name, part.el.localName);
|
|
574
|
+
committed[j] = String(v ?? '');
|
|
575
|
+
let text = mp.statics[0];
|
|
576
|
+
for (let k = 0; k < mp.group.length; k++) text += (committed[k] ?? '') + (mp.statics[k + 1] || '');
|
|
577
|
+
part.el.setAttribute(part.name, text);
|
|
578
|
+
});
|
|
579
|
+
return now.has ? now.value : (committed[j] ?? '');
|
|
580
|
+
}
|
|
581
|
+
|
|
196
582
|
/**
|
|
197
583
|
* @param {BoundPart} part
|
|
198
584
|
* @param {unknown} value
|
|
199
585
|
* @param {unknown} _prev
|
|
200
586
|
*/
|
|
201
587
|
export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb) {
|
|
202
|
-
|
|
588
|
+
// An interpolated attribute is rebuilt from ALL its pieces, so a `noChange`
|
|
589
|
+
// in the anchor position must still reach the attr-mixed branch, which
|
|
590
|
+
// resolves each piece on its own. Every other part keeps its value.
|
|
591
|
+
if (value === noChange && part.kind !== 'attr-mixed') return;
|
|
592
|
+
// A custom directive on an element, attribute, property, boolean or event
|
|
593
|
+
// part resolves to the value it renders before anything else looks at the
|
|
594
|
+
// value. A child part resolves in applyChild (around its own directive
|
|
595
|
+
// state); an interpolated attribute resolves per piece below.
|
|
596
|
+
if (part.kind !== 'child' && part.kind !== 'attr-mixed' && part.kind !== 'slot' && part.kind !== 'noop'
|
|
597
|
+
&& (/** @type any */ (part).__directive || isDirectiveResult(value))) {
|
|
598
|
+
value = resolveDirectiveChain(part, part, '__directive', value, (v) => {
|
|
599
|
+
const p = /** @type any */ (part);
|
|
600
|
+
p.__settingValue = true;
|
|
601
|
+
try { applyPart(part, v, undefined, undefined, reconcileFormActionsCb); } finally { p.__settingValue = false; }
|
|
602
|
+
}, true);
|
|
603
|
+
if (value === noChange) return;
|
|
604
|
+
}
|
|
605
|
+
// A bare `${signal}` in an attribute or boolean hole: bind the part to the
|
|
606
|
+
// signal so a change rewrites just this attribute, with no host re-render
|
|
607
|
+
// (lit's signals package does the same). An interpolated attribute tracks
|
|
608
|
+
// every signal piece under one part-level watcher. A `.prop` hole is left
|
|
609
|
+
// alone, so the property receives the signal object itself.
|
|
610
|
+
const pAny = /** @type any */ (part);
|
|
611
|
+
if ((part.kind === 'attr' || part.kind === 'bool' || part.kind === 'attr-mixed') && !pAny.__applyingSignal) {
|
|
612
|
+
const holdsSignal = part.kind === 'attr-mixed'
|
|
613
|
+
? /** @type any */ (part).group.some((/** @type {number} */ i) => isSignalLike(allValues ? allValues[i] : value))
|
|
614
|
+
: isSignalLike(value);
|
|
615
|
+
if (holdsSignal) {
|
|
616
|
+
bindSignalPart(part, () => {
|
|
617
|
+
pAny.__applyingSignal = true;
|
|
618
|
+
try {
|
|
619
|
+
applyPart(part, part.kind === 'attr-mixed' ? value : readSignalLike(value), _prev, allValues, reconcileFormActionsCb);
|
|
620
|
+
} finally {
|
|
621
|
+
pAny.__applyingSignal = false;
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (pAny.__sigSub) teardownSignalPart(pAny);
|
|
627
|
+
}
|
|
203
628
|
if (isAttributeMap(value)) { applyAttributeMap(part, value); return; }
|
|
204
629
|
part.__mapKind = undefined;
|
|
630
|
+
// The value-carrying built-in directives outside a child position, as in
|
|
631
|
+
// Lit (#1686): `guard` commits its callback's result (re-run only when its
|
|
632
|
+
// deps change), `keyed` and `cache` their value, and `until` its first
|
|
633
|
+
// non-promise argument now and a higher-priority promise's value once it
|
|
634
|
+
// settles. They used to be written as "[object Object]". The pieces of an
|
|
635
|
+
// interpolated attribute take the same unwrap in the 'attr-mixed' case.
|
|
636
|
+
if (part.kind === 'attr' || part.kind === 'prop' || part.kind === 'bool' || part.kind === 'event') {
|
|
637
|
+
if (!pAny.__untilApplying) {
|
|
638
|
+
pAny.__attrUntil = null;
|
|
639
|
+
if (isGuard(value)) {
|
|
640
|
+
const deps = /** @type any */ (value).deps;
|
|
641
|
+
if (sameGuardDeps(pAny.__attrGuardDeps, deps)) return;
|
|
642
|
+
pAny.__attrGuardDeps = deps;
|
|
643
|
+
value = /** @type any */ (value).fn();
|
|
644
|
+
} else {
|
|
645
|
+
pAny.__attrGuardDeps = undefined;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
while (isKeyed(value) || isCache(value)) value = /** @type any */ (value).value;
|
|
649
|
+
if (isUntil(value)) {
|
|
650
|
+
const token = {};
|
|
651
|
+
pAny.__attrUntil = token;
|
|
652
|
+
const now = untilValueNow(/** @type any */ (value).args, () => pAny.__attrUntil === token, (v) => {
|
|
653
|
+
pAny.__untilApplying = true;
|
|
654
|
+
try { applyPart(part, v, undefined, allValues, reconcileFormActionsCb); } finally { pAny.__untilApplying = false; }
|
|
655
|
+
});
|
|
656
|
+
if (!now.has) return;
|
|
657
|
+
value = now.value;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
205
660
|
// Unwrap live() to dirty-check against the live DOM value, not the
|
|
206
661
|
// last rendered value. Essential for <input> two-way binding.
|
|
207
662
|
if (isLive(value)) {
|
|
208
663
|
const liveVal = /** @type any */ (value).value;
|
|
664
|
+
// live(noChange) keeps whatever the element holds now, as in Lit (#1686).
|
|
665
|
+
if (liveVal === noChange) return;
|
|
209
666
|
if (part.kind === 'prop' && /** @type any */ (part.el)[part.name] === liveVal) return;
|
|
210
667
|
if (part.kind === 'attr' && part.el.getAttribute(part.name) === String(liveVal)) return;
|
|
211
668
|
if (part.kind === 'bool' && part.el.hasAttribute(part.name) === !!liveVal) return;
|
|
@@ -290,8 +747,36 @@ export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb)
|
|
|
290
747
|
// Reconstruct the attribute from static pieces + all dynamic values.
|
|
291
748
|
const mp = /** @type {{ statics: string[], group: number[] }} */ (/** @type any */ (part));
|
|
292
749
|
let val = mp.statics[0];
|
|
750
|
+
// The text each piece last committed. A `noChange` piece reuses it (empty
|
|
751
|
+
// on the first render, which is what SSR serves), so it never stringifies
|
|
752
|
+
// the sentinel itself.
|
|
753
|
+
const committed = /** @type any */ (part).__pieces ??= [];
|
|
754
|
+
// A lone hole (`a="${x}"`) is a whole-value binding, as in lit, so a
|
|
755
|
+
// first-render `noChange` leaves the attribute absent rather than empty.
|
|
756
|
+
if (mp.group.length === 1 && !mp.statics[0] && !mp.statics[1] && !committed.length
|
|
757
|
+
&& (allValues ? allValues[mp.group[0]] : value) === noChange) break;
|
|
758
|
+
let hasLive = false;
|
|
293
759
|
for (let j = 0; j < mp.group.length; j++) {
|
|
294
760
|
let piece = allValues ? allValues[mp.group[j]] : value;
|
|
761
|
+
// A custom directive in this piece: one instance per piece, its
|
|
762
|
+
// `setValue()` rewriting just this piece of the attribute.
|
|
763
|
+
const pieceRecs = /** @type any */ (part).__pieceDirectives;
|
|
764
|
+
if (isDirectiveResult(piece) || (pieceRecs && pieceRecs[j])) {
|
|
765
|
+
const holder = (/** @type any */ (part).__pieceDirectives ??= {});
|
|
766
|
+
const pieceIndex = j;
|
|
767
|
+
piece = resolveDirectiveChain(part, holder, String(j), piece, (v) => {
|
|
768
|
+
if (v === noChange) return;
|
|
769
|
+
if (v === nothing) { part.el.removeAttribute(part.name); return; }
|
|
770
|
+
committed[pieceIndex] = String(v ?? '');
|
|
771
|
+
let text = mp.statics[0];
|
|
772
|
+
for (let k = 0; k < mp.group.length; k++) text += (committed[k] ?? '') + (mp.statics[k + 1] || '');
|
|
773
|
+
part.el.setAttribute(part.name, text);
|
|
774
|
+
}, true, j);
|
|
775
|
+
}
|
|
776
|
+
// A signal piece reads its current value; the part-level watcher
|
|
777
|
+
// applyPart installed records the read, so a change rebuilds this attribute.
|
|
778
|
+
if (isSignalLike(piece)) piece = readSignalLike(piece);
|
|
779
|
+
if (piece === noChange) piece = committed[j] ?? '';
|
|
295
780
|
// Unwrap live() PER PIECE (#1443). The top-of-function unwrap only
|
|
296
781
|
// sees the anchor hole's own value, and every hole inside a QUOTED
|
|
297
782
|
// attribute lands here (single-hole included, the compiler classifies
|
|
@@ -299,13 +784,22 @@ export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb)
|
|
|
299
784
|
// server now emits correctly would be rewritten to the wrapper's
|
|
300
785
|
// stringification on the next client commit. Mirrors the reconciler's
|
|
301
786
|
// effectiveFormAttr, which already unwraps each group piece.
|
|
302
|
-
if (isLive(piece))
|
|
787
|
+
if (isLive(piece)) {
|
|
788
|
+
piece = /** @type any */ (piece).value;
|
|
789
|
+
hasLive = true;
|
|
790
|
+
if (piece === noChange) piece = committed[j] ?? '';
|
|
791
|
+
}
|
|
792
|
+
piece = mixedPieceValue(part, j, piece, committed, mp);
|
|
303
793
|
// #1154: same function guard for each piece of a mixed attribute.
|
|
304
794
|
assertNotFunctionActionAttr(piece, part.name, part.el.localName);
|
|
305
795
|
if (piece === nothing) { part.el.removeAttribute(part.name); return; }
|
|
306
|
-
|
|
796
|
+
committed[j] = String(piece ?? '');
|
|
797
|
+
val += committed[j];
|
|
307
798
|
val += mp.statics[j + 1] || '';
|
|
308
799
|
}
|
|
800
|
+
// A live() piece checks the DOM, not the last render: an identical value
|
|
801
|
+
// is not written again (Lit's live, #1686).
|
|
802
|
+
if (hasLive && part.el.getAttribute(part.name) === val) break;
|
|
309
803
|
part.el.setAttribute(part.name, val);
|
|
310
804
|
break;
|
|
311
805
|
}
|
|
@@ -460,7 +954,7 @@ export function applyElement(part, value) {
|
|
|
460
954
|
|
|
461
955
|
if (refChanged && prevTarget) {
|
|
462
956
|
if (typeof prevTarget === 'function') {
|
|
463
|
-
try { prevTarget(undefined); } catch {}
|
|
957
|
+
try { prevTarget.call(partAny.refHost, undefined); } catch {}
|
|
464
958
|
} else if (typeof prevTarget === 'object') {
|
|
465
959
|
prevTarget.value = undefined;
|
|
466
960
|
}
|
|
@@ -473,9 +967,9 @@ export function applyElement(part, value) {
|
|
|
473
967
|
// Same callback now pointing at a different element: deliver
|
|
474
968
|
// an `undefined` cleanup for the prior element first.
|
|
475
969
|
if (!refChanged && partAny.__refElement !== undefined) {
|
|
476
|
-
try { nextTarget(undefined); } catch {}
|
|
970
|
+
try { nextTarget.call(partAny.refHost, undefined); } catch {}
|
|
477
971
|
}
|
|
478
|
-
try { nextTarget(part.el); } catch {}
|
|
972
|
+
try { nextTarget.call(partAny.refHost, part.el); } catch {}
|
|
479
973
|
} else if (typeof nextTarget === 'object') {
|
|
480
974
|
nextTarget.value = part.el;
|
|
481
975
|
}
|
|
@@ -495,6 +989,18 @@ export function applyChild(part, value, reconcileFormActionsCb) {
|
|
|
495
989
|
// directive handlers recurse via applyChildInner (no re-clear) so
|
|
496
990
|
// their own state survives the recursion.
|
|
497
991
|
if (value === noChange) return;
|
|
992
|
+
// A custom directive resolves first, so the per-part state of the built-in
|
|
993
|
+
// directive it may return (until, guard, cache, watch) is judged against
|
|
994
|
+
// what actually renders here. Its own record is released when a plain
|
|
995
|
+
// value takes the part over, but kept while a wrapper (keyed / guard /
|
|
996
|
+
// cache / live) passes an inner value on, since that inner value is
|
|
997
|
+
// resolved against the same record further down.
|
|
998
|
+
const partAny = /** @type any */ (part);
|
|
999
|
+
if (partAny.__directive || isDirectiveResult(value)) {
|
|
1000
|
+
value = resolveDirectiveChain(part, part, '__directive', value,
|
|
1001
|
+
(v) => applyChildInner(part, v, reconcileFormActionsCb), !isWrapperDirective(value));
|
|
1002
|
+
if (value === noChange) return;
|
|
1003
|
+
}
|
|
498
1004
|
clearStaleDirectiveState(part, value);
|
|
499
1005
|
return applyChildInner(part, value, reconcileFormActionsCb);
|
|
500
1006
|
}
|
|
@@ -518,11 +1024,13 @@ export function applyChildInner(part, value, reconcileFormActionsCb) {
|
|
|
518
1024
|
// marked as renderer writes rather than authored content.
|
|
519
1025
|
const previousHost = currentRenderHost;
|
|
520
1026
|
currentRenderHost = part.__eventHost;
|
|
1027
|
+
const previousLogical = logicalParent;
|
|
1028
|
+
logicalParent = logicalParentOf(part.marker && part.marker.parentNode);
|
|
521
1029
|
try {
|
|
522
1030
|
return commitInto(part.marker && part.marker.parentNode, () =>
|
|
523
1031
|
applyChildInnerRaw(part, value, reconcileFormActionsCb),
|
|
524
1032
|
);
|
|
525
|
-
} finally { currentRenderHost = previousHost; }
|
|
1033
|
+
} finally { currentRenderHost = previousHost; logicalParent = previousLogical; }
|
|
526
1034
|
}
|
|
527
1035
|
|
|
528
1036
|
/**
|
|
@@ -532,6 +1040,15 @@ export function applyChildInner(part, value, reconcileFormActionsCb) {
|
|
|
532
1040
|
export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
533
1041
|
const marker = part.marker;
|
|
534
1042
|
|
|
1043
|
+
// A custom directive a built-in directive passed on (`keyed(k, mine())`,
|
|
1044
|
+
// `guard(deps, () => mine())`): resolved against the part's record, which
|
|
1045
|
+
// applyChild kept for exactly this.
|
|
1046
|
+
if (isDirectiveResult(value)) {
|
|
1047
|
+
value = resolveDirectiveChain(part, part, '__directive', value,
|
|
1048
|
+
(v) => applyChildInner(part, v, reconcileFormActionsCb), false);
|
|
1049
|
+
if (value === noChange) return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
535
1052
|
// live() in a CHILD position (#1443). applyPart's top-of-function unwrap only
|
|
536
1053
|
// sees the hole's own value, so a live() nested inside an ARRAY child arrives
|
|
537
1054
|
// here still wrapped and would stringify to "[object Object]". The server
|
|
@@ -548,15 +1065,29 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
548
1065
|
|
|
549
1066
|
// unsafeHTML directive: inject raw HTML string as DOM nodes.
|
|
550
1067
|
if (isUnsafeHTML(value)) {
|
|
551
|
-
teardownChild(part);
|
|
552
1068
|
const htmlStr = String(/** @type any */ (value).value ?? '');
|
|
1069
|
+
const ns = /** @type any */ (value).ns;
|
|
1070
|
+
// Dirty-checked against the markup last committed, as in Lit (#1686): the
|
|
1071
|
+
// same string again keeps the nodes, and anything the page did to them,
|
|
1072
|
+
// rather than re-parsing (each render builds a new directive object).
|
|
1073
|
+
const last = /** @type any */ (part).__unsafeCommitted;
|
|
1074
|
+
if (last && last.str === htmlStr && last.ns === ns && part.child === last.nodes) return;
|
|
1075
|
+
teardownChild(part);
|
|
553
1076
|
const template = document.createElement('template');
|
|
554
|
-
|
|
1077
|
+
// unsafeSVG / unsafeMathML: parse inside the namespace's root element so
|
|
1078
|
+
// the nodes are SVG / MathML elements, then lift them out of the wrapper.
|
|
1079
|
+
const wrapTag = ns === 'svg' ? 'svg' : ns === 'mathml' ? 'math' : null;
|
|
1080
|
+
template.innerHTML = wrapTag ? `<${wrapTag}>${htmlStr}</${wrapTag}>` : htmlStr;
|
|
1081
|
+
if (wrapTag) {
|
|
1082
|
+
const wrapper = /** @type {Element} */ (template.content.firstChild);
|
|
1083
|
+
wrapper.replaceWith(...wrapper.childNodes);
|
|
1084
|
+
}
|
|
555
1085
|
const nodes = [...template.content.childNodes];
|
|
556
1086
|
const frag = document.createDocumentFragment();
|
|
557
1087
|
for (const n of nodes) frag.appendChild(n);
|
|
558
1088
|
marker.parentNode?.insertBefore(frag, marker);
|
|
559
1089
|
part.child = nodes;
|
|
1090
|
+
/** @type any */ (part).__unsafeCommitted = { str: htmlStr, ns, nodes };
|
|
560
1091
|
return;
|
|
561
1092
|
}
|
|
562
1093
|
|
|
@@ -611,7 +1142,7 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
611
1142
|
teardownChild(part);
|
|
612
1143
|
const tpl = /** @type any */ (value).template;
|
|
613
1144
|
if (tpl && tpl.content) {
|
|
614
|
-
const frag = tpl.content
|
|
1145
|
+
const frag = document.importNode(tpl.content, true);
|
|
615
1146
|
const nodes = [...frag.childNodes];
|
|
616
1147
|
marker.parentNode?.insertBefore(frag, marker);
|
|
617
1148
|
part.child = nodes;
|
|
@@ -654,6 +1185,11 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
654
1185
|
if (isWatch(value)) {
|
|
655
1186
|
return applyWatch(part, /** @type any */ (value).signal, reconcileFormActionsCb);
|
|
656
1187
|
}
|
|
1188
|
+
// A bare `${signal}` in a child position binds exactly like `watch(signal)`
|
|
1189
|
+
// (lit's signals package renders a bare signal the same way).
|
|
1190
|
+
if (isSignal(value)) {
|
|
1191
|
+
return applyWatch(part, /** @type any */ (value), reconcileFormActionsCb);
|
|
1192
|
+
}
|
|
657
1193
|
|
|
658
1194
|
// asyncAppend / asyncReplace: subscribe to the AsyncIterable. Each
|
|
659
1195
|
// yielded value is mapped (optional) and appended (asyncAppend) or
|
|
@@ -689,6 +1225,17 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
689
1225
|
// flipping one item's binding rebuilt the WHOLE list and detached every
|
|
690
1226
|
// node, which cancels a native drag mid-gesture. Use `repeat()` for keyed
|
|
691
1227
|
// reordering.
|
|
1228
|
+
//
|
|
1229
|
+
// Any other iterable (a generator from `map()` / `join()` / `range()`, a
|
|
1230
|
+
// Set) renders exactly like an array, as in lit, so it is drained into one
|
|
1231
|
+
// first. A string never reaches here as an iterable (it is checked by
|
|
1232
|
+
// `typeof`), and directive markers are plain objects with no iterator.
|
|
1233
|
+
// A DOM node is committed as itself (below), even one that is also iterable
|
|
1234
|
+
// (a `<form>`, a `<select>`), as in lit.
|
|
1235
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value) && !isDomNode(value)
|
|
1236
|
+
&& typeof (/** @type any */ (value))[Symbol.iterator] === 'function') {
|
|
1237
|
+
value = Array.from(/** @type {Iterable<unknown>} */ (value));
|
|
1238
|
+
}
|
|
692
1239
|
if (Array.isArray(value)) {
|
|
693
1240
|
if (part.child && /** @type any */ (part.child).kind === 'array') {
|
|
694
1241
|
reconcileArray(part, value, reconcileFormActionsCb);
|
|
@@ -701,6 +1248,10 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
701
1248
|
return;
|
|
702
1249
|
}
|
|
703
1250
|
|
|
1251
|
+
// The same node again keeps it where it is, as lit's dirty check does, so
|
|
1252
|
+
// its focus and state are not disturbed by a re-insert.
|
|
1253
|
+
if (isDomNode(value) && Array.isArray(part.child) && part.child.length === 1 && part.child[0] === value) return;
|
|
1254
|
+
|
|
704
1255
|
// Remove previously rendered nodes between marker and its next sibling we own.
|
|
705
1256
|
if (part.child) {
|
|
706
1257
|
const c = /** @type any */ (part.child);
|
|
@@ -720,6 +1271,10 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
720
1271
|
updateInstance(inst, /** @type any */ (value).values, reconcileFormActionsCb);
|
|
721
1272
|
return;
|
|
722
1273
|
}
|
|
1274
|
+
// A full teardown, as `clear()` does: a ref on an element of the
|
|
1275
|
+
// outgoing template is unset (its callback gets `undefined`), the way
|
|
1276
|
+
// Lit's ref directive is on disconnection, before the new one binds.
|
|
1277
|
+
disposeInstance(inst);
|
|
723
1278
|
removeBetween(inst.startNode, inst.endNode);
|
|
724
1279
|
part.child = undefined;
|
|
725
1280
|
} else {
|
|
@@ -731,12 +1286,13 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
731
1286
|
}
|
|
732
1287
|
}
|
|
733
1288
|
|
|
734
|
-
|
|
1289
|
+
// An empty string renders no node at all, as lit treats it like `nothing`.
|
|
1290
|
+
if (value == null || value === nothing || value === noChange || value === '') return;
|
|
735
1291
|
|
|
736
1292
|
if (isTemplate(value)) {
|
|
737
1293
|
const tr = /** @type any */ (value);
|
|
738
1294
|
const { templateEl, parts, formActions } = compile(tr);
|
|
739
|
-
const frag = /** @type DocumentFragment */ (templateEl.content
|
|
1295
|
+
const frag = /** @type DocumentFragment */ (document.importNode(templateEl.content, true));
|
|
740
1296
|
const startNode = document.createComment(`${MARKER}s`);
|
|
741
1297
|
const endNode = document.createComment(`${MARKER}e`);
|
|
742
1298
|
const bound = parts.map((p) => bindPart(p, frag));
|
|
@@ -760,17 +1316,44 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
760
1316
|
return;
|
|
761
1317
|
}
|
|
762
1318
|
|
|
763
|
-
|
|
1319
|
+
// A DOM node renders as itself; anything else as text.
|
|
1320
|
+
const node = isDomNode(value) ? /** @type {ChildNode} */ (value) : document.createTextNode(String(value));
|
|
764
1321
|
marker.parentNode?.insertBefore(node, marker);
|
|
765
1322
|
part.child = [node];
|
|
766
1323
|
}
|
|
767
1324
|
|
|
1325
|
+
/**
|
|
1326
|
+
* A DOM node a child binding renders as itself (lit's `_commitNode`). Checked
|
|
1327
|
+
* by shape rather than `instanceof Node`, so a node from another realm (an
|
|
1328
|
+
* iframe's document) is recognised too.
|
|
1329
|
+
* @param {unknown} v
|
|
1330
|
+
* @returns {boolean}
|
|
1331
|
+
*/
|
|
1332
|
+
export function isDomNode(v) {
|
|
1333
|
+
return v !== null && typeof v === 'object' && typeof (/** @type any */ (v)).nodeType === 'number'
|
|
1334
|
+
&& typeof (/** @type any */ (v)).cloneNode === 'function';
|
|
1335
|
+
}
|
|
1336
|
+
|
|
768
1337
|
export function updateInstance(inst, values, reconcileFormActionsCb) {
|
|
769
|
-
|
|
1338
|
+
// `noChange` keeps whatever this hole last committed, so it maps to the
|
|
1339
|
+
// previous value and the `Object.is` skip below leaves the DOM alone. It must
|
|
1340
|
+
// not fall back to `nothing` when that value was nullish: a hole that rendered
|
|
1341
|
+
// `title=""` from `undefined` keeps its attribute, as in Lit.
|
|
1342
|
+
values = values.map((value, i) => value === noChange ? inst.lastValues[i] : value);
|
|
1343
|
+
/** Interpolated attributes already rebuilt this pass, so each is built once. */
|
|
1344
|
+
/** @type {Set<number> | null} */
|
|
1345
|
+
let rebuilt = null;
|
|
770
1346
|
for (let i = 0; i < values.length; i++) {
|
|
771
1347
|
const next = values[i];
|
|
772
|
-
|
|
1348
|
+
// A custom directive runs on every render even when handed the same
|
|
1349
|
+
// result object, as in lit, which resolves a directive before it compares.
|
|
1350
|
+
if (Object.is(next, inst.lastValues[i]) && !isDirectiveResult(next)) continue;
|
|
773
1351
|
const bp = inst.bound[i];
|
|
1352
|
+
const anchorIdx = bp.kind === 'attr-mixed' ? i : /** @type any */ (bp).mixedAnchor;
|
|
1353
|
+
if (anchorIdx != null) {
|
|
1354
|
+
if (rebuilt?.has(anchorIdx)) { inst.lastValues[i] = next; continue; }
|
|
1355
|
+
(rebuilt ??= new Set()).add(anchorIdx);
|
|
1356
|
+
}
|
|
774
1357
|
// A hole that belongs to a mixed attribute (`class="a ${x} b ${y}"`) is a
|
|
775
1358
|
// `noop` pointing at the attribute's anchor part; re-apply the anchor so the
|
|
776
1359
|
// whole attribute is rebuilt from every hole's current value, not just the
|
|
@@ -876,7 +1459,7 @@ export function nodesToFrag(nodes) {
|
|
|
876
1459
|
*/
|
|
877
1460
|
export function buildDetached(tr, reconcileFormActionsCb) {
|
|
878
1461
|
const { templateEl, parts, formActions } = compile(tr);
|
|
879
|
-
const frag = /** @type DocumentFragment */ (templateEl.content
|
|
1462
|
+
const frag = /** @type DocumentFragment */ (document.importNode(templateEl.content, true));
|
|
880
1463
|
const startNode = document.createComment(`${MARKER}s`);
|
|
881
1464
|
const endNode = document.createComment(`${MARKER}e`);
|
|
882
1465
|
const bound = parts.map((p) => bindPart(p, frag));
|
|
@@ -908,6 +1491,9 @@ export function buildDetached(tr, reconcileFormActionsCb) {
|
|
|
908
1491
|
|
|
909
1492
|
/** @param {TemplateInstance} inst */
|
|
910
1493
|
export function disposeInstance(inst) {
|
|
1494
|
+
// Custom directives in the instance (and in the regions its child parts
|
|
1495
|
+
// rendered) are released first; an async one runs its `disconnected()`.
|
|
1496
|
+
releaseInstanceDirectives(inst);
|
|
911
1497
|
for (const p of inst.bound) {
|
|
912
1498
|
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher, p.handler);
|
|
913
1499
|
if (p.kind === 'element') {
|
|
@@ -938,7 +1524,7 @@ export function disposeInstance(inst) {
|
|
|
938
1524
|
const prev = /** @type any */ (p).lastTarget;
|
|
939
1525
|
if (prev) {
|
|
940
1526
|
if (typeof prev === 'function') {
|
|
941
|
-
try { prev(undefined); } catch {}
|
|
1527
|
+
try { prev.call(/** @type any */ (p).refHost, undefined); } catch {}
|
|
942
1528
|
} else if (typeof prev === 'object') {
|
|
943
1529
|
try { prev.value = undefined; } catch {}
|
|
944
1530
|
}
|
|
@@ -962,8 +1548,9 @@ function applyRepeatFresh(marker, state, value, reconcileFormActionsCb) {
|
|
|
962
1548
|
const parent = marker.parentNode;
|
|
963
1549
|
if (!parent) return;
|
|
964
1550
|
const bulk = document.createDocumentFragment();
|
|
1551
|
+
const seen = new Map();
|
|
965
1552
|
for (let i = 0; i < items.length; i++) {
|
|
966
|
-
const key = keyFn(items[i], i);
|
|
1553
|
+
const key = slotKey(state, keyFn(items[i], i), seen);
|
|
967
1554
|
const tr = templateFn(items[i], i);
|
|
968
1555
|
if (!isTemplate(tr)) continue;
|
|
969
1556
|
const { inst, frag } = buildDetached(/** @type any */ (tr), reconcileFormActionsCb);
|
|
@@ -973,6 +1560,28 @@ function applyRepeatFresh(marker, state, value, reconcileFormActionsCb) {
|
|
|
973
1560
|
parent.insertBefore(bulk, marker);
|
|
974
1561
|
}
|
|
975
1562
|
|
|
1563
|
+
/**
|
|
1564
|
+
* The map key for one occurrence of `key` in a pass (#1686). The first
|
|
1565
|
+
* occurrence is the key itself; the nth repeat of it is a placeholder object
|
|
1566
|
+
* kept on the state, the same one every pass, so a list with duplicate keys
|
|
1567
|
+
* gives each row its own entry. A plain key map let the second row overwrite
|
|
1568
|
+
* the first, whose nodes then stayed in the document tracked by nothing, and
|
|
1569
|
+
* each re-render added another copy. Lit leaves duplicate keys undefined but
|
|
1570
|
+
* never leaks a row.
|
|
1571
|
+
* @param {any} state
|
|
1572
|
+
* @param {unknown} key
|
|
1573
|
+
* @param {Map<unknown, number>} seen occurrences so far in this pass
|
|
1574
|
+
*/
|
|
1575
|
+
function slotKey(state, key, seen) {
|
|
1576
|
+
const n = seen.get(key) ?? 0;
|
|
1577
|
+
seen.set(key, n + 1);
|
|
1578
|
+
if (n === 0) return key;
|
|
1579
|
+
const dups = (state.dups ??= new Map());
|
|
1580
|
+
let list = dups.get(key);
|
|
1581
|
+
if (!list) dups.set(key, (list = []));
|
|
1582
|
+
return (list[n - 1] ??= { duplicateOf: key, n });
|
|
1583
|
+
}
|
|
1584
|
+
|
|
976
1585
|
/**
|
|
977
1586
|
* Keyed reconciliation. For each key in the new list:
|
|
978
1587
|
* - hit: update the existing instance in place (if template shape matches),
|
|
@@ -990,11 +1599,12 @@ function reconcileRepeat(part, value, reconcileFormActionsCb) {
|
|
|
990
1599
|
const state = /** @type {{ kind: 'repeat', map: Map<any, TemplateInstance> }} */ (part.child);
|
|
991
1600
|
const { items, keyFn, templateFn } = value;
|
|
992
1601
|
const newMap = new Map();
|
|
1602
|
+
const seen = new Map();
|
|
993
1603
|
|
|
994
1604
|
try {
|
|
995
1605
|
// Walk the new list and position each item's nodes immediately before the marker.
|
|
996
1606
|
for (let i = 0; i < items.length; i++) {
|
|
997
|
-
const key = keyFn(items[i], i);
|
|
1607
|
+
const key = slotKey(state, keyFn(items[i], i), seen);
|
|
998
1608
|
const tr = templateFn(items[i], i);
|
|
999
1609
|
if (!isTemplate(tr)) continue;
|
|
1000
1610
|
const existing = state.map.get(key);
|
|
@@ -1128,22 +1738,29 @@ function teardownRepeat(state) {
|
|
|
1128
1738
|
* instance (bookended by its own markers), a `text` carries a single text
|
|
1129
1739
|
* node, and an `empty` slot (a nullish / boolean element) renders nothing
|
|
1130
1740
|
* but still holds the position so index-based reconciliation stays aligned.
|
|
1131
|
-
*
|
|
1741
|
+
* A `node` is a DOM node the array held, rendered as itself.
|
|
1742
|
+
* @typedef {{ type: 'tpl', inst: TemplateInstance } | { type: 'text', node: Text } | { type: 'node', node: ChildNode } | { type: 'empty' } | { type: 'part', start: Comment, sub: any }} ArrayItem
|
|
1132
1743
|
*/
|
|
1133
1744
|
|
|
1134
1745
|
/** @param {ArrayItem} item @returns {ChildNode | null} */
|
|
1135
1746
|
function arrayItemFirstNode(item) {
|
|
1747
|
+
if (item.type === 'part') return item.start;
|
|
1136
1748
|
if (item.type === 'tpl') return item.inst.startNode;
|
|
1137
|
-
if (item.type === 'text') return item.node;
|
|
1749
|
+
if (item.type === 'text' || item.type === 'node') return item.node;
|
|
1138
1750
|
return null;
|
|
1139
1751
|
}
|
|
1140
1752
|
|
|
1141
1753
|
/** @param {ArrayItem} item */
|
|
1142
1754
|
function removeArrayItem(item) {
|
|
1143
|
-
if (item.type === '
|
|
1755
|
+
if (item.type === 'part') {
|
|
1756
|
+
// Its directive and any signal subscription go with it, as a hole's do.
|
|
1757
|
+
releasePartDirectives(item.sub);
|
|
1758
|
+
teardownChild(item.sub);
|
|
1759
|
+
removeBetween(item.start, item.sub.marker);
|
|
1760
|
+
} else if (item.type === 'tpl') {
|
|
1144
1761
|
disposeInstance(item.inst);
|
|
1145
1762
|
removeBetween(item.inst.startNode, item.inst.endNode);
|
|
1146
|
-
} else if (item.type === 'text') {
|
|
1763
|
+
} else if (item.type === 'text' || item.type === 'node') {
|
|
1147
1764
|
item.node.parentNode?.removeChild(item.node);
|
|
1148
1765
|
}
|
|
1149
1766
|
}
|
|
@@ -1159,11 +1776,24 @@ function buildArrayItem(v, reconcileFormActionsCb) {
|
|
|
1159
1776
|
// #1443: unwrap live() per ITEM. applyPart unwraps the hole's own value, so
|
|
1160
1777
|
// an array child arrives unwrapped at the top and its ITEMS do not.
|
|
1161
1778
|
if (isLive(v)) v = /** @type any */ (v).value;
|
|
1779
|
+
if (isPartItem(v)) {
|
|
1780
|
+
// A directive or a nested array gets a child part of its own between two
|
|
1781
|
+
// comments, as each item is in Lit, so it renders and keeps its directive
|
|
1782
|
+
// state exactly as it would in a hole. It is filled once its comments are
|
|
1783
|
+
// in the document (`fill`), because a part commits relative to its parent.
|
|
1784
|
+
const start = document.createComment('');
|
|
1785
|
+
const marker = document.createComment('');
|
|
1786
|
+
const frag = document.createDocumentFragment();
|
|
1787
|
+
frag.append(start, marker);
|
|
1788
|
+
const sub = { kind: 'child', marker, __eventHost: currentRenderHost };
|
|
1789
|
+
return { item: { type: 'part', start, sub }, frag, fill: () => applyChild(/** @type any */ (sub), v, reconcileFormActionsCb) };
|
|
1790
|
+
}
|
|
1162
1791
|
if (isTemplate(v)) {
|
|
1163
1792
|
const { inst, frag } = buildDetached(/** @type any */ (v), reconcileFormActionsCb);
|
|
1164
1793
|
return { item: { type: 'tpl', inst }, frag };
|
|
1165
1794
|
}
|
|
1166
|
-
if (v
|
|
1795
|
+
if (isDomNode(v)) return { item: { type: 'node', node: /** @type any */ (v) }, frag: /** @type any */ (v) };
|
|
1796
|
+
if (v != null && v !== nothing && v !== noChange && v !== '') {
|
|
1167
1797
|
const node = document.createTextNode(String(v));
|
|
1168
1798
|
return { item: { type: 'text', node }, frag: node };
|
|
1169
1799
|
}
|
|
@@ -1182,13 +1812,27 @@ function applyArrayFresh(marker, state, value, reconcileFormActionsCb) {
|
|
|
1182
1812
|
const bulk = document.createDocumentFragment();
|
|
1183
1813
|
/** @type {ArrayItem[]} */
|
|
1184
1814
|
const items = [];
|
|
1815
|
+
/** @type {Array<() => void>} */
|
|
1816
|
+
const fills = [];
|
|
1185
1817
|
for (const v of value) {
|
|
1186
|
-
const { item, frag } = buildArrayItem(v, reconcileFormActionsCb);
|
|
1818
|
+
const { item, frag, fill } = buildArrayItem(v, reconcileFormActionsCb);
|
|
1187
1819
|
if (frag) bulk.appendChild(frag);
|
|
1820
|
+
if (fill) fills.push(fill);
|
|
1188
1821
|
items.push(item);
|
|
1189
1822
|
}
|
|
1190
1823
|
parent.insertBefore(bulk, marker);
|
|
1191
1824
|
state.items = items;
|
|
1825
|
+
for (const fill of fills) fill();
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Whether an array item renders through a child part of its own: a directive
|
|
1830
|
+
* result or a nested array / iterable, anything that is not a template, a node
|
|
1831
|
+
* or a primitive (which the item types below handle without one).
|
|
1832
|
+
* @param {unknown} v
|
|
1833
|
+
*/
|
|
1834
|
+
function isPartItem(v) {
|
|
1835
|
+
return v !== null && typeof v === 'object' && !isTemplate(v) && !isDomNode(v);
|
|
1192
1836
|
}
|
|
1193
1837
|
|
|
1194
1838
|
/**
|
|
@@ -1222,7 +1866,14 @@ function reconcileArray(part, value, reconcileFormActionsCb) {
|
|
|
1222
1866
|
const raw = value[i];
|
|
1223
1867
|
const v = isLive(raw) ? /** @type any */ (raw).value : raw;
|
|
1224
1868
|
const o = old[i];
|
|
1225
|
-
if (
|
|
1869
|
+
if (isPartItem(v)) {
|
|
1870
|
+
if (o && o.type === 'part') {
|
|
1871
|
+
applyChild(o.sub, v, reconcileFormActionsCb);
|
|
1872
|
+
next.push(o);
|
|
1873
|
+
consumed = i + 1;
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
} else if (isTemplate(v)) {
|
|
1226
1877
|
const tr = /** @type any */ (v);
|
|
1227
1878
|
if (o && o.type === 'tpl' && o.inst.strings === tr.strings) {
|
|
1228
1879
|
updateInstance(o.inst, tr.values, reconcileFormActionsCb);
|
|
@@ -1230,7 +1881,13 @@ function reconcileArray(part, value, reconcileFormActionsCb) {
|
|
|
1230
1881
|
consumed = i + 1;
|
|
1231
1882
|
continue;
|
|
1232
1883
|
}
|
|
1233
|
-
} else if (v
|
|
1884
|
+
} else if (isDomNode(v)) {
|
|
1885
|
+
if (o && o.type === 'node' && o.node === v) {
|
|
1886
|
+
next.push(o);
|
|
1887
|
+
consumed = i + 1;
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
} else if (v != null && v !== nothing && v !== noChange && v !== '') {
|
|
1234
1891
|
if (o && o.type === 'text') {
|
|
1235
1892
|
const str = String(v);
|
|
1236
1893
|
if (o.node.data !== str) o.node.data = str;
|
|
@@ -1259,9 +1916,10 @@ function reconcileArray(part, value, reconcileFormActionsCb) {
|
|
|
1259
1916
|
// The push sits BEFORE the removal, which is a pure reordering on the
|
|
1260
1917
|
// success path and means a slot that has already been built and
|
|
1261
1918
|
// inserted is never untracked at any throw point.
|
|
1262
|
-
const { item, frag } = buildArrayItem(v, reconcileFormActionsCb);
|
|
1919
|
+
const { item, frag, fill } = buildArrayItem(v, reconcileFormActionsCb);
|
|
1263
1920
|
if (frag) parent.insertBefore(frag, nextArrayAnchor(old, i, marker));
|
|
1264
1921
|
next.push(item);
|
|
1922
|
+
if (fill) fill();
|
|
1265
1923
|
if (o) removeArrayItem(o);
|
|
1266
1924
|
consumed = i + 1;
|
|
1267
1925
|
}
|
|
@@ -1436,7 +2094,7 @@ export function clearStaleDirectiveState(part, value) {
|
|
|
1436
2094
|
if (partAny.__keyedKey !== undefined && !isKeyed(value)) {
|
|
1437
2095
|
partAny.__keyedKey = undefined;
|
|
1438
2096
|
}
|
|
1439
|
-
if (partAny.__watchSub && !isWatch(value)) {
|
|
2097
|
+
if (partAny.__watchSub && !isWatch(value) && !isSignal(value)) {
|
|
1440
2098
|
teardownWatch(partAny);
|
|
1441
2099
|
}
|
|
1442
2100
|
}
|
|
@@ -1785,6 +2443,62 @@ function applyWatch(part, sig, reconcileFormActionsCb) {
|
|
|
1785
2443
|
applyChildInner(part, initial, reconcileFormActionsCb);
|
|
1786
2444
|
}
|
|
1787
2445
|
|
|
2446
|
+
/**
|
|
2447
|
+
* Bind an attribute-shaped part (attribute, boolean, interpolated attribute)
|
|
2448
|
+
* to the signals it renders. `run` applies the part's value, reading each
|
|
2449
|
+
* signal inside the part's own `Signal.subtle.Watcher`, so a change re-runs
|
|
2450
|
+
* `run` in a microtask and rewrites only this attribute; the host's render
|
|
2451
|
+
* watcher does not see the read (the commit runs untracked), which is what
|
|
2452
|
+
* keeps a signal change from re-rendering the whole component.
|
|
2453
|
+
*
|
|
2454
|
+
* The watcher is rebuilt on every render of the part, so the read set always
|
|
2455
|
+
* matches the values the latest render bound.
|
|
2456
|
+
*
|
|
2457
|
+
* @param {any} part
|
|
2458
|
+
* @param {() => void} run
|
|
2459
|
+
*/
|
|
2460
|
+
function bindSignalPart(part, run) {
|
|
2461
|
+
teardownSignalPart(part);
|
|
2462
|
+
if (currentRenderRoot) part.__commitOwner = boundaryOwnerOf(currentRenderRoot);
|
|
2463
|
+
const watcher = new Signal.subtle.Watcher(() => {
|
|
2464
|
+
queueMicrotask(() => {
|
|
2465
|
+
if (part.__sigSub !== watcher) return;
|
|
2466
|
+
try {
|
|
2467
|
+
commitOutOfBand(part, () => watcher.observe(run));
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
reportOutOfBandCommitError(part, err);
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2472
|
+
});
|
|
2473
|
+
part.__sigSub = watcher;
|
|
2474
|
+
watcher.observe(run);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* Whether an attribute-shaped hole is bound to a signal: a bare signal, or a
|
|
2479
|
+
* `watch(signal)` marker (which means the same thing outside a child hole).
|
|
2480
|
+
* @param {unknown} v
|
|
2481
|
+
*/
|
|
2482
|
+
function isSignalLike(v) {
|
|
2483
|
+
return isSignal(v) || isWatch(v);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/** @param {any} v */
|
|
2487
|
+
function readSignalLike(v) {
|
|
2488
|
+
return isWatch(v) ? v.signal.get() : v.get();
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
/**
|
|
2492
|
+
* Dispose an attribute-shaped part's signal watcher.
|
|
2493
|
+
* @param {any} part
|
|
2494
|
+
*/
|
|
2495
|
+
function teardownSignalPart(part) {
|
|
2496
|
+
if (part.__sigSub) {
|
|
2497
|
+
part.__sigSub.dispose();
|
|
2498
|
+
part.__sigSub = undefined;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
|
|
1788
2502
|
/**
|
|
1789
2503
|
* Dispose a `watch` directive's per-part watcher. Called from
|
|
1790
2504
|
* `teardownChild` and from `clearStaleDirectiveState` when the value
|
|
@@ -2029,7 +2743,7 @@ function renderToNodes(value, reconcileFormActionsCb) {
|
|
|
2029
2743
|
if (isTemplate(value)) {
|
|
2030
2744
|
const tr = /** @type any */ (value);
|
|
2031
2745
|
const { templateEl, parts, formActions } = compile(tr);
|
|
2032
|
-
const frag = /** @type DocumentFragment */ (templateEl.content
|
|
2746
|
+
const frag = /** @type DocumentFragment */ (document.importNode(templateEl.content, true));
|
|
2033
2747
|
const bound = parts.map((p) => bindPart(p, frag));
|
|
2034
2748
|
for (let i = 0; i < tr.values.length; i++) {
|
|
2035
2749
|
applyPart(bound[i], tr.values[i], undefined, tr.values, reconcileFormActionsCb);
|