@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.
Files changed (58) hide show
  1. package/dist/webjs-core-browser.js +5 -5
  2. package/dist/webjs-core-browser.js.map +3 -3
  3. package/dist/webjs-core-testing.js +21 -22
  4. package/dist/webjs-core-testing.js.map +3 -3
  5. package/dist/webjs-core.js +6 -7
  6. package/dist/webjs-core.js.map +3 -3
  7. package/index-browser.js +6 -2
  8. package/index.d.ts +7 -3
  9. package/index.js +6 -2
  10. package/package.json +1 -1
  11. package/src/attribute-directives.d.ts +6 -0
  12. package/src/attribute-directives.js +49 -0
  13. package/src/attribute-reader.js +37 -3
  14. package/src/binding-prefixes.js +10 -41
  15. package/src/component/lifecycle.js +261 -101
  16. package/src/component/reactive.js +5 -4
  17. package/src/component.d.ts +11 -5
  18. package/src/directives.d.ts +2 -0
  19. package/src/directives.js +2 -0
  20. package/src/escape.js +12 -2
  21. package/src/form-action.js +7 -7
  22. package/src/html.d.ts +4 -0
  23. package/src/html.js +22 -1
  24. package/src/optimistic.js +3 -3
  25. package/src/registry.d.ts +5 -0
  26. package/src/registry.js +35 -4
  27. package/src/render-client/adopt-slots.js +245 -0
  28. package/src/render-client/parts.js +53 -19
  29. package/src/render-client/reconciler.js +28 -12
  30. package/src/render-client/template-compiler.js +19 -5
  31. package/src/render-client.d.ts +2 -1
  32. package/src/render-server/attrs.js +5 -4
  33. package/src/render-server/compile.js +433 -0
  34. package/src/render-server/dsd.js +11 -435
  35. package/src/render-server/errors.js +84 -0
  36. package/src/render-server/expand.js +346 -0
  37. package/src/render-server/html-scan.js +19 -358
  38. package/src/render-server/served-attrs.js +79 -0
  39. package/src/render-server/stream.js +11 -13
  40. package/src/render-server/template-renderer.js +13 -936
  41. package/src/render-server/walk.js +798 -0
  42. package/src/router-client/anchors.js +3 -1
  43. package/src/router-client/boundaries.js +15 -1
  44. package/src/router-client/constants.js +30 -0
  45. package/src/router-client/fetch-apply.js +21 -0
  46. package/src/router-client/frames.js +6 -3
  47. package/src/router-client/head-merge.js +58 -10
  48. package/src/router-client/navigator.js +4 -0
  49. package/src/router-client/owned-styles.js +372 -0
  50. package/src/router-client/prefetch.js +3 -0
  51. package/src/router-client/snapshot-cache.js +2 -12
  52. package/src/router-client/stylesheets.js +191 -0
  53. package/src/router-client/swap.js +49 -9
  54. package/src/sentinels.d.ts +2 -0
  55. package/src/sentinels.js +4 -0
  56. package/src/slot.js +1 -1
  57. package/src/webjs-config.d.ts +1 -1
  58. 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) { part.handler?.(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
- part.handler = typeof value === 'function' ? /** @type any */ (value) : null;
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
- const frag = document.createDocumentFragment();
337
- while (slotEl.firstChild) frag.appendChild(slotEl.firstChild);
338
- /** @type {any} */ (slotEl)[SLOT_FALLBACK_FRAG] = frag;
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 the legacy `lastTarget` field in sync for clearInstance /
457
- // disposeInstance which read it for template-disposal cleanup.
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
- return commitInto(part.marker && part.marker.parentNode, () =>
491
- applyChildInnerRaw(part, value, reconcileFormActionsCb),
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 === false || value === true) return;
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 !== false && v !== true) {
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 !== false && v !== true) {
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 === false || value === true) return [];
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 will be replaced with identical output -
48
- // no visible flash because SSR and client render produce the same HTML.
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
- if (firstChild && firstChild.nodeType === 8 && /** @type {Comment} */ (firstChild).data === 'webjs-hydrate') {
51
- firstChild.remove();
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 === false || value === true) return;
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
- /** @type any */ (container).replaceChildren(startNode, ...frag.childNodes, endNode);
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: ((e: Event) => void) | null, dispatcher: (e: Event) => void }
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
- templateEl.innerHTML = html;
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
@@ -1 +1,2 @@
1
- export function render(value: unknown, container: Element | DocumentFragment | ShadowRoot): void;
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 [name, raw] of Object.entries(attrs)) {
46
- instance.setAttribute(name, decodeAttrEntities(raw));
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 [sourceName, sourceValue] of Object.entries(attrs)) {
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`.