@webjsdev/core 0.7.64 → 0.8.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/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +21 -22
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +6 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +6 -2
- package/index.d.ts +7 -3
- package/index.js +6 -2
- package/package.json +1 -1
- package/src/attribute-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +37 -3
- package/src/binding-prefixes.js +10 -41
- package/src/component/lifecycle.js +261 -101
- package/src/component/reactive.js +5 -4
- package/src/component.d.ts +11 -5
- package/src/directives.d.ts +2 -0
- package/src/directives.js +2 -0
- package/src/escape.js +12 -2
- package/src/form-action.js +7 -7
- package/src/html.d.ts +4 -0
- package/src/html.js +22 -1
- package/src/optimistic.js +3 -3
- package/src/registry.d.ts +5 -0
- package/src/registry.js +35 -4
- package/src/render-client/adopt-slots.js +245 -0
- package/src/render-client/parts.js +53 -19
- package/src/render-client/reconciler.js +28 -12
- package/src/render-client/template-compiler.js +19 -5
- package/src/render-client.d.ts +2 -1
- package/src/render-server/attrs.js +5 -4
- package/src/render-server/compile.js +433 -0
- package/src/render-server/dsd.js +11 -435
- package/src/render-server/errors.js +84 -0
- package/src/render-server/expand.js +346 -0
- package/src/render-server/html-scan.js +19 -358
- package/src/render-server/served-attrs.js +79 -0
- package/src/render-server/stream.js +11 -13
- package/src/render-server/template-renderer.js +13 -936
- package/src/render-server/walk.js +798 -0
- package/src/router-client/anchors.js +3 -1
- package/src/router-client/boundaries.js +15 -1
- package/src/router-client/constants.js +30 -0
- package/src/router-client/fetch-apply.js +21 -0
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +4 -0
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +3 -0
- package/src/router-client/snapshot-cache.js +2 -12
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +49 -9
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- package/src/slot.js +1 -1
- package/src/webjs-config.d.ts +1 -1
- package/src/render-server/slots.js +0 -213
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isAttributeMap, applyAttributeMap } from '../attribute-directives.js';
|
|
2
|
+
import { nothing, noChange } from '../sentinels.js';
|
|
1
3
|
import { isTemplate, MARKER } from '../html.js';
|
|
2
4
|
import {
|
|
3
5
|
assertNotFunctionActionAttr, assertNotFunctionReflectedActionProp, isBoundFormAction,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
SLOT_FALLBACK_FRAG, SLOT_STATE, SLOT_OWNER, applySlotAssignments, withRendererWrites,
|
|
12
14
|
} from '../slot.js';
|
|
13
15
|
import { compile, templateCache } from './template-compiler.js';
|
|
16
|
+
import { adoptedSlots } from './adopt-slots.js';
|
|
14
17
|
import { attrHoleValue } from '../binding-prefixes.js';
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -23,6 +26,8 @@ import { attrHoleValue } from '../binding-prefixes.js';
|
|
|
23
26
|
* first. Same reason `SLOT_OWNER` exists.
|
|
24
27
|
* @type {any}
|
|
25
28
|
*/
|
|
29
|
+
export let currentRenderHost;
|
|
30
|
+
export function setCurrentRenderHost(host) { currentRenderHost = host; }
|
|
26
31
|
export let currentRenderRoot = null;
|
|
27
32
|
|
|
28
33
|
export function setCurrentRenderRoot(root) {
|
|
@@ -132,6 +137,8 @@ export function boundaryOwnerOf(root) {
|
|
|
132
137
|
export function commitOutOfBand(part, fn) {
|
|
133
138
|
const owner = /** @type any */ (part).__commitOwner;
|
|
134
139
|
const prev = currentRenderRoot;
|
|
140
|
+
const prevHost = currentRenderHost;
|
|
141
|
+
currentRenderHost = part.__eventHost;
|
|
135
142
|
if (owner) currentRenderRoot = owner;
|
|
136
143
|
try {
|
|
137
144
|
fn();
|
|
@@ -150,7 +157,7 @@ export function bindPart(p, root) {
|
|
|
150
157
|
let node = /** @type Node */ (root);
|
|
151
158
|
for (const i of p.path) node = node.childNodes[i];
|
|
152
159
|
if (p.kind === 'child') {
|
|
153
|
-
return { kind: 'child', marker: /** @type Comment */ (node) };
|
|
160
|
+
return { kind: 'child', marker: /** @type Comment */ (node), __eventHost: currentRenderHost };
|
|
154
161
|
}
|
|
155
162
|
const el = /** @type Element */ (node);
|
|
156
163
|
if (p.kind === 'event') {
|
|
@@ -160,10 +167,13 @@ export function bindPart(p, root) {
|
|
|
160
167
|
el,
|
|
161
168
|
name: p.name || '',
|
|
162
169
|
handler: null,
|
|
170
|
+
eventHost: currentRenderHost,
|
|
163
171
|
// The dispatcher is the registered listener; handler swaps behind it.
|
|
164
|
-
dispatcher(ev) {
|
|
172
|
+
dispatcher(ev) {
|
|
173
|
+
if (typeof part.handler === 'function') part.handler.call(part.eventHost ?? part.el, ev);
|
|
174
|
+
else part.handler?.handleEvent(ev);
|
|
175
|
+
},
|
|
165
176
|
};
|
|
166
|
-
el.addEventListener(part.name, part.dispatcher);
|
|
167
177
|
return part;
|
|
168
178
|
}
|
|
169
179
|
if (p.kind === 'attr') return { kind: 'attr', el, name: p.name || '' };
|
|
@@ -189,6 +199,9 @@ export function bindPart(p, root) {
|
|
|
189
199
|
* @param {unknown} _prev
|
|
190
200
|
*/
|
|
191
201
|
export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb) {
|
|
202
|
+
if (value === noChange) return;
|
|
203
|
+
if (isAttributeMap(value)) { applyAttributeMap(part, value); return; }
|
|
204
|
+
part.__mapKind = undefined;
|
|
192
205
|
// Unwrap live() to dirty-check against the live DOM value, not the
|
|
193
206
|
// last rendered value. Essential for <input> two-way binding.
|
|
194
207
|
if (isLive(value)) {
|
|
@@ -247,21 +260,28 @@ export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb)
|
|
|
247
260
|
// first and is left alone, so it still writes whatever the author
|
|
248
261
|
// returns for a function.
|
|
249
262
|
assertNotFunctionReflectedActionProp(value, part.name, part.el.localName);
|
|
250
|
-
/** @type any */ (part.el)[part.name] = value;
|
|
263
|
+
/** @type any */ (part.el)[part.name] = value === nothing ? undefined : value;
|
|
251
264
|
break;
|
|
252
265
|
case 'bool':
|
|
253
266
|
// #1154: never leaked (a boolean binding stringifies nothing), but
|
|
254
267
|
// `?action=${fn}` is meaningless in every case, and refusing it keeps
|
|
255
268
|
// this renderer agreeing with both SSR machines.
|
|
256
269
|
assertNotFunctionActionAttr(value, part.name, part.el.localName);
|
|
257
|
-
if (value) part.el.setAttribute(part.name, '');
|
|
270
|
+
if (value && value !== nothing) part.el.setAttribute(part.name, '');
|
|
258
271
|
else part.el.removeAttribute(part.name);
|
|
259
272
|
break;
|
|
260
273
|
case 'event':
|
|
261
274
|
// NOT guarded, deliberately. An event binding never stringifies its
|
|
262
275
|
// value, and a function is the legitimate thing to pass one, so
|
|
263
276
|
// `<my-el @action=${handler}>` has to keep working.
|
|
264
|
-
|
|
277
|
+
{
|
|
278
|
+
const old = part.handler;
|
|
279
|
+
const next = value == null || value === nothing ? null : value;
|
|
280
|
+
const changed = old?.capture !== next?.capture || old?.once !== next?.once || old?.passive !== next?.passive;
|
|
281
|
+
if (old && (!next || changed)) part.el.removeEventListener(part.name, part.dispatcher, old);
|
|
282
|
+
if (next && (!old || changed)) part.el.addEventListener(part.name, part.dispatcher, next);
|
|
283
|
+
part.handler = next;
|
|
284
|
+
}
|
|
265
285
|
break;
|
|
266
286
|
case 'element':
|
|
267
287
|
applyElement(part, value);
|
|
@@ -282,6 +302,7 @@ export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb)
|
|
|
282
302
|
if (isLive(piece)) piece = /** @type any */ (piece).value;
|
|
283
303
|
// #1154: same function guard for each piece of a mixed attribute.
|
|
284
304
|
assertNotFunctionActionAttr(piece, part.name, part.el.localName);
|
|
305
|
+
if (piece === nothing) { part.el.removeAttribute(part.name); return; }
|
|
285
306
|
val += String(piece ?? '');
|
|
286
307
|
val += mp.statics[j + 1] || '';
|
|
287
308
|
}
|
|
@@ -333,9 +354,16 @@ export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb)
|
|
|
333
354
|
// application is deferred one microtask so the documented
|
|
334
355
|
// lifecycle timing holds: firstUpdated() sees the <slot>
|
|
335
356
|
// element itself, the populated content lands right after.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
357
|
+
// A slot a hydration render ADOPTED (#1666) is the live SSR slot: its
|
|
358
|
+
// children are the projected nodes, left in place, and its holding
|
|
359
|
+
// fragment already carries the fresh clone's fallback.
|
|
360
|
+
if (adoptedSlots.has(slotEl)) {
|
|
361
|
+
adoptedSlots.delete(slotEl);
|
|
362
|
+
} else {
|
|
363
|
+
const frag = document.createDocumentFragment();
|
|
364
|
+
while (slotEl.firstChild) frag.appendChild(slotEl.firstChild);
|
|
365
|
+
/** @type {any} */ (slotEl)[SLOT_FALLBACK_FRAG] = frag;
|
|
366
|
+
}
|
|
339
367
|
queueMicrotask(() => applySlotAssignments(host));
|
|
340
368
|
};
|
|
341
369
|
const directHost = findSlotHost(slotEl);
|
|
@@ -453,8 +481,8 @@ export function applyElement(part, value) {
|
|
|
453
481
|
}
|
|
454
482
|
}
|
|
455
483
|
partAny.__refElement = part.el;
|
|
456
|
-
// Keep
|
|
457
|
-
//
|
|
484
|
+
// Keep `lastTarget` in sync for clearInstance / disposeInstance, which
|
|
485
|
+
// read it for template-disposal cleanup.
|
|
458
486
|
part.lastTarget = nextTarget;
|
|
459
487
|
}
|
|
460
488
|
}
|
|
@@ -466,6 +494,7 @@ export function applyChild(part, value, reconcileFormActionsCb) {
|
|
|
466
494
|
// short-circuit, etc. Done once per outermost applyChild call; the
|
|
467
495
|
// directive handlers recurse via applyChildInner (no re-clear) so
|
|
468
496
|
// their own state survives the recursion.
|
|
497
|
+
if (value === noChange) return;
|
|
469
498
|
clearStaleDirectiveState(part, value);
|
|
470
499
|
return applyChildInner(part, value, reconcileFormActionsCb);
|
|
471
500
|
}
|
|
@@ -487,9 +516,13 @@ export function applyChildInner(part, value, reconcileFormActionsCb) {
|
|
|
487
516
|
// asyncReplace, streaming) re-enter here from a promise / microtask OUTSIDE
|
|
488
517
|
// any render() window, so this is where those commits into a light host are
|
|
489
518
|
// marked as renderer writes rather than authored content.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
519
|
+
const previousHost = currentRenderHost;
|
|
520
|
+
currentRenderHost = part.__eventHost;
|
|
521
|
+
try {
|
|
522
|
+
return commitInto(part.marker && part.marker.parentNode, () =>
|
|
523
|
+
applyChildInnerRaw(part, value, reconcileFormActionsCb),
|
|
524
|
+
);
|
|
525
|
+
} finally { currentRenderHost = previousHost; }
|
|
493
526
|
}
|
|
494
527
|
|
|
495
528
|
/**
|
|
@@ -698,7 +731,7 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
698
731
|
}
|
|
699
732
|
}
|
|
700
733
|
|
|
701
|
-
if (value == null || value ===
|
|
734
|
+
if (value == null || value === nothing || value === noChange) return;
|
|
702
735
|
|
|
703
736
|
if (isTemplate(value)) {
|
|
704
737
|
const tr = /** @type any */ (value);
|
|
@@ -733,6 +766,7 @@ export function applyChildInnerRaw(part, value, reconcileFormActionsCb) {
|
|
|
733
766
|
}
|
|
734
767
|
|
|
735
768
|
export function updateInstance(inst, values, reconcileFormActionsCb) {
|
|
769
|
+
values = values.map((value, i) => value === noChange ? (inst.lastValues[i] ?? nothing) : value);
|
|
736
770
|
for (let i = 0; i < values.length; i++) {
|
|
737
771
|
const next = values[i];
|
|
738
772
|
if (Object.is(next, inst.lastValues[i])) continue;
|
|
@@ -875,7 +909,7 @@ export function buildDetached(tr, reconcileFormActionsCb) {
|
|
|
875
909
|
/** @param {TemplateInstance} inst */
|
|
876
910
|
export function disposeInstance(inst) {
|
|
877
911
|
for (const p of inst.bound) {
|
|
878
|
-
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher);
|
|
912
|
+
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher, p.handler);
|
|
879
913
|
if (p.kind === 'element') {
|
|
880
914
|
// Unbind any active ref so the user observes the element being
|
|
881
915
|
// removed (callback receives undefined / Ref.value cleared).
|
|
@@ -1129,7 +1163,7 @@ function buildArrayItem(v, reconcileFormActionsCb) {
|
|
|
1129
1163
|
const { inst, frag } = buildDetached(/** @type any */ (v), reconcileFormActionsCb);
|
|
1130
1164
|
return { item: { type: 'tpl', inst }, frag };
|
|
1131
1165
|
}
|
|
1132
|
-
if (v != null && v !==
|
|
1166
|
+
if (v != null && v !== nothing && v !== noChange) {
|
|
1133
1167
|
const node = document.createTextNode(String(v));
|
|
1134
1168
|
return { item: { type: 'text', node }, frag: node };
|
|
1135
1169
|
}
|
|
@@ -1196,7 +1230,7 @@ function reconcileArray(part, value, reconcileFormActionsCb) {
|
|
|
1196
1230
|
consumed = i + 1;
|
|
1197
1231
|
continue;
|
|
1198
1232
|
}
|
|
1199
|
-
} else if (v != null && v !==
|
|
1233
|
+
} else if (v != null && v !== nothing && v !== noChange) {
|
|
1200
1234
|
if (o && o.type === 'text') {
|
|
1201
1235
|
const str = String(v);
|
|
1202
1236
|
if (o.node.data !== str) o.node.data = str;
|
|
@@ -1991,7 +2025,7 @@ function renderToNodes(value, reconcileFormActionsCb) {
|
|
|
1991
2025
|
// #1443: the streamed / detached counterpart of the per-item unwrap above.
|
|
1992
2026
|
// Recursion covers a nested array, since each level re-enters here.
|
|
1993
2027
|
if (isLive(value)) value = /** @type any */ (value).value;
|
|
1994
|
-
if (value == null || value ===
|
|
2028
|
+
if (value == null || value === nothing || value === noChange) return [];
|
|
1995
2029
|
if (isTemplate(value)) {
|
|
1996
2030
|
const tr = /** @type any */ (value);
|
|
1997
2031
|
const { templateEl, parts, formActions } = compile(tr);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nothing, noChange } from '../sentinels.js';
|
|
1
2
|
import { isTemplate, MARKER } from '../html.js';
|
|
2
3
|
import {
|
|
3
4
|
reconcileFormAction, isBoundFormAction, ABSENT, assertSubmitterHasNoName, assertSubmitterType,
|
|
@@ -10,8 +11,9 @@ import { isLive } from '../directives.js';
|
|
|
10
11
|
import { attrHoleValue } from '../binding-prefixes.js';
|
|
11
12
|
import { RENDERING, SLOT_OWNER, SLOT_STATE, drainRendererBackstop, rescueAssignedNodes } from '../slot.js';
|
|
12
13
|
import { compile, submitterActionBindings, INSTANCE } from './template-compiler.js';
|
|
14
|
+
import { commitAdoption, planSlotAdoption, retargetParts } from './adopt-slots.js';
|
|
13
15
|
import {
|
|
14
|
-
applyPart, bindPart, currentRenderRoot, findSlotHost, setCurrentRenderRoot, updateInstance,
|
|
16
|
+
applyPart, bindPart, currentRenderHost, setCurrentRenderHost, currentRenderRoot, findSlotHost, setCurrentRenderRoot, updateInstance,
|
|
15
17
|
} from './parts.js';
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -19,8 +21,10 @@ import {
|
|
|
19
21
|
*
|
|
20
22
|
* @param {unknown} value
|
|
21
23
|
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
24
|
+
* @param {{host?: object}} [options]
|
|
22
25
|
*/
|
|
23
|
-
export function render(value, container) {
|
|
26
|
+
export function render(value, container, options = {}) {
|
|
27
|
+
if (value === noChange) return;
|
|
24
28
|
const host = /** @type any */ (container);
|
|
25
29
|
// Open the renderer-write window for the whole commit: every host-receiver
|
|
26
30
|
// write below (and in createInstance / updateInstance / clearInstance / all
|
|
@@ -29,6 +33,8 @@ export function render(value, container) {
|
|
|
29
33
|
// between a renderer commit and an author write.
|
|
30
34
|
const prevRendering = host[RENDERING];
|
|
31
35
|
host[RENDERING] = true;
|
|
36
|
+
const prevHost = currentRenderHost;
|
|
37
|
+
setCurrentRenderHost(options.host);
|
|
32
38
|
const prevRenderRoot = currentRenderRoot;
|
|
33
39
|
setCurrentRenderRoot(container);
|
|
34
40
|
try {
|
|
@@ -44,12 +50,12 @@ export function render(value, container) {
|
|
|
44
50
|
|
|
45
51
|
// Light DOM hydration: if container has SSR content (marked by
|
|
46
52
|
// <!--webjs-hydrate-->), remove the marker and proceed with normal
|
|
47
|
-
// rendering. The content
|
|
48
|
-
//
|
|
53
|
+
// rendering. The content is replaced with identical output, so nothing
|
|
54
|
+
// flashes, except that the SSR slots holding projected children stay in
|
|
55
|
+
// place (createInstance, #1666), so those children never leave the page.
|
|
49
56
|
const firstChild = container.firstChild;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
57
|
+
const hydrating = !!firstChild && firstChild.nodeType === 8 && /** @type {Comment} */ (firstChild).data === 'webjs-hydrate';
|
|
58
|
+
if (hydrating) firstChild.remove();
|
|
53
59
|
|
|
54
60
|
// Pre-set the symbol to an explicit null BEFORE the commit,
|
|
55
61
|
// UNCONDITIONALLY: if createInstance throws after its replaceChildren
|
|
@@ -59,7 +65,7 @@ export function render(value, container) {
|
|
|
59
65
|
// instance on the template-swap path (whose bookends would misclassify
|
|
60
66
|
// the half-committed new roots as unowned and fold them).
|
|
61
67
|
host[INSTANCE] = null;
|
|
62
|
-
const inst = createInstance(tr, container);
|
|
68
|
+
const inst = createInstance(tr, container, hydrating);
|
|
63
69
|
host[INSTANCE] = inst;
|
|
64
70
|
return;
|
|
65
71
|
}
|
|
@@ -68,7 +74,7 @@ export function render(value, container) {
|
|
|
68
74
|
if (prev) clearInstance(prev, container);
|
|
69
75
|
host[INSTANCE] = null;
|
|
70
76
|
container.replaceChildren();
|
|
71
|
-
if (value == null || value ===
|
|
77
|
+
if (value == null || value === nothing) return;
|
|
72
78
|
if (Array.isArray(value)) {
|
|
73
79
|
for (const v of value) {
|
|
74
80
|
const text = document.createTextNode(String(v ?? ''));
|
|
@@ -79,6 +85,7 @@ export function render(value, container) {
|
|
|
79
85
|
container.appendChild(document.createTextNode(String(value)));
|
|
80
86
|
} finally {
|
|
81
87
|
setCurrentRenderRoot(prevRenderRoot);
|
|
88
|
+
setCurrentRenderHost(prevHost);
|
|
82
89
|
host[RENDERING] = prevRendering;
|
|
83
90
|
// Outermost window closing: drain this commit's childList records off the
|
|
84
91
|
// slot backstop (drainRendererBackstop processes them with a renderer-output
|
|
@@ -307,7 +314,7 @@ function resolveHoleValue(v) {
|
|
|
307
314
|
* @param {import('../html.js').TemplateResult} tr
|
|
308
315
|
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
309
316
|
*/
|
|
310
|
-
function createInstance(tr, container) {
|
|
317
|
+
function createInstance(tr, container, hydrating = false) {
|
|
311
318
|
const { templateEl, parts, formActions } = compile(tr);
|
|
312
319
|
const frag = /** @type DocumentFragment */ (templateEl.content.cloneNode(true));
|
|
313
320
|
// Bookend markers bound the instance so we can tear it down cleanly.
|
|
@@ -315,6 +322,11 @@ function createInstance(tr, container) {
|
|
|
315
322
|
const endNode = document.createComment(`${MARKER}e`);
|
|
316
323
|
|
|
317
324
|
const bound = parts.map((p) => bindPart(p, frag));
|
|
325
|
+
// Hydration keeps the SSR slots that hold projected children, and their
|
|
326
|
+
// ancestors, in the document (#1666): the parts targeting them are pointed at
|
|
327
|
+
// the live elements now, before any applies.
|
|
328
|
+
const adoption = hydrating ? planSlotAdoption(container, frag, bound) : null;
|
|
329
|
+
if (adoption) retargetParts(adoption, bound);
|
|
318
330
|
const lastValues = [];
|
|
319
331
|
for (let i = 0; i < tr.values.length; i++) {
|
|
320
332
|
applyPart(bound[i], tr.values[i], undefined, tr.values, reconcileFormActions);
|
|
@@ -322,7 +334,11 @@ function createInstance(tr, container) {
|
|
|
322
334
|
}
|
|
323
335
|
reconcileFormActions(formActions, bound, tr.values);
|
|
324
336
|
|
|
325
|
-
|
|
337
|
+
if (adoption) {
|
|
338
|
+
commitAdoption(adoption, /** @type {Element} */ (container), [startNode, ...frag.childNodes, endNode]);
|
|
339
|
+
} else {
|
|
340
|
+
/** @type any */ (container).replaceChildren(startNode, ...frag.childNodes, endNode);
|
|
341
|
+
}
|
|
326
342
|
|
|
327
343
|
// Slot parts have no value-hole to drive applyPart from the loop above.
|
|
328
344
|
// Apply them once now that the fragment is inserted into the live
|
|
@@ -363,7 +379,7 @@ function clearInstance(inst, container) {
|
|
|
363
379
|
// slot parts so they survive teardown of a collapsing conditional
|
|
364
380
|
// fragment.
|
|
365
381
|
for (const p of inst.bound) {
|
|
366
|
-
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher);
|
|
382
|
+
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher, p.handler);
|
|
367
383
|
if (p.kind === 'element') {
|
|
368
384
|
// Guarded for the same reason as the sibling in `disposeInstance`, and
|
|
369
385
|
// the stakes are higher here: this is the container-level teardown
|
|
@@ -31,10 +31,10 @@ export const INSTANCE = Symbol.for('webjs.instance');
|
|
|
31
31
|
* }} TemplateInstance
|
|
32
32
|
*
|
|
33
33
|
* @typedef {
|
|
34
|
-
* | { kind: 'child', marker: Comment, child?: TemplateInstance | ChildNode[] }
|
|
34
|
+
* | { kind: 'child', marker: Comment, __eventHost?: object, child?: TemplateInstance | ChildNode[] }
|
|
35
35
|
* | { kind: 'attr', el: Element, name: string }
|
|
36
36
|
* | { kind: 'attr-mixed', el: Element, name: string, statics: string[], group: number[] }
|
|
37
|
-
* | { kind: 'event', el: Element, name: string, handler: (
|
|
37
|
+
* | { kind: 'event', el: Element, name: string, handler: (EventListenerOrEventListenerObject & AddEventListenerOptions) | null, eventHost?: object, dispatcher: (e: Event) => void }
|
|
38
38
|
* | { kind: 'prop', el: Element, name: string }
|
|
39
39
|
* | { kind: 'bool', el: Element, name: string }
|
|
40
40
|
* | { kind: 'element', el: Element, lastTarget?: any }
|
|
@@ -169,10 +169,14 @@ export function compile(tr) {
|
|
|
169
169
|
const eq = sLast.indexOf(attrQuote);
|
|
170
170
|
statics.push(eq >= 0 ? sLast.slice(0, eq) : sLast);
|
|
171
171
|
|
|
172
|
+
const specialized = isBindingPrefix(mixedAttr.name[0]);
|
|
173
|
+
if (specialized && (group.length !== 1 || statics.some((piece) => piece !== ''))) {
|
|
174
|
+
throw new Error('Event, property and boolean bindings require exactly one expression with no surrounding text.');
|
|
175
|
+
}
|
|
172
176
|
parts[idx0] = {
|
|
173
|
-
kind: 'attr-mixed',
|
|
177
|
+
kind: specialized ? BINDING_PREFIXES[mixedAttr.name[0]] : 'attr-mixed',
|
|
174
178
|
path: [],
|
|
175
|
-
name: mixedAttr.name,
|
|
179
|
+
name: specialized ? mixedAttr.name.slice(1) : mixedAttr.name,
|
|
176
180
|
statics,
|
|
177
181
|
group,
|
|
178
182
|
};
|
|
@@ -262,7 +266,17 @@ export function compile(tr) {
|
|
|
262
266
|
}
|
|
263
267
|
|
|
264
268
|
const templateEl = document.createElement('template');
|
|
265
|
-
|
|
269
|
+
if (tr.svg) {
|
|
270
|
+
// An svg`` fragment is parsed inside an <svg> wrapper so its elements
|
|
271
|
+
// land in the SVG namespace, then the wrapper is unwrapped BEFORE the
|
|
272
|
+
// slot and path walks, so every recorded path is relative to the
|
|
273
|
+
// fragment's own root, as it is for an html`` template (lit does the same).
|
|
274
|
+
templateEl.innerHTML = `<svg>${html}</svg>`;
|
|
275
|
+
const wrapper = /** @type {Element} */ (templateEl.content.firstChild);
|
|
276
|
+
wrapper.replaceWith(...wrapper.childNodes);
|
|
277
|
+
} else {
|
|
278
|
+
templateEl.innerHTML = html;
|
|
279
|
+
}
|
|
266
280
|
|
|
267
281
|
// Mark every <slot> in the template for framework projection and
|
|
268
282
|
// register a SLOT part for each so the slot-apply step can find them on
|
package/src/render-client.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface RenderOptions { host?: object; }
|
|
2
|
+
export function render(value: unknown, container: Element | DocumentFragment | ShadowRoot, options?: RenderOptions): void;
|
|
@@ -42,8 +42,8 @@ export function parseAttrs(attrStr) {
|
|
|
42
42
|
*/
|
|
43
43
|
export function seedServerAttrs(instance, attrs) {
|
|
44
44
|
if (!instance || typeof instance.setAttribute !== 'function') return;
|
|
45
|
-
for (const
|
|
46
|
-
instance.setAttribute(name, decodeAttrEntities(
|
|
45
|
+
for (const name in attrs) {
|
|
46
|
+
instance.setAttribute(name, decodeAttrEntities(attrs[name]));
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ export function seedServerAttrs(instance, attrs) {
|
|
|
55
55
|
* @returns {string}
|
|
56
56
|
*/
|
|
57
57
|
export function withHostMarker(opening) {
|
|
58
|
-
if (/\sdata-wj-host(?=[\s>=])/i.test(opening)) return opening;
|
|
58
|
+
if (opening.includes('data-wj-host') && /\sdata-wj-host(?=[\s>=])/i.test(opening)) return opening;
|
|
59
59
|
return `${opening.slice(0, -1)} data-wj-host>`;
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -94,7 +94,8 @@ export function appendReflectedAttrs(opening, instance, presentAttrNames) {
|
|
|
94
94
|
* based on its static `properties` declaration.
|
|
95
95
|
*/
|
|
96
96
|
export function applyAttrsToInstance(instance, attrs, Cls) {
|
|
97
|
-
for (const
|
|
97
|
+
for (const sourceName in attrs) {
|
|
98
|
+
const sourceValue = attrs[sourceName];
|
|
98
99
|
// The browser LOWERCASES every attribute name while parsing an HTML
|
|
99
100
|
// document, so `cfgData="x"` reaches the client reader as `cfgdata` and a
|
|
100
101
|
// camelCase name can never match anything in `observedAttributes`.
|