@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.
- package/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +19 -19
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +6 -6
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +5 -1
- package/index.d.ts +5 -1
- package/index.js +5 -1
- 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 +17 -3
- package/src/binding-prefixes.js +10 -41
- package/src/component/lifecycle.js +177 -59
- 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/html.d.ts +4 -0
- package/src/html.js +22 -1
- package/src/render-client/adopt-slots.js +245 -0
- package/src/render-client/parts.js +51 -17
- 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/compile.js +16 -2
- package/src/render-server/expand.js +39 -12
- package/src/render-server/served-attrs.js +79 -0
- package/src/render-server/walk.js +42 -2
- 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/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/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:
|
|
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
|
|
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
|
-
* -
|
|
112
|
-
*
|
|
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`
|
package/src/component.d.ts
CHANGED
|
@@ -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,
|
|
89
|
-
*
|
|
90
|
-
* `@webjsdev/core`'s `signal()`. The framework's built-in
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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>;
|
package/src/directives.d.ts
CHANGED
|
@@ -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
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) {
|
|
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);
|
|
@@ -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);
|