@webjsdev/core 0.7.65 → 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 (44) 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 +19 -19
  4. package/dist/webjs-core-testing.js.map +3 -3
  5. package/dist/webjs-core.js +6 -6
  6. package/dist/webjs-core.js.map +3 -3
  7. package/index-browser.js +5 -1
  8. package/index.d.ts +5 -1
  9. package/index.js +5 -1
  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 +17 -3
  14. package/src/binding-prefixes.js +10 -41
  15. package/src/component/lifecycle.js +177 -59
  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/html.d.ts +4 -0
  21. package/src/html.js +22 -1
  22. package/src/render-client/adopt-slots.js +245 -0
  23. package/src/render-client/parts.js +51 -17
  24. package/src/render-client/reconciler.js +28 -12
  25. package/src/render-client/template-compiler.js +19 -5
  26. package/src/render-client.d.ts +2 -1
  27. package/src/render-server/compile.js +16 -2
  28. package/src/render-server/expand.js +39 -12
  29. package/src/render-server/served-attrs.js +79 -0
  30. package/src/render-server/walk.js +42 -2
  31. package/src/router-client/anchors.js +3 -1
  32. package/src/router-client/boundaries.js +15 -1
  33. package/src/router-client/constants.js +30 -0
  34. package/src/router-client/fetch-apply.js +21 -0
  35. package/src/router-client/frames.js +6 -3
  36. package/src/router-client/head-merge.js +58 -10
  37. package/src/router-client/navigator.js +4 -0
  38. package/src/router-client/owned-styles.js +372 -0
  39. package/src/router-client/prefetch.js +3 -0
  40. package/src/router-client/stylesheets.js +191 -0
  41. package/src/router-client/swap.js +49 -9
  42. package/src/sentinels.d.ts +2 -0
  43. package/src/sentinels.js +4 -0
  44. package/src/webjs-config.d.ts +1 -1
@@ -1,13 +1,13 @@
1
1
  import { tagOf } from '../registry.js';
2
2
 
3
3
  /**
4
- * Default change detection: strict inequality.
4
+ * Default change detection: Object.is inequality (NaN is stable; signed zero changes).
5
5
  * @param {unknown} a
6
6
  * @param {unknown} b
7
7
  * @returns {boolean}
8
8
  */
9
9
  export function defaultHasChanged(a, b) {
10
- return a !== b;
10
+ return !Object.is(a, b);
11
11
  }
12
12
 
13
13
  /**
@@ -108,8 +108,9 @@ export function warnUnserializableReflection(host, propName, attrName, detail) {
108
108
  * while staying JSDoc-only and no-build.
109
109
  *
110
110
  * Subclasses declare:
111
- * - `static properties`: attribute/property declarations with type info,
112
- * reflection, custom converters, and internal-state mode
111
+ * - reactive properties, through the `WebComponent({ ... })` factory:
112
+ * attribute/property declarations with type info, reflection and custom
113
+ * converters (internal state is an instance signal, not a property)
113
114
  * - `static styles`: CSSResult or array thereof (only meaningful with
114
115
  * `static shadow = true`; light-DOM components inherit global CSS)
115
116
  * - `static shadow`: set `true` to opt in to shadow DOM (default: `false`
@@ -85,11 +85,11 @@ interface WebComponentBase extends HTMLElement {
85
85
  * Schedule a re-render. Optionally record a property change so lifecycle
86
86
  * hooks can branch on what changed via `changedProperties`.
87
87
  *
88
- * For instance-local state, prefer a reactive property (`WebComponent({
89
- * foo: prop({ state: true }) })`) or a signal from
90
- * `@webjsdev/core`'s `signal()`. The framework's built-in
91
- * SignalWatcher wires every signal read in `render()` to a re-render
92
- * automatically; explicit `requestUpdate()` is rarely needed.
88
+ * For instance-local state, use a reactive property (`WebComponent({
89
+ * open: prop<boolean>({ state: true }) })`) or a signal from
90
+ * `@webjsdev/core`'s `signal()`. The framework's built-in SignalWatcher
91
+ * wires every signal the update cycle reads to a re-render automatically;
92
+ * explicit `requestUpdate()` is rarely needed.
93
93
  */
94
94
  requestUpdate(name?: string, oldValue?: unknown): void;
95
95
  /** A Promise that resolves after the next render commit. */
@@ -210,6 +210,12 @@ export declare function prop<T>(
210
210
  opts?: Omit<PropertyDeclaration<T>, 'type'>
211
211
  ): PropertyDeclaration<T>;
212
212
 
213
+ // No type constructor and no attribute: nothing says what the value is, so it
214
+ // is `unknown` until narrowed (`prop<boolean>({ state: true })`).
215
+ export declare function prop<T = unknown>(
216
+ opts: PropertyDeclaration<T> & ({ state: true } | { attribute: false })
217
+ ): PropertyDeclaration<T>;
218
+
213
219
  export declare function prop<T = string>(
214
220
  opts?: PropertyDeclaration<T>
215
221
  ): PropertyDeclaration<T>;
@@ -96,3 +96,5 @@ export function asyncReplace<T>(
96
96
  export function isAsyncReplace(x: unknown): x is AsyncReplaceDirective;
97
97
  export function watch<T>(sig: { get(): T; __isSignal: true }): WatchDirective<T>;
98
98
  export function isWatch(x: unknown): x is WatchDirective;
99
+
100
+ export { classMap, styleMap, ifDefined } from './attribute-directives.js';
package/src/directives.js CHANGED
@@ -458,3 +458,5 @@ export function watch(sig) {
458
458
  export function isWatch(x) {
459
459
  return !!x && typeof x === 'object' && /** @type {any} */ (x)._$webjs === 'watch';
460
460
  }
461
+
462
+ export { classMap, styleMap, ifDefined } from './attribute-directives.js';
package/src/html.d.ts CHANGED
@@ -2,8 +2,12 @@ export interface TemplateResult {
2
2
  _$webjs: 'template';
3
3
  strings: TemplateStringsArray | string[];
4
4
  values: unknown[];
5
+ /** Set by the `svg` tag: the client parses the strings inside an `<svg>` wrapper. */
6
+ svg?: boolean;
5
7
  }
6
8
 
7
9
  export function html(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
10
+ /** lit's `svg` tag: a fragment parsed as SVG on the client, rendered like `html` on the server. */
11
+ export function svg(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
8
12
  export function isTemplate(x: unknown): x is TemplateResult;
9
13
  export const MARKER: 'wjm-';
package/src/html.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * A TemplateResult is an isomorphic description of HTML: the static string
5
5
  * pieces plus the dynamic values. Server and client renderers both consume it.
6
6
  *
7
- * @typedef {{ _$webjs: 'template', strings: TemplateStringsArray | string[], values: unknown[] }} TemplateResult
7
+ * @typedef {{ _$webjs: 'template', strings: TemplateStringsArray | string[], values: unknown[], svg?: boolean }} TemplateResult
8
8
  *
9
9
  * @param {TemplateStringsArray | string[]} strings
10
10
  * @param {...unknown} values
@@ -14,6 +14,27 @@ export function html(strings, ...values) {
14
14
  return { _$webjs: 'template', strings, values };
15
15
  }
16
16
 
17
+ /**
18
+ * Tagged template literal for an SVG FRAGMENT, lit's `svg` tag.
19
+ *
20
+ * Use it for markup that sits INSIDE an `<svg>` element but is built in its
21
+ * own template (a `<rect>` per data point, a `<path>` a helper returns).
22
+ * An `html` template is parsed as HTML on the client, so a `<rect>` there
23
+ * becomes an HTML element and never paints. An `svg` template is parsed
24
+ * inside an `<svg>` wrapper, so its elements are SVG elements. A whole
25
+ * `<svg>` element still belongs in `html`.
26
+ *
27
+ * The server renders it exactly like `html` (markup is text there), so the
28
+ * two differ only in how the client parses the static strings.
29
+ *
30
+ * @param {TemplateStringsArray | string[]} strings
31
+ * @param {...unknown} values
32
+ * @returns {TemplateResult}
33
+ */
34
+ export function svg(strings, ...values) {
35
+ return { _$webjs: 'template', strings, values, svg: true };
36
+ }
37
+
17
38
  /**
18
39
  * Identity check for TemplateResult.
19
40
  * @param {unknown} x
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Hydration slot adoption: keep a light-DOM host's SSR-projected children in
3
+ * the document while its first client render replaces the SSR markup (#1666).
4
+ *
5
+ * Hydration renders the template from a fresh clone and swaps it in for the
6
+ * SSR markup. The projected children sit inside the SSR `<slot>`, so swapping
7
+ * the SSR tree out took them out of the document, and the first slot apply put
8
+ * them back a microtask later. A removal is not free: the HTML spec navigates
9
+ * a re-inserted iframe again (and `loading="lazy"` defers only the FIRST
10
+ * insertion, so a hidden lazy frame loads), and every nested custom element
11
+ * disconnects and reconnects.
12
+ *
13
+ * So for each of the template's own slots whose SSR counterpart holds exactly
14
+ * the nodes the host's record adopted, the SSR slot and its ancestor chain up
15
+ * to the host are KEPT: the parts that target a fresh element on that chain
16
+ * are pointed at its live twin, and the fresh siblings around the chain are
17
+ * inserted into the live elements instead of the live tree being replaced. No
18
+ * kept node moves, so the projected children never leave the document. The
19
+ * fresh slot's cloned fallback goes to the slot's holding fragment, where the
20
+ * slot part would have put it.
21
+ *
22
+ * Conservative by construction: a slot is adopted only when every link of
23
+ * both chains matches by tag, no link is a custom element (whose SSR markup is
24
+ * its own rendered template) or a `<form>` (form-action reconciliation inserts
25
+ * children into it), the slot's name is static, and the kept nodes sit in the
26
+ * same relative order on both sides. Anything else, a slot inside a
27
+ * conditional template among them, falls back to the plain swap.
28
+ *
29
+ * @module
30
+ */
31
+ import {
32
+ PROJECTION_ACTUAL, PROJECTION_ATTR, SLOT_FALLBACK_FRAG, SLOT_OWNER_ATTR,
33
+ ensureSlotState, hasSlotState, keyOfName,
34
+ } from '../slot.js';
35
+
36
+ /**
37
+ * Live SSR slots a hydration render adopted. The slot part's finalize reads
38
+ * it to leave the slot's children (the projected nodes) in place instead of
39
+ * harvesting them as fallback, since the holding fragment is already set.
40
+ * @type {WeakSet<Element>}
41
+ */
42
+ export const adoptedSlots = new WeakSet();
43
+
44
+ /**
45
+ * @typedef {{ map: Map<Node, Element>, slots: [Element, Element][], chains: [Element, Element][] }} AdoptionPlan
46
+ */
47
+
48
+ /**
49
+ * Plan the adoption for a hydration render, after the parts are bound and
50
+ * before any is applied. Returns null when no slot qualifies.
51
+ *
52
+ * @param {Element | DocumentFragment | ShadowRoot} container
53
+ * @param {DocumentFragment} frag the fresh clone
54
+ * @param {any[]} bound the template's bound parts
55
+ * @returns {AdoptionPlan | null}
56
+ */
57
+ export function planSlotAdoption(container, frag, bound) {
58
+ if (/** @type {any} */ (container).nodeType !== 1 || !hasSlotState(/** @type {Element} */ (container))) return null;
59
+ const state = ensureSlotState(/** @type {Element} */ (container));
60
+ /** @type {Set<Element>} */
61
+ const dynamicName = new Set();
62
+ for (const p of bound) {
63
+ if ((p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') && p.name === 'name') dynamicName.add(p.el);
64
+ }
65
+ /** @type {Map<Node, Element>} fresh element to its live twin */
66
+ const map = new Map();
67
+ /** @type {Set<Element>} */
68
+ const usedLive = new Set();
69
+ /** @type {[Element, Element][]} */
70
+ const slots = [];
71
+ /** @type {[Element, Element][]} */
72
+ const chains = [];
73
+ for (const p of bound) {
74
+ if (p.kind !== 'slot') continue;
75
+ const slotF = /** @type {Element} */ (p.slotEl);
76
+ if (dynamicName.has(slotF)) continue;
77
+ const assigned = state.assignedByName.get(keyOfName(slotF.getAttribute('name')));
78
+ if (!assigned || assigned.length === 0) continue;
79
+ const slotL = /** @type {Element | null} */ (assigned[0].parentNode);
80
+ if (!slotL || slotL.nodeType !== 1 || slotL.localName !== 'slot' || usedLive.has(slotL)) continue;
81
+ if (slotL.getAttribute(PROJECTION_ATTR) !== PROJECTION_ACTUAL) continue;
82
+ if (!sameNodes(slotL.childNodes, assigned)) continue;
83
+ const fChain = ancestorsUpTo(slotF, frag);
84
+ const lChain = ancestorsUpTo(slotL, container);
85
+ if (!fChain || !lChain || fChain.length !== lChain.length) continue;
86
+ let ok = true;
87
+ for (let i = 0; i < fChain.length && ok; i++) {
88
+ const f = fChain[i];
89
+ const l = lChain[i];
90
+ const name = f.localName;
91
+ if (name !== l.localName || name.includes('-') || name === 'form') ok = false;
92
+ else {
93
+ const prior = map.get(f);
94
+ if (prior ? prior !== l : usedLive.has(l)) ok = false;
95
+ }
96
+ }
97
+ if (!ok) continue;
98
+ map.set(slotF, slotL);
99
+ usedLive.add(slotL);
100
+ slots.push([slotF, slotL]);
101
+ for (let i = 0; i < fChain.length; i++) {
102
+ if (map.has(fChain[i])) continue;
103
+ map.set(fChain[i], lChain[i]);
104
+ usedLive.add(lChain[i]);
105
+ chains.push([fChain[i], lChain[i]]);
106
+ }
107
+ }
108
+ if (slots.length === 0) return null;
109
+ // No kept node may have to move: under each kept parent the kept children
110
+ // must already sit in the order the template puts them in.
111
+ for (const fParent of [frag, ...chains.map(([f]) => f)]) {
112
+ let last = null;
113
+ for (const child of fParent.childNodes) {
114
+ const live = map.get(child);
115
+ if (!live) continue;
116
+ if (last && !(last.compareDocumentPosition(live) & Node.DOCUMENT_POSITION_FOLLOWING)) return null;
117
+ last = live;
118
+ }
119
+ }
120
+ return { map, slots, chains };
121
+ }
122
+
123
+ /**
124
+ * Point every bound part that targets a kept fresh element at its live twin,
125
+ * and give each kept element the fresh one's static attributes. Runs before
126
+ * the parts apply, so every dynamic value lands on the live element directly.
127
+ *
128
+ * @param {AdoptionPlan} plan
129
+ * @param {any[]} bound
130
+ */
131
+ export function retargetParts(plan, bound) {
132
+ /** @type {Map<Element, Set<string>>} attributes a part owns, per live element */
133
+ const owned = new Map();
134
+ for (const p of bound) {
135
+ if (p.kind === 'slot') {
136
+ const live = plan.map.get(p.slotEl);
137
+ if (live) p.slotEl = live;
138
+ continue;
139
+ }
140
+ if (!p.el) continue;
141
+ const live = plan.map.get(p.el);
142
+ if (!live) continue;
143
+ if (p.kind === 'event') {
144
+ p.el.removeEventListener(p.name, p.dispatcher);
145
+ live.addEventListener(p.name, p.dispatcher);
146
+ }
147
+ p.el = live;
148
+ if (p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') {
149
+ let names = owned.get(live);
150
+ if (!names) owned.set(live, names = new Set());
151
+ names.add(p.name);
152
+ }
153
+ }
154
+ for (const [fresh, live] of [...plan.chains, ...plan.slots]) {
155
+ const isSlot = live.localName === 'slot';
156
+ const names = owned.get(live);
157
+ for (const a of Array.from(live.attributes)) {
158
+ if (fresh.hasAttribute(a.name) || names?.has(a.name)) continue;
159
+ // The SSR projection stamps stay: the slot IS projected, and the owner
160
+ // carrier is what isOwnSlot reads until the owner symbol is stamped.
161
+ if (isSlot && (a.name === PROJECTION_ATTR || a.name === SLOT_OWNER_ATTR)) continue;
162
+ live.removeAttribute(a.name);
163
+ }
164
+ for (const a of Array.from(fresh.attributes)) {
165
+ if (live.getAttribute(a.name) !== a.value) live.setAttribute(a.name, a.value);
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Commit the fresh template into the container around the kept live nodes:
172
+ * every live child that is not kept is removed, every fresh node is inserted
173
+ * in template order, and no kept node moves. The fresh slots' cloned fallback
174
+ * goes to each kept slot's holding fragment.
175
+ *
176
+ * @param {AdoptionPlan} plan
177
+ * @param {Element} container
178
+ * @param {Node[]} top the container's new children, bookends included
179
+ */
180
+ export function commitAdoption(plan, container, top) {
181
+ const mapped = (/** @type {Node} */ n) => plan.map.get(n) || n;
182
+ const state = ensureSlotState(container);
183
+ for (const [fresh, live] of plan.chains) mergeChildren(live, Array.from(fresh.childNodes, mapped));
184
+ mergeChildren(container, top.map(mapped));
185
+ for (const [fresh, live] of plan.slots) {
186
+ const fallback = document.createDocumentFragment();
187
+ while (fresh.firstChild) fallback.appendChild(fresh.firstChild);
188
+ /** @type {any} */ (live)[SLOT_FALLBACK_FRAG] = fallback;
189
+ adoptedSlots.add(live);
190
+ // Forget the adopted snapshot so the first apply reports the initial
191
+ // projection (one slotchange), exactly as the plain swap did.
192
+ state.lastSnapshot.delete(live);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Make `parent`'s children exactly `want`, removing what is not wanted and
198
+ * inserting the rest, without moving a node already in place in order.
199
+ *
200
+ * @param {Node} parent
201
+ * @param {Node[]} want
202
+ */
203
+ function mergeChildren(parent, want) {
204
+ const keep = new Set(want);
205
+ for (const c of Array.from(parent.childNodes)) {
206
+ if (!keep.has(c)) parent.removeChild(c);
207
+ }
208
+ let cursor = parent.firstChild;
209
+ for (const node of want) {
210
+ if (node === cursor) {
211
+ cursor = cursor.nextSibling;
212
+ continue;
213
+ }
214
+ parent.insertBefore(node, cursor);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * The element ancestors of `node` below `root`, nearest first, or null when
220
+ * `root` is not an ancestor.
221
+ *
222
+ * @param {Node} node
223
+ * @param {Node} root
224
+ * @returns {Element[] | null}
225
+ */
226
+ function ancestorsUpTo(node, root) {
227
+ const out = [];
228
+ let p = node.parentNode;
229
+ while (p && p !== root) {
230
+ if (p.nodeType !== 1) return null;
231
+ out.push(/** @type {Element} */ (p));
232
+ p = p.parentNode;
233
+ }
234
+ return p === root ? out : null;
235
+ }
236
+
237
+ /**
238
+ * @param {NodeListOf<ChildNode>} list
239
+ * @param {Node[]} nodes
240
+ */
241
+ function sameNodes(list, nodes) {
242
+ if (list.length !== nodes.length) return false;
243
+ for (let i = 0; i < nodes.length; i++) if (list[i] !== nodes[i]) return false;
244
+ return true;
245
+ }
@@ -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);
@@ -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);