@fluixi/dom 1.0.0-alpha.82 → 1.0.0-alpha.84
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/cdn/dom-client.cjs +1427 -1
- package/dist/cdn/dom-client.d.ts +2 -2
- package/dist/cdn/dom-client.global.js +2 -1
- package/dist/cdn/dom-client.js +3 -3
- package/dist/cdn/dom-client.mjs +1430 -1
- package/dist/cdn/dom.cjs +3127 -1
- package/dist/cdn/dom.d.ts +2 -2
- package/dist/cdn/dom.global.js +2 -1
- package/dist/cdn/dom.js +4 -4
- package/dist/cdn/dom.mjs +3133 -1
- package/dist/index.cjs +3155 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +3149 -1
- package/dist/lib/component/index.cjs +128 -1
- package/dist/lib/component/index.mjs +105 -1
- package/dist/lib/component/modifiers.cjs +126 -1
- package/dist/lib/component/modifiers.d.ts +12 -0
- package/dist/lib/component/modifiers.d.ts.map +1 -1
- package/dist/lib/component/modifiers.js +12 -84
- package/dist/lib/component/modifiers.mjs +105 -1
- package/dist/lib/dom/delegate.cjs +60 -1
- package/dist/lib/dom/delegate.js +1 -1
- package/dist/lib/dom/delegate.mjs +39 -1
- package/dist/lib/dom/dynamic-element.cjs +949 -1
- package/dist/lib/dom/dynamic-element.d.ts +3 -3
- package/dist/lib/dom/dynamic-element.js +3 -3
- package/dist/lib/dom/dynamic-element.mjs +928 -1
- package/dist/lib/dom/hydration-state.cjs +93 -1
- package/dist/lib/dom/hydration-state.d.ts +1 -1
- package/dist/lib/dom/hydration-state.js +1 -1
- package/dist/lib/dom/hydration-state.mjs +72 -1
- package/dist/lib/dom/hydration.cjs +685 -1
- package/dist/lib/dom/hydration.js +8 -8
- package/dist/lib/dom/hydration.mjs +664 -1
- package/dist/lib/dom/index.cjs +2040 -1
- package/dist/lib/dom/index.js +1 -1
- package/dist/lib/dom/index.mjs +2020 -1
- package/dist/lib/dom/integration.cjs +71 -1
- package/dist/lib/dom/integration.d.ts.map +1 -1
- package/dist/lib/dom/integration.js +15 -6
- package/dist/lib/dom/integration.mjs +51 -1
- package/dist/lib/dom/island.cjs +790 -1
- package/dist/lib/dom/island.d.ts +1 -1
- package/dist/lib/dom/island.js +12 -12
- package/dist/lib/dom/island.mjs +769 -1
- package/dist/lib/dom/runtime.cjs +1432 -1
- package/dist/lib/dom/runtime.d.ts +71 -19
- package/dist/lib/dom/runtime.d.ts.map +1 -1
- package/dist/lib/dom/runtime.js +261 -100
- package/dist/lib/dom/runtime.mjs +1414 -1
- package/dist/lib/dom/server/fx-data.cjs +53 -1
- package/dist/lib/dom/server/fx-data.d.ts +5 -5
- package/dist/lib/dom/server/fx-data.js +6 -6
- package/dist/lib/dom/server/fx-data.mjs +32 -1
- package/dist/lib/dom/server/host.cjs +66 -1
- package/dist/lib/dom/server/host.d.ts +14 -2
- package/dist/lib/dom/server/host.d.ts.map +1 -1
- package/dist/lib/dom/server/host.js +38 -10
- package/dist/lib/dom/server/host.mjs +45 -1
- package/dist/lib/dom/server/index.cjs +759 -1
- package/dist/lib/dom/server/index.d.ts +1 -1
- package/dist/lib/dom/server/index.js +1 -1
- package/dist/lib/dom/server/index.mjs +743 -1
- package/dist/lib/dom/server/nodes.cjs +302 -1
- package/dist/lib/dom/server/nodes.d.ts +2 -2
- package/dist/lib/dom/server/nodes.d.ts.map +1 -1
- package/dist/lib/dom/server/nodes.js +4 -4
- package/dist/lib/dom/server/nodes.mjs +279 -1
- package/dist/lib/dom/server/parse-template.cjs +395 -1
- package/dist/lib/dom/server/parse-template.d.ts +1 -1
- package/dist/lib/dom/server/parse-template.js +4 -4
- package/dist/lib/dom/server/parse-template.mjs +372 -1
- package/dist/lib/dom/server/render.cjs +691 -1
- package/dist/lib/dom/server/render.d.ts +9 -1
- package/dist/lib/dom/server/render.d.ts.map +1 -1
- package/dist/lib/dom/server/render.js +25 -17
- package/dist/lib/dom/server/render.mjs +677 -1
- package/dist/lib/dom/server/request-context.cjs +110 -1
- package/dist/lib/dom/server/request-context.d.ts +29 -9
- package/dist/lib/dom/server/request-context.d.ts.map +1 -1
- package/dist/lib/dom/server/request-context.js +28 -8
- package/dist/lib/dom/server/request-context.mjs +89 -1
- package/dist/lib/dom/server/serialize.cjs +187 -1
- package/dist/lib/dom/server/serialize.js +3 -3
- package/dist/lib/dom/server/serialize.mjs +164 -1
- package/dist/lib/dom/server-renderer.cjs +760 -1
- package/dist/lib/dom/server-renderer.js +1 -1
- package/dist/lib/dom/server-renderer.mjs +745 -1
- package/dist/lib/dom/types.cjs +18 -1
- package/dist/lib/dom/utils.cjs +381 -1
- package/dist/lib/dom/utils.d.ts +1 -88
- package/dist/lib/dom/utils.d.ts.map +1 -1
- package/dist/lib/dom/utils.js +2 -99
- package/dist/lib/dom/utils.mjs +361 -1
- package/dist/lib/dom/versions.cjs +60 -1
- package/dist/lib/dom/versions.d.ts +3 -3
- package/dist/lib/dom/versions.d.ts.map +1 -1
- package/dist/lib/dom/versions.js +17 -5
- package/dist/lib/dom/versions.mjs +37 -1
- package/dist/lib/element/attributes.cjs +19 -1
- package/dist/lib/element/attributes.d.ts +7 -7
- package/dist/lib/element/attributes.js +1 -1
- package/dist/lib/element/attributes.mjs +1 -0
- package/dist/lib/element/index.cjs +19 -1
- package/dist/lib/element/index.mjs +1 -0
- package/dist/lib/element/types.cjs +18 -1
- package/dist/lib/element/types.d.ts +4 -5
- package/dist/lib/element/types.d.ts.map +1 -1
- package/dist/lib/element/types.js +0 -1
- package/dist/lib/flow/class-map.cjs +38 -1
- package/dist/lib/flow/class-map.d.ts +3 -3
- package/dist/lib/flow/class-map.js +3 -3
- package/dist/lib/flow/class-map.mjs +17 -1
- package/dist/lib/flow/client-only.cjs +57 -1
- package/dist/lib/flow/client-only.d.ts +4 -4
- package/dist/lib/flow/client-only.js +4 -4
- package/dist/lib/flow/client-only.mjs +36 -1
- package/dist/lib/flow/dynamic.cjs +53 -1
- package/dist/lib/flow/dynamic.d.ts +2 -2
- package/dist/lib/flow/dynamic.js +4 -4
- package/dist/lib/flow/dynamic.mjs +32 -1
- package/dist/lib/flow/error-boundary.cjs +43 -1
- package/dist/lib/flow/error-boundary.d.ts +2 -2
- package/dist/lib/flow/error-boundary.js +4 -4
- package/dist/lib/flow/error-boundary.mjs +22 -1
- package/dist/lib/flow/for.cjs +66 -1
- package/dist/lib/flow/for.d.ts +9 -9
- package/dist/lib/flow/for.d.ts.map +1 -1
- package/dist/lib/flow/for.js +2 -2
- package/dist/lib/flow/for.mjs +45 -1
- package/dist/lib/flow/index-flow.cjs +67 -1
- package/dist/lib/flow/index-flow.d.ts +5 -5
- package/dist/lib/flow/index-flow.js +4 -4
- package/dist/lib/flow/index-flow.mjs +46 -1
- package/dist/lib/flow/index.cjs +805 -1
- package/dist/lib/flow/index.mjs +792 -1
- package/dist/lib/flow/portal.cjs +538 -1
- package/dist/lib/flow/portal.mjs +519 -1
- package/dist/lib/flow/show.cjs +81 -1
- package/dist/lib/flow/show.d.ts +1 -1
- package/dist/lib/flow/show.d.ts.map +1 -1
- package/dist/lib/flow/show.js +12 -118
- package/dist/lib/flow/show.mjs +67 -1
- package/dist/lib/flow/style-map.cjs +43 -1
- package/dist/lib/flow/style-map.d.ts +3 -3
- package/dist/lib/flow/style-map.js +4 -4
- package/dist/lib/flow/style-map.mjs +22 -1
- package/dist/lib/flow/switch.cjs +91 -1
- package/dist/lib/flow/switch.d.ts +3 -3
- package/dist/lib/flow/switch.d.ts.map +1 -1
- package/dist/lib/flow/switch.js +7 -7
- package/dist/lib/flow/switch.mjs +70 -1
- package/dist/lib/flow/utils.cjs +44 -1
- package/dist/lib/flow/utils.d.ts +1 -1
- package/dist/lib/flow/utils.js +1 -1
- package/dist/lib/flow/utils.mjs +23 -1
- package/dist/lib/html.cjs +32 -1
- package/dist/lib/html.d.ts +6 -6
- package/dist/lib/html.js +5 -5
- package/dist/lib/html.mjs +11 -1
- package/dist/lib/index.cjs +3104 -1
- package/dist/lib/index.d.ts +0 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +2 -3
- package/dist/lib/index.mjs +3097 -1
- package/dist/lib/shared/types.cjs +18 -1
- package/dist/lib/shared/types.d.ts +8 -8
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/version.generated.cjs +26 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +2 -2
- package/dist/version.generated.mjs +5 -1
- package/package.json +7 -6
- package/dist/lib/jsx/control-flow/flow.cjs +0 -1
- package/dist/lib/jsx/control-flow/flow.d.ts +0 -2
- package/dist/lib/jsx/control-flow/flow.d.ts.map +0 -1
- package/dist/lib/jsx/control-flow/flow.js +0 -627
- package/dist/lib/jsx/control-flow/flow.mjs +0 -0
- package/dist/lib/jsx/control-flow/for-flow.cjs +0 -1
- package/dist/lib/jsx/control-flow/for-flow.d.ts +0 -2
- package/dist/lib/jsx/control-flow/for-flow.d.ts.map +0 -1
- package/dist/lib/jsx/control-flow/for-flow.js +0 -1
- package/dist/lib/jsx/control-flow/for-flow.mjs +0 -0
package/dist/lib/dom/runtime.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Core Reactive DOM Runtime
|
|
3
2
|
* @module @fluixi/dom/runtime
|
|
4
3
|
*
|
|
5
4
|
* Fine-grained reactive DOM manipulation utilities inspired by SolidJS.
|
|
6
5
|
* Key design: Event delegation uses `$$eventname` properties on DOM nodes
|
|
7
|
-
* (the SolidJS pattern)
|
|
6
|
+
* (the SolidJS pattern): simpler and more reliable than Map-based lookup.
|
|
8
7
|
*
|
|
9
8
|
* Critical fixes applied:
|
|
10
9
|
* - insertContent array branch: nested signal content cleanup via onCleanup
|
|
@@ -18,8 +17,8 @@ import { stampVersions, warnOnVersionSkew } from './versions.js';
|
|
|
18
17
|
import { isComponent } from '../component/modifiers.js';
|
|
19
18
|
// Reactive primitives come straight from the signal core (the globalThis-singleton graph makes
|
|
20
19
|
// this duplication-safe). The integration bridge is only for framework-provided glue that is
|
|
21
|
-
//
|
|
22
|
-
import { batch, createEffect, createRenderEffect, createRoot, onCleanup, readChildren } from '@fluixi/reactive/signal';
|
|
20
|
+
// not in the reactive core (e.g. createComponent).
|
|
21
|
+
import { batch, createEffect, createRenderEffect, createRoot, onCleanup, readChildren, reportReactiveBinding, reportReactiveDirective, untrack } from '@fluixi/reactive/signal';
|
|
23
22
|
import { isServer, getServerNodes, isDomNode, isDomElement, isDomText, } from './server/host.js';
|
|
24
23
|
import { hydration, getHydrationWalker, isAdopted, runHydrationAt, ascendPast, } from './hydration-state.js';
|
|
25
24
|
function getElementFingerprint(el) {
|
|
@@ -71,24 +70,34 @@ export const generateID = () => {
|
|
|
71
70
|
// ============================================================================
|
|
72
71
|
const EMPTY_OBJ = {};
|
|
73
72
|
const EMPTY_ARR = [];
|
|
73
|
+
/** Where devtools leaves the source position of a hole. Absent in a production build. */
|
|
74
|
+
const HOLE_TAG = '__fx_at';
|
|
75
|
+
/** Where an element's own source position goes, for devtools to read back. */
|
|
76
|
+
const LOC_TAG = '__fx_loc';
|
|
77
|
+
/** Only ever called with a position when the compiler recorded one. */
|
|
78
|
+
function located(element, at) {
|
|
79
|
+
if (at)
|
|
80
|
+
element[LOC_TAG] = at;
|
|
81
|
+
return element;
|
|
82
|
+
}
|
|
74
83
|
// Template cache
|
|
75
84
|
const templateCache = new Map();
|
|
76
85
|
// ============================================================================
|
|
77
|
-
// Event Delegation
|
|
86
|
+
// Event Delegation. SolidJS $$eventname property pattern
|
|
78
87
|
//
|
|
79
88
|
// Handlers are stored directly on DOM nodes as `node.$$click = handler`.
|
|
80
|
-
// The capture listener reads `node.$$click` directly
|
|
89
|
+
// The capture listener reads `node.$$click` directly, no Map lookups needed.
|
|
81
90
|
// This approach correctly handles e.preventDefault() and stopPropagation.
|
|
82
91
|
// ============================================================================
|
|
83
92
|
const delegatedSet = new Set();
|
|
84
|
-
// Events that don't bubble
|
|
93
|
+
// Events that don't bubble: must use direct addEventListener
|
|
85
94
|
const NON_DELEGATED = new Set(['scroll', 'focus', 'blur', 'load', 'error', 'resize', 'mouseenter', 'mouseleave', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture']);
|
|
86
95
|
/**
|
|
87
96
|
* Register global delegated event listener for the given event names.
|
|
88
97
|
* Stores handlers as `element.$$eventname = handler` properties.
|
|
89
98
|
*/
|
|
90
99
|
export function delegateEvents(eventNames) {
|
|
91
|
-
if (isServer)
|
|
100
|
+
if (isServer())
|
|
92
101
|
return; // no document to attach to; events are inert in SSR
|
|
93
102
|
for (const name of eventNames) {
|
|
94
103
|
if (delegatedSet.has(name) || NON_DELEGATED.has(name))
|
|
@@ -115,12 +124,12 @@ function delegatedEventHandler(e) {
|
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
/**
|
|
118
|
-
* Add a delegated event handler
|
|
127
|
+
* Add a delegated event handler: stores as $$eventname property
|
|
119
128
|
*/
|
|
120
129
|
export function addDelegatedEventListener(element, name, handler) {
|
|
121
130
|
element[`$$${name}`] = handler;
|
|
122
131
|
if (NON_DELEGATED.has(name)) {
|
|
123
|
-
// Non-bubbling event (focus/blur/pointerenter
|
|
132
|
+
// Non-bubbling event (focus/blur/pointerenter/...): document delegation can't observe it, so
|
|
124
133
|
// attach a direct listener once. A stable forwarder reads $$name so updating the handler
|
|
125
134
|
// doesn't churn addEventListener/removeEventListener.
|
|
126
135
|
const key = `__fx_${name}`;
|
|
@@ -163,9 +172,9 @@ export function isFunction(value) {
|
|
|
163
172
|
return typeof value === 'function';
|
|
164
173
|
}
|
|
165
174
|
function createTextNode(value, anchor = false) {
|
|
166
|
-
if (isServer)
|
|
175
|
+
if (isServer())
|
|
167
176
|
return getServerNodes().createText(value);
|
|
168
|
-
// Anchors/placeholders (insert markers) are client-only insertion points
|
|
177
|
+
// Anchors/placeholders (insert markers) are client-only insertion points, the server
|
|
169
178
|
// serialized nothing for them, so they must never adopt a server text node. Only real
|
|
170
179
|
// content goes through the hydration walker.
|
|
171
180
|
if (hydration.active && !anchor) {
|
|
@@ -176,7 +185,7 @@ function createTextNode(value, anchor = false) {
|
|
|
176
185
|
return document.createTextNode(value);
|
|
177
186
|
}
|
|
178
187
|
function createMarker(value) {
|
|
179
|
-
if (isServer)
|
|
188
|
+
if (isServer())
|
|
180
189
|
return getServerNodes().createComment(value);
|
|
181
190
|
if (hydration.active) {
|
|
182
191
|
const node = getHydrationWalker()?.hydrateMarker(value);
|
|
@@ -185,18 +194,34 @@ function createMarker(value) {
|
|
|
185
194
|
}
|
|
186
195
|
return document.createComment(value);
|
|
187
196
|
}
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Create an element, or adopt the server's during hydration.
|
|
199
|
+
*
|
|
200
|
+
* One of the calls the compiler emits, so its three modes are the whole of how a
|
|
201
|
+
* template becomes DOM. Server rendering routes to the realm's node factory and returns
|
|
202
|
+
* a string-serializable stand-in rather than a real element. Hydration asks the walker
|
|
203
|
+
* for the next element the server wrote and reuses it. Otherwise it creates one.
|
|
204
|
+
*
|
|
205
|
+
* `at` is a dev-only source position, stamped onto the node for devtools. A production
|
|
206
|
+
* build passes none and carries no marker.
|
|
207
|
+
*/
|
|
208
|
+
export function createNativeElement(tag, isSVG = false, at) {
|
|
209
|
+
if (isServer())
|
|
190
210
|
return getServerNodes().createElement(tag, isSVG);
|
|
191
211
|
if (hydration.active) {
|
|
192
212
|
const node = getHydrationWalker()?.hydrateElement(tag);
|
|
193
213
|
if (node)
|
|
194
|
-
return node;
|
|
214
|
+
return at ? located(node, at) : node;
|
|
195
215
|
}
|
|
196
|
-
return isSVG
|
|
197
|
-
? document.createElementNS(SVG_NAMESPACE, tag)
|
|
198
|
-
: document.createElement(tag);
|
|
216
|
+
return located(isSVG ? document.createElementNS(SVG_NAMESPACE, tag) : document.createElement(tag), at);
|
|
199
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Parse static template HTML once and cache it by that string.
|
|
220
|
+
*
|
|
221
|
+
* Every clone of a template comes from one parse, which is most of why a compiled
|
|
222
|
+
* template is cheap. The cache is keyed by the markup rather than by identity, so two
|
|
223
|
+
* modules emitting the same HTML share the parse.
|
|
224
|
+
*/
|
|
200
225
|
export function createTemplate(html, isSVG = false) {
|
|
201
226
|
const cached = templateCache.get(html);
|
|
202
227
|
if (cached)
|
|
@@ -216,17 +241,17 @@ export function createTemplate(html, isSVG = false) {
|
|
|
216
241
|
* needs it.
|
|
217
242
|
*
|
|
218
243
|
* The three cases mirror {@link createNativeElement}, which is the point: a template has
|
|
219
|
-
* to behave like the elements it replaces, or it works only in client-rendered apps
|
|
244
|
+
* to behave like the elements it replaces, or it works only in client-rendered apps,
|
|
220
245
|
* which is exactly how it failed before this existed.
|
|
221
246
|
*
|
|
222
|
-
* - **server
|
|
247
|
+
* - **server**: carried as text and written out verbatim. Parsing it into elements
|
|
223
248
|
* only to serialize them straight back would be pure cost.
|
|
224
|
-
* - **hydrating
|
|
249
|
+
* - **hydrating**: adopt the server's existing subtree and skip past it. The subtree
|
|
225
250
|
* is inert, so there is nothing inside to wire up.
|
|
226
|
-
* - **client
|
|
251
|
+
* - **client**: parse once (cached by HTML) and clone per instance.
|
|
227
252
|
*/
|
|
228
|
-
export function templateNode(html, tag, holes = false, isSVG = false) {
|
|
229
|
-
if (isServer) {
|
|
253
|
+
export function templateNode(html, tag, holes = false, isSVG = false, at) {
|
|
254
|
+
if (isServer()) {
|
|
230
255
|
// With holes the server needs real nodes for the hole content to be inserted into;
|
|
231
256
|
// without them the string can be written out untouched, which is strictly cheaper.
|
|
232
257
|
const nodes = getServerNodes();
|
|
@@ -237,16 +262,38 @@ export function templateNode(html, tag, holes = false, isSVG = false) {
|
|
|
237
262
|
// through the adopted tree, whose structure matches the template by construction.
|
|
238
263
|
const node = getHydrationWalker()?.hydrateStatic(tag);
|
|
239
264
|
if (node)
|
|
240
|
-
return node;
|
|
265
|
+
return stampPositions(node, at);
|
|
241
266
|
}
|
|
242
|
-
return cloneTemplate(createTemplate(html, isSVG), isSVG).firstChild;
|
|
267
|
+
return stampPositions(cloneTemplate(createTemplate(html, isSVG), isSVG).firstChild, at);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Write each element's own source position onto it.
|
|
271
|
+
*
|
|
272
|
+
* The whole template is one string, so a source map traces every element in it back to the
|
|
273
|
+
* line the root opens at: devtools would name the component and nothing finer. This is the
|
|
274
|
+
* one moment the two line up: the tree holds exactly the elements the compiler listed, in
|
|
275
|
+
* the same order, because holes are filled after this returns.
|
|
276
|
+
*
|
|
277
|
+
* Only reached when the compiler emitted positions, which it does for a dev build.
|
|
278
|
+
*/
|
|
279
|
+
function stampPositions(node, at) {
|
|
280
|
+
const root = node;
|
|
281
|
+
if (!at || typeof root?.querySelectorAll !== 'function')
|
|
282
|
+
return node;
|
|
283
|
+
const all = [root, ...root.querySelectorAll('*')];
|
|
284
|
+
for (let i = 0; i < all.length && i * 2 + 1 < at.pos.length; i++) {
|
|
285
|
+
const line = at.pos[i * 2];
|
|
286
|
+
if (line)
|
|
287
|
+
all[i][LOC_TAG] = { file: at.file, line, column: at.pos[i * 2 + 1] };
|
|
288
|
+
}
|
|
289
|
+
return node;
|
|
243
290
|
}
|
|
244
291
|
/**
|
|
245
292
|
* Evaluate a hole's content with the hydration cursor placed at what the server rendered
|
|
246
293
|
* for *that hole*.
|
|
247
294
|
*
|
|
248
295
|
* Without this, only the hole's top level adopts. `templateNode` moves the cursor past the
|
|
249
|
-
* whole template
|
|
296
|
+
* whole template, the subtree is inert, so there is nothing in it to walk, which leaves
|
|
250
297
|
* the cursor in the wrong place for anything the hole builds internally. A component in a
|
|
251
298
|
* hole would then recreate its own elements: no duplication, but every server node thrown
|
|
252
299
|
* away, which is most of what hydration is for.
|
|
@@ -257,22 +304,35 @@ export function holeScope(start, produce) {
|
|
|
257
304
|
if (!hydration.active)
|
|
258
305
|
return produce();
|
|
259
306
|
// `produce` is called with the cursor already placed, because the value may be built
|
|
260
|
-
// eagerly
|
|
307
|
+
// eagerly: `props.children` is a getter, and reading it constructs its nodes there and
|
|
261
308
|
// then. Wrapping the result instead would be too late: the DOM would already exist,
|
|
262
309
|
// built against whatever cursor happened to be current, and the server's nodes would be
|
|
263
310
|
// left behind as duplicates.
|
|
264
311
|
const value = runHydrationAt(start.nextSibling, produce);
|
|
265
312
|
// A memo or accessor builds nothing until it is read, which happens inside `insert`'s
|
|
266
|
-
// effect
|
|
313
|
+
// effect, so it needs the cursor placed again at that moment. The guard matters: once
|
|
267
314
|
// hydration is over, later updates must render normally rather than re-enter it.
|
|
268
315
|
if (typeof value === 'function') {
|
|
269
316
|
const accessor = value;
|
|
270
|
-
|
|
317
|
+
const wrapped = (...args) => hydration.active
|
|
271
318
|
? runHydrationAt(start.nextSibling, () => accessor(...args))
|
|
272
|
-
: accessor(...args)
|
|
319
|
+
: accessor(...args);
|
|
320
|
+
return carryHoleTag(accessor, wrapped);
|
|
273
321
|
}
|
|
274
322
|
return value;
|
|
275
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* The compiler tags a hole's accessor with where it was written, and devtools reads the tag
|
|
326
|
+
* off whatever finally reaches `insert`. Under hydration that is this wrapper, not the
|
|
327
|
+
* accessor, so the tag has to come along: otherwise the binding reports the frame it was
|
|
328
|
+
* created from, which is a line in this file.
|
|
329
|
+
*/
|
|
330
|
+
function carryHoleTag(from, to) {
|
|
331
|
+
const tag = from[HOLE_TAG];
|
|
332
|
+
if (tag !== undefined)
|
|
333
|
+
to[HOLE_TAG] = tag;
|
|
334
|
+
return to;
|
|
335
|
+
}
|
|
276
336
|
/**
|
|
277
337
|
* The closing marker for a hole opened at `start`.
|
|
278
338
|
*
|
|
@@ -293,7 +353,7 @@ export function holeEnd(start) {
|
|
|
293
353
|
*
|
|
294
354
|
* On a fresh clone the markers are adjacent and this is empty, so `insert` builds the
|
|
295
355
|
* content. When hydrating, the server's rendered content sits between them and is handed
|
|
296
|
-
* back
|
|
356
|
+
* back: `insertExpression` then reuses those nodes (setting `.data` on a text node
|
|
297
357
|
* rather than creating one), which is what adoption means here.
|
|
298
358
|
*
|
|
299
359
|
* Paired markers are what make this exact: a hole's content is *by definition* the nodes
|
|
@@ -331,12 +391,12 @@ export function fnName(fn) {
|
|
|
331
391
|
return fn._name || fn.name || 'anonymous';
|
|
332
392
|
}
|
|
333
393
|
/**
|
|
334
|
-
* iterateFn
|
|
394
|
+
* iterateFn: resolves a value by calling through plain function wrappers.
|
|
335
395
|
*
|
|
336
396
|
* Must stop at signals/memos/resources ($SIGNAL marker) and
|
|
337
397
|
* component thunks (fluixi-component marker). Without this:
|
|
338
398
|
* - components returning memos get unwrapped to static values, losing reactivity
|
|
339
|
-
* - lazy component createMemo re-triggers C(props)
|
|
399
|
+
* - lazy component createMemo re-triggers C(props) -> infinite recursion
|
|
340
400
|
*/
|
|
341
401
|
export function iterateFn(item) {
|
|
342
402
|
// Stop immediately if it's a signal, memo, or resource
|
|
@@ -347,7 +407,7 @@ export function iterateFn(item) {
|
|
|
347
407
|
return item;
|
|
348
408
|
if (typeof item !== 'function')
|
|
349
409
|
return item;
|
|
350
|
-
// Unwrap plain function wrappers
|
|
410
|
+
// Unwrap plain function wrappers: stop at signals and component thunks
|
|
351
411
|
let content = item();
|
|
352
412
|
while (typeof content === 'function' &&
|
|
353
413
|
!isSignal(content) &&
|
|
@@ -365,13 +425,18 @@ const MAX_INSERT_DEPTH = 200;
|
|
|
365
425
|
// _createInsertEffect
|
|
366
426
|
//
|
|
367
427
|
// Creates a reactive render effect for DOM insertion.
|
|
368
|
-
// Uses a componentCache to ensure component thunks are called at most once
|
|
428
|
+
// Uses a componentCache to ensure component thunks are called at most once:
|
|
369
429
|
// same thunk function reference always returns the same output across re-runs.
|
|
370
430
|
// ============================================================================
|
|
371
431
|
function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCurrent) {
|
|
372
432
|
let current = initialCurrent;
|
|
373
433
|
const componentCache = new Map();
|
|
374
434
|
return createRenderEffect(() => {
|
|
435
|
+
// The accessor, never its result: it carries the compiler's hole position, and calling it
|
|
436
|
+
// again here builds the dom twice, during hydration, a duplicated subtree.
|
|
437
|
+
//
|
|
438
|
+
// Every run, because what this effect writes into moves as the value does.
|
|
439
|
+
reportReactiveBinding(marker ?? parent, 'content', undefined, getAccessor);
|
|
375
440
|
let value;
|
|
376
441
|
// ── 0. If accessor itself is a component thunk (not a signal/memo), call it once via cache ──
|
|
377
442
|
// must check !isSignal to avoid caching reactive values that are also
|
|
@@ -441,9 +506,22 @@ function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCur
|
|
|
441
506
|
// ============================================================================
|
|
442
507
|
// insert
|
|
443
508
|
// ============================================================================
|
|
509
|
+
/**
|
|
510
|
+
* Place content in a parent, and keep it current when the content is reactive.
|
|
511
|
+
*
|
|
512
|
+
* The other half of the compiler's output. `accessor` may be a value or a function: a
|
|
513
|
+
* function is read inside a render effect, so the position updates on its own when
|
|
514
|
+
* anything it read changes. That is where fine-grained updating actually happens, since
|
|
515
|
+
* only this position is touched rather than the subtree around it.
|
|
516
|
+
*
|
|
517
|
+
* `marker` is the node the content sits before, which is what makes several holes in one
|
|
518
|
+
* parent independent. `init` is what the position already holds, empty on a fresh clone
|
|
519
|
+
* and the server's nodes when hydrating, so hydration reconciles against them instead of
|
|
520
|
+
* building a second copy.
|
|
521
|
+
*/
|
|
444
522
|
export function insert(parent, accessor, marker, init) {
|
|
445
523
|
if (_insertDepth > MAX_INSERT_DEPTH) {
|
|
446
|
-
console.error('[insert] Exceeded max call depth
|
|
524
|
+
console.error('[insert] Exceeded max call depth, possible infinite loop.');
|
|
447
525
|
return () => null;
|
|
448
526
|
}
|
|
449
527
|
if (marker !== undefined && !marker) {
|
|
@@ -462,7 +540,7 @@ export function insert(parent, accessor, marker, init) {
|
|
|
462
540
|
if (typeof accessor !== 'function' && !isReactiveArray(accessor)) {
|
|
463
541
|
_insertDepth++;
|
|
464
542
|
try {
|
|
465
|
-
// `current
|
|
543
|
+
// `current`, not null. The caller passes what is already in this slot, and for a
|
|
466
544
|
// hydrating template hole that is the server's own nodes (`holeContent`). Dropping it
|
|
467
545
|
// meant a static value never adopted: insertExpression saw an empty slot and appended
|
|
468
546
|
// a second copy beside the server's, so `{cmd}` rendered the string twice.
|
|
@@ -547,14 +625,14 @@ export function insertExpression(parent, value, marker, current, componentCache)
|
|
|
547
625
|
}
|
|
548
626
|
else if (!isAdopted(parent, node)) {
|
|
549
627
|
// during hydration createTextNode reuses the in-place server text node and
|
|
550
|
-
// already advanced the cursor
|
|
628
|
+
// already advanced the cursor: don't re-insert (that would reorder).
|
|
551
629
|
insertBefore(parent, node, marker);
|
|
552
630
|
}
|
|
553
631
|
return node;
|
|
554
632
|
}
|
|
555
633
|
if (isDomNode(value)) {
|
|
556
634
|
// Hydration: the node was already produced from the server DOM (reused by
|
|
557
|
-
// createNativeElement) and sits in `parent` in place
|
|
635
|
+
// createNativeElement) and sits in `parent` in place: adopt it without
|
|
558
636
|
// re-inserting, and ascend the cursor past its (already-walked) subtree.
|
|
559
637
|
if (isAdopted(parent, value)) {
|
|
560
638
|
ascendPast(value);
|
|
@@ -598,13 +676,13 @@ export function insertExpression(parent, value, marker, current, componentCache)
|
|
|
598
676
|
const reactiveSlots = [];
|
|
599
677
|
flattenArray(value, array, parent, marker, true, componentCache, reactiveSlots);
|
|
600
678
|
const reconciled = reconcileArrays(parent, currentArray, array, marker);
|
|
601
|
-
// Markers are now in the DOM
|
|
679
|
+
// Markers are now in the DOM: wire a per-slot reactive insert. insert() sets up a nested
|
|
602
680
|
// render effect that re-inserts the member's content before its marker on change, so a bare
|
|
603
681
|
// control-flow component return (<Show>/<Switch>/<For>) updates in place.
|
|
604
682
|
//
|
|
605
683
|
// The per-slot content lives OUTSIDE the reconcile array (only the marker is tracked), so if
|
|
606
|
-
// this enclosing effect re-runs with a shorter array
|
|
607
|
-
// resolved to a memo (Provider -> component)
|
|
684
|
+
// this enclosing effect re-runs with a shorter array: e.g. a <For> item dropped, or its child
|
|
685
|
+
// resolved to a memo (Provider -> component): reconcile removes the marker but would orphan the
|
|
608
686
|
// content. Register a cleanup that removes the slot's nodes when its effect is torn down. This
|
|
609
687
|
// only fires when the enclosing effect re-runs (the array itself changed); an in-place update
|
|
610
688
|
// (e.g. <Show> toggling) never re-runs this effect, so it keeps updating in place.
|
|
@@ -630,12 +708,12 @@ export function insertExpression(parent, value, marker, current, componentCache)
|
|
|
630
708
|
}
|
|
631
709
|
return cleanChildren(parent, current, marker);
|
|
632
710
|
}
|
|
633
|
-
// flattenArray
|
|
711
|
+
// flattenArray: flatten a children array to DOM nodes. A signal whose result is a
|
|
634
712
|
// component thunk (e.g. a router memo returning a SuspenseThunk) is recursed through
|
|
635
713
|
// flattenArray, not read inline, so the thunk goes through componentCache and stays
|
|
636
714
|
// stable across navigations.
|
|
637
715
|
function flattenArray(arr, result, parent, marker, track = true, componentCache,
|
|
638
|
-
// When provided, a reactive member is
|
|
716
|
+
// When provided, a reactive member is not read inline (which would subscribe the enclosing
|
|
639
717
|
// render effect and re-render the whole subtree on change). Instead it emits a placeholder
|
|
640
718
|
// marker and records itself here; the array caller wires a fine-grained insert() per slot
|
|
641
719
|
// after the markers are placed. This is what makes a bare control-flow component return
|
|
@@ -656,14 +734,14 @@ reactiveSlots) {
|
|
|
656
734
|
if (typeof item === 'function') {
|
|
657
735
|
let resolved;
|
|
658
736
|
if (isComponent(item) && !isSignal(item)) {
|
|
659
|
-
// Pure component thunk (e.g. () => <div>)
|
|
737
|
+
// Pure component thunk (e.g. () => <div>): call exactly once and cache so
|
|
660
738
|
// re-runs of the enclosing render effect return the same DOM nodes (keeps
|
|
661
739
|
// focus etc. stable across navigations).
|
|
662
740
|
// must check !isSignal. A memo stamped as a component (e.g. an
|
|
663
741
|
// Outlet/Routes/Suspense output returned via $component/asComponent) is BOTH
|
|
664
742
|
// isComponent AND isSignal; caching it would freeze its first value (so a
|
|
665
|
-
// boundary that later switches from a single node to an array
|
|
666
|
-
// children
|
|
743
|
+
// boundary that later switches from a single node to an array: fallback ->
|
|
744
|
+
// children: would never update). Read those reactively in the else branch.
|
|
667
745
|
if (!componentCache)
|
|
668
746
|
componentCache = new Map();
|
|
669
747
|
if (!componentCache.has(item))
|
|
@@ -671,17 +749,17 @@ reactiveSlots) {
|
|
|
671
749
|
resolved = componentCache.get(item);
|
|
672
750
|
}
|
|
673
751
|
else {
|
|
674
|
-
// Signal/memo (or memo-stamped component)
|
|
752
|
+
// Signal/memo (or memo-stamped component): read to track reactivity.
|
|
675
753
|
resolved = item();
|
|
676
754
|
}
|
|
677
755
|
// Unwrap plain wrappers; stop at signals and component thunks.
|
|
678
|
-
// Cache calls by function reference
|
|
756
|
+
// Cache calls by function reference, a plain function returned by a
|
|
679
757
|
// signal/memo (e.g. `createMemo(() => cond ? () => <JSX> : ...)`) is a
|
|
680
758
|
// stable reference as long as the memo doesn't re-evaluate. Calling it
|
|
681
759
|
// once and caching the result means re-runs of the outer render effect
|
|
682
760
|
// (triggered by unrelated signals like `activeLevel()`) hit the cache and
|
|
683
761
|
// return the same DOM nodes. reconcileArrays then sees identical references
|
|
684
|
-
// and skips the slot
|
|
762
|
+
// and skips the slot, the focused <input> is never removed.
|
|
685
763
|
while (typeof resolved === 'function' &&
|
|
686
764
|
!isPromise(resolved) &&
|
|
687
765
|
!isSignal(resolved) &&
|
|
@@ -705,8 +783,8 @@ reactiveSlots) {
|
|
|
705
783
|
reactiveSlots.push({ marker: slot, sig: resolved });
|
|
706
784
|
continue;
|
|
707
785
|
}
|
|
708
|
-
// Read the signal
|
|
709
|
-
// Use flattenArray recursively (
|
|
786
|
+
// Read the signal: tracked inside the enclosing render effect.
|
|
787
|
+
// Use flattenArray recursively (not normalizeValue) so
|
|
710
788
|
// component thunks inside the result go through componentCache.
|
|
711
789
|
let val = resolved();
|
|
712
790
|
while (typeof val === 'function' && isSignal(val))
|
|
@@ -720,7 +798,7 @@ reactiveSlots) {
|
|
|
720
798
|
flattenArray([val], result, parent, marker, track, componentCache);
|
|
721
799
|
continue;
|
|
722
800
|
}
|
|
723
|
-
// Other function type
|
|
801
|
+
// Other function type: skip
|
|
724
802
|
continue;
|
|
725
803
|
}
|
|
726
804
|
// resolved is not a function
|
|
@@ -786,15 +864,15 @@ reactiveSlots) {
|
|
|
786
864
|
// ============================================================================
|
|
787
865
|
function reconcileArrays(parent, a, b, marker) {
|
|
788
866
|
// Pre-process: clone any node already owned by a different parent.
|
|
789
|
-
// The Babel plugin hoists static elements to module-level
|
|
867
|
+
// The Babel plugin hoists static elements to module-level, the same node
|
|
790
868
|
// can appear in `b` for multiple parents simultaneously. Clone to avoid
|
|
791
869
|
// stealing it from its current owner; future reconciles track the clone.
|
|
792
870
|
//
|
|
793
|
-
// A DocumentFragment is
|
|
871
|
+
// A DocumentFragment is not such an owner. `cloneTemplate` hands back the clone still
|
|
794
872
|
// sitting in its fragment, so a template-cloned element arrives here with a non-null
|
|
795
873
|
// parent that nothing else refers to. Cloning it again threw away the node whose props
|
|
796
|
-
// had just been applied
|
|
797
|
-
// properties
|
|
874
|
+
// had just been applied: `cloneNode` copies attributes but not the event-handler
|
|
875
|
+
// properties, so an element built from a template lost its handlers the moment it was
|
|
798
876
|
// inserted as part of an array. Moving it out of the fragment is exactly right.
|
|
799
877
|
for (let i = 0; i < b.length; i++) {
|
|
800
878
|
const owner = b[i].parentNode;
|
|
@@ -811,18 +889,18 @@ function reconcileArrays(parent, a, b, marker) {
|
|
|
811
889
|
? a[aEnd - 1].nextSibling ?? undefined
|
|
812
890
|
: marker, map = null;
|
|
813
891
|
while (aStart < aEnd || bStart < bEnd) {
|
|
814
|
-
// 1. Common prefix
|
|
892
|
+
// 1. Common prefix: already in the right place, skip.
|
|
815
893
|
if (a[aStart] === b[bStart]) {
|
|
816
894
|
aStart++;
|
|
817
895
|
bStart++;
|
|
818
896
|
continue;
|
|
819
897
|
}
|
|
820
|
-
// 2. Common suffix
|
|
898
|
+
// 2. Common suffix: already in the right place, shrink window.
|
|
821
899
|
while (aEnd > aStart && bEnd > bStart && a[aEnd - 1] === b[bEnd - 1]) {
|
|
822
900
|
aEnd--;
|
|
823
901
|
bEnd--;
|
|
824
902
|
}
|
|
825
|
-
// 3. Old list exhausted
|
|
903
|
+
// 3. Old list exhausted: insert remaining new nodes.
|
|
826
904
|
if (aEnd === aStart) {
|
|
827
905
|
const node = bEnd < bLength
|
|
828
906
|
? bStart
|
|
@@ -832,7 +910,7 @@ function reconcileArrays(parent, a, b, marker) {
|
|
|
832
910
|
while (bStart < bEnd)
|
|
833
911
|
insertBefore(parent, b[bStart++], node);
|
|
834
912
|
}
|
|
835
|
-
// 4. New list exhausted
|
|
913
|
+
// 4. New list exhausted: remove remaining old nodes.
|
|
836
914
|
else if (bEnd === bStart) {
|
|
837
915
|
while (aStart < aEnd) {
|
|
838
916
|
if ((!map || !map.has(a[aStart])) && a[aStart].parentNode === parent)
|
|
@@ -870,7 +948,7 @@ function reconcileArrays(parent, a, b, marker) {
|
|
|
870
948
|
insertBefore(parent, b[bStart++], node);
|
|
871
949
|
}
|
|
872
950
|
else {
|
|
873
|
-
// Guard replaceChild
|
|
951
|
+
// Guard replaceChild, if a[aStart] was moved out by another effect, insert instead.
|
|
874
952
|
if (a[aStart].parentNode === parent) {
|
|
875
953
|
parent.replaceChild(b[bStart++], a[aStart++]);
|
|
876
954
|
}
|
|
@@ -965,10 +1043,41 @@ export function addNativeEventListener(element, name, value) {
|
|
|
965
1043
|
element.addEventListener(name, handler, options);
|
|
966
1044
|
return () => element.removeEventListener(name, handler, options);
|
|
967
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Name each key of a class or style object as the directive it came from.
|
|
1048
|
+
*
|
|
1049
|
+
* `class:a class:b` are merged by the compiler into one classList prop, so the keys are the
|
|
1050
|
+
* only record that two directives were written.
|
|
1051
|
+
*/
|
|
1052
|
+
/**
|
|
1053
|
+
* A directive the compiler had to name, because the runtime sees only what it lowers to.
|
|
1054
|
+
* `bind:value` arrives as an ordinary value and an input handler.
|
|
1055
|
+
*/
|
|
1056
|
+
function reportMarked(element, accessor) {
|
|
1057
|
+
if (!accessor || typeof accessor !== 'function')
|
|
1058
|
+
return;
|
|
1059
|
+
const written = accessor[HOLE_TAG]?.name;
|
|
1060
|
+
if (!written)
|
|
1061
|
+
return;
|
|
1062
|
+
const colon = written.indexOf(':');
|
|
1063
|
+
if (colon === -1)
|
|
1064
|
+
return;
|
|
1065
|
+
reportReactiveDirective(element, written.slice(0, colon), written.slice(colon + 1), accessor);
|
|
1066
|
+
}
|
|
1067
|
+
function reportKeys(element, kind, value) {
|
|
1068
|
+
const from = typeof value === 'function' ? untrack(() => value()) : value;
|
|
1069
|
+
if (!from || typeof from !== 'object')
|
|
1070
|
+
return;
|
|
1071
|
+
for (const key of Object.keys(from))
|
|
1072
|
+
reportReactiveDirective(element, kind, key);
|
|
1073
|
+
}
|
|
968
1074
|
export function applyUse(element, value) {
|
|
969
1075
|
const call = (dir, accessor) => {
|
|
970
|
-
if (typeof dir
|
|
971
|
-
|
|
1076
|
+
if (typeof dir !== 'function')
|
|
1077
|
+
return;
|
|
1078
|
+
// Named so a watcher can tell one directive from another. Nothing else reads it.
|
|
1079
|
+
reportReactiveDirective(element, 'use', dir.name || 'use', accessor);
|
|
1080
|
+
dir(element, accessor ?? (() => undefined));
|
|
972
1081
|
};
|
|
973
1082
|
if (typeof value === 'function') {
|
|
974
1083
|
call(value);
|
|
@@ -989,11 +1098,13 @@ export function applyUse(element, value) {
|
|
|
989
1098
|
}
|
|
990
1099
|
}
|
|
991
1100
|
export function setDynamicAttribute(element, name, accessor, isSVG = false) {
|
|
1101
|
+
reportMarked(element, accessor);
|
|
992
1102
|
if (typeof accessor !== 'function') {
|
|
993
1103
|
setAttribute(element, name, accessor, isSVG);
|
|
994
1104
|
return () => { };
|
|
995
1105
|
}
|
|
996
1106
|
return createRenderEffect(() => {
|
|
1107
|
+
reportReactiveBinding(element, 'attribute', name, accessor);
|
|
997
1108
|
const value = typeof accessor === 'function' ? accessor() : accessor;
|
|
998
1109
|
setAttribute(element, name, value, isSVG);
|
|
999
1110
|
});
|
|
@@ -1003,7 +1114,9 @@ export function setDynamicProperty(element, name, accessor) {
|
|
|
1003
1114
|
setProperty(element, name, accessor);
|
|
1004
1115
|
return () => { };
|
|
1005
1116
|
}
|
|
1117
|
+
reportMarked(element, accessor);
|
|
1006
1118
|
return createRenderEffect(() => {
|
|
1119
|
+
reportReactiveBinding(element, 'property', name, accessor);
|
|
1007
1120
|
const value = typeof accessor === 'function' ? accessor() : accessor;
|
|
1008
1121
|
setProperty(element, name, value);
|
|
1009
1122
|
});
|
|
@@ -1016,6 +1129,7 @@ export function setDynamicBoolAttribute(element, name, accessor) {
|
|
|
1016
1129
|
return () => { };
|
|
1017
1130
|
}
|
|
1018
1131
|
return createRenderEffect(() => {
|
|
1132
|
+
reportReactiveBinding(element, 'attribute', name, accessor);
|
|
1019
1133
|
const value = accessor();
|
|
1020
1134
|
setAttribute(element, name, !!value);
|
|
1021
1135
|
});
|
|
@@ -1029,9 +1143,12 @@ export function setClassName(element, value) {
|
|
|
1029
1143
|
return;
|
|
1030
1144
|
}
|
|
1031
1145
|
if (typeof value === 'function') {
|
|
1032
|
-
// A render effect, like every other attribute binding
|
|
1146
|
+
// A render effect, like every other attribute binding: `spread` already wraps
|
|
1033
1147
|
// class this way, so going through `applyProps` shouldn't schedule it later.
|
|
1034
|
-
createRenderEffect(() =>
|
|
1148
|
+
createRenderEffect(() => {
|
|
1149
|
+
reportReactiveBinding(element, 'attribute', 'class', value);
|
|
1150
|
+
setClassName(element, value());
|
|
1151
|
+
});
|
|
1035
1152
|
return;
|
|
1036
1153
|
}
|
|
1037
1154
|
if (typeof value === 'string') {
|
|
@@ -1055,7 +1172,7 @@ export function setClassName(element, value) {
|
|
|
1055
1172
|
element.setAttribute('class', String(value));
|
|
1056
1173
|
}
|
|
1057
1174
|
/**
|
|
1058
|
-
* Apply a `classList`-style map `{ 'a b': condition }
|
|
1175
|
+
* Apply a `classList`-style map `{ 'a b': condition }`, toggling each class on
|
|
1059
1176
|
* the element. A condition may be a value or a reactive accessor; when
|
|
1060
1177
|
* `disposers` is provided, accessor entries are wired with a render effect so
|
|
1061
1178
|
* they update fine-grained (Solid-style `classList`).
|
|
@@ -1105,6 +1222,19 @@ export function setStyle(element, _value) {
|
|
|
1105
1222
|
// ============================================================================
|
|
1106
1223
|
// spread
|
|
1107
1224
|
// ============================================================================
|
|
1225
|
+
/**
|
|
1226
|
+
* Apply a props object to an element, and re-apply what changes.
|
|
1227
|
+
*
|
|
1228
|
+
* Handles every prop kind the compiler can emit: attributes, properties, events,
|
|
1229
|
+
* `class`/`style` in their object and string forms, `ref` and `use:`. Each reactive prop
|
|
1230
|
+
* gets its own effect, so a change touches one attribute rather than re-running the
|
|
1231
|
+
* element.
|
|
1232
|
+
*
|
|
1233
|
+
* `prevProps` is what was applied last, which is how a removed key is cleared rather
|
|
1234
|
+
* than left behind. A getter-backed prop stays reactive: the value is read inside the
|
|
1235
|
+
* effect, not when the object was built, which is what keeps `splitProps` and a
|
|
1236
|
+
* polymorphic wrapper live.
|
|
1237
|
+
*/
|
|
1108
1238
|
export function spread(options) {
|
|
1109
1239
|
const { element, props, prevProps = EMPTY_OBJ, isSVG = false, skipChildren = false, } = options;
|
|
1110
1240
|
if (!element || !isDomElement(element)) {
|
|
@@ -1117,7 +1247,7 @@ export function spread(options) {
|
|
|
1117
1247
|
}
|
|
1118
1248
|
// Trust the element's real namespace over the compile-time isSVG flag: the codegen only marks
|
|
1119
1249
|
// some SVG elements (e.g. <svg>/<defs>) and misses nested ones (<linearGradient>, <stop>). For
|
|
1120
|
-
// an actually-SVG element, a key like `x1`
|
|
1250
|
+
// an actually-SVG element, a key like `x1` is `in element` (read-only SVGAnimatedLength), so the
|
|
1121
1251
|
// property branch below would throw "Cannot set property x1". Detecting SVG by namespaceURI keeps
|
|
1122
1252
|
// those on the setAttribute path. (Matters under hydration, where we adopt the real, correctly
|
|
1123
1253
|
// namespaced server node.)
|
|
@@ -1151,22 +1281,28 @@ export function spread(options) {
|
|
|
1151
1281
|
continue;
|
|
1152
1282
|
}
|
|
1153
1283
|
// Forced bindings: prop:x always assigns the property, attr:x always sets the
|
|
1154
|
-
// attribute, bool:x toggles a boolean attribute
|
|
1284
|
+
// attribute, bool:x toggles a boolean attribute, skipping the heuristic below.
|
|
1155
1285
|
if (key.startsWith('prop:')) {
|
|
1286
|
+
reportReactiveDirective(element, 'prop', key.slice(5), value);
|
|
1156
1287
|
disposers.push(setDynamicProperty(element, key.slice(5), value));
|
|
1157
1288
|
continue;
|
|
1158
1289
|
}
|
|
1159
1290
|
if (key.startsWith('attr:')) {
|
|
1291
|
+
reportReactiveDirective(element, 'attr', key.slice(5), value);
|
|
1160
1292
|
disposers.push(setDynamicAttribute(element, key.slice(5), value, elementIsSVG));
|
|
1161
1293
|
continue;
|
|
1162
1294
|
}
|
|
1163
1295
|
if (key.startsWith('bool:')) {
|
|
1296
|
+
reportReactiveDirective(element, 'bool', key.slice(5), value);
|
|
1164
1297
|
disposers.push(setDynamicBoolAttribute(element, key.slice(5), value));
|
|
1165
1298
|
continue;
|
|
1166
1299
|
}
|
|
1167
1300
|
if (key === 'class' || key === 'className') {
|
|
1168
1301
|
if (typeof value === 'function') {
|
|
1169
|
-
disposers.push(createRenderEffect(() =>
|
|
1302
|
+
disposers.push(createRenderEffect(() => {
|
|
1303
|
+
reportReactiveBinding(element, 'attribute', 'class', value);
|
|
1304
|
+
setClassName(element, value());
|
|
1305
|
+
}));
|
|
1170
1306
|
}
|
|
1171
1307
|
else {
|
|
1172
1308
|
setClassName(element, value);
|
|
@@ -1174,8 +1310,12 @@ export function spread(options) {
|
|
|
1174
1310
|
continue;
|
|
1175
1311
|
}
|
|
1176
1312
|
if (key === 'classList') {
|
|
1313
|
+
reportKeys(element, 'class', value);
|
|
1177
1314
|
if (typeof value === 'function') {
|
|
1178
|
-
disposers.push(createRenderEffect(() =>
|
|
1315
|
+
disposers.push(createRenderEffect(() => {
|
|
1316
|
+
reportReactiveBinding(element, 'attribute', 'class', value);
|
|
1317
|
+
setClassList(element, value());
|
|
1318
|
+
}));
|
|
1179
1319
|
}
|
|
1180
1320
|
else {
|
|
1181
1321
|
setClassList(element, value, disposers);
|
|
@@ -1183,8 +1323,12 @@ export function spread(options) {
|
|
|
1183
1323
|
continue;
|
|
1184
1324
|
}
|
|
1185
1325
|
if (key === 'style') {
|
|
1326
|
+
reportKeys(element, 'style', value);
|
|
1186
1327
|
if (typeof value === 'function') {
|
|
1187
|
-
disposers.push(createRenderEffect(() =>
|
|
1328
|
+
disposers.push(createRenderEffect(() => {
|
|
1329
|
+
reportReactiveBinding(element, 'attribute', 'style', value);
|
|
1330
|
+
setStyle(element, value());
|
|
1331
|
+
}));
|
|
1188
1332
|
}
|
|
1189
1333
|
else {
|
|
1190
1334
|
setStyle(element, value);
|
|
@@ -1207,12 +1351,12 @@ export function spread(options) {
|
|
|
1207
1351
|
: setDynamicAttribute(element, key, value, elementIsSVG));
|
|
1208
1352
|
}
|
|
1209
1353
|
else {
|
|
1210
|
-
// A plain, non-function value may still be backed by a reactive
|
|
1354
|
+
// A plain, non-function value may still be backed by a reactive getter:
|
|
1211
1355
|
// component props compile to `get x() {...}`, and splitProps/mergeProps
|
|
1212
1356
|
// forward those getters. Reading `props[key]` once (above) snapshots the
|
|
1213
1357
|
// current value and drops the dependency, so the DOM never updates. Re-read
|
|
1214
1358
|
// it inside a render effect so the getter's reads are tracked. (A truly
|
|
1215
|
-
// static value just runs the effect once with no deps
|
|
1359
|
+
// static value just runs the effect once with no deps: negligible.)
|
|
1216
1360
|
disposers.push(createRenderEffect(() => asProperty
|
|
1217
1361
|
? setProperty(element, key, props[key])
|
|
1218
1362
|
: setAttribute(element, key, props[key], elementIsSVG)));
|
|
@@ -1255,16 +1399,19 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1255
1399
|
addNativeEventListener(element, key.slice(3), val);
|
|
1256
1400
|
continue;
|
|
1257
1401
|
}
|
|
1258
|
-
// Forced bindings
|
|
1402
|
+
// Forced bindings: see spread().
|
|
1259
1403
|
if (key.startsWith('prop:')) {
|
|
1404
|
+
reportReactiveDirective(element, 'prop', key.slice(5), val);
|
|
1260
1405
|
setDynamicProperty(element, key.slice(5), val);
|
|
1261
1406
|
continue;
|
|
1262
1407
|
}
|
|
1263
1408
|
if (key.startsWith('attr:')) {
|
|
1409
|
+
reportReactiveDirective(element, 'attr', key.slice(5), val);
|
|
1264
1410
|
setDynamicAttribute(element, key.slice(5), val, isSVG);
|
|
1265
1411
|
continue;
|
|
1266
1412
|
}
|
|
1267
1413
|
if (key.startsWith('bool:')) {
|
|
1414
|
+
reportReactiveDirective(element, 'bool', key.slice(5), val);
|
|
1268
1415
|
setDynamicBoolAttribute(element, key.slice(5), val);
|
|
1269
1416
|
continue;
|
|
1270
1417
|
}
|
|
@@ -1272,7 +1419,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1272
1419
|
const name = key.slice(2).toLowerCase();
|
|
1273
1420
|
if (typeof val === 'function') {
|
|
1274
1421
|
if (NON_DELEGATED.has(name)) {
|
|
1275
|
-
// Non-bubbling events (blur, focus, scroll
|
|
1422
|
+
// Non-bubbling events (blur, focus, scroll...): store handler on the element
|
|
1276
1423
|
// and attach a single stable forwarder listener. This avoids
|
|
1277
1424
|
// addEventListener/removeEventListener churn when a reactive scope re-runs,
|
|
1278
1425
|
// which would silently drop the listener via onCleanup before re-adding it.
|
|
@@ -1298,27 +1445,35 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1298
1445
|
continue;
|
|
1299
1446
|
}
|
|
1300
1447
|
if (key === 'classList') {
|
|
1448
|
+
reportKeys(element, 'class', val);
|
|
1301
1449
|
if (typeof val === 'function')
|
|
1302
|
-
createRenderEffect(() =>
|
|
1450
|
+
createRenderEffect(() => {
|
|
1451
|
+
reportReactiveBinding(element, 'attribute', 'class', val);
|
|
1452
|
+
setClassList(element, val());
|
|
1453
|
+
});
|
|
1303
1454
|
else
|
|
1304
1455
|
setClassList(element, val);
|
|
1305
1456
|
continue;
|
|
1306
1457
|
}
|
|
1307
1458
|
if (key === 'style') {
|
|
1308
1459
|
// An accessor reaches applyProps whenever a component forwards a computed
|
|
1309
|
-
// style through a props object rather than as a compiled JSX attribute
|
|
1460
|
+
// style through a props object rather than as a compiled JSX attribute,
|
|
1310
1461
|
// which is what every polymorphic/`as` wrapper does. class and classList
|
|
1311
1462
|
// already handled that; style applied once and never updated.
|
|
1463
|
+
reportKeys(element, 'style', val);
|
|
1312
1464
|
if (typeof val === 'function')
|
|
1313
|
-
createRenderEffect(() =>
|
|
1465
|
+
createRenderEffect(() => {
|
|
1466
|
+
reportReactiveBinding(element, 'attribute', 'style', val);
|
|
1467
|
+
setStyle(element, val());
|
|
1468
|
+
});
|
|
1314
1469
|
else
|
|
1315
1470
|
setStyle(element, val);
|
|
1316
1471
|
continue;
|
|
1317
1472
|
}
|
|
1318
1473
|
if (key === 'innerHTML') {
|
|
1319
|
-
// Accept a static string
|
|
1474
|
+
// Accept a static string or a reactive accessor. A function is wrapped in an effect so
|
|
1320
1475
|
// updates apply; a plain value is set once. (The previous code always called val(), which
|
|
1321
|
-
// threw on a static string
|
|
1476
|
+
// threw on a static string, wiping content during hydration.)
|
|
1322
1477
|
if (typeof val === 'function') {
|
|
1323
1478
|
const dispose = createEffect(() => {
|
|
1324
1479
|
let v = val();
|
|
@@ -1333,7 +1488,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1333
1488
|
}
|
|
1334
1489
|
continue;
|
|
1335
1490
|
}
|
|
1336
|
-
// Special case: <input>/<textarea> value
|
|
1491
|
+
// Special case: <input>/<textarea> value: use DOM property assignment, not
|
|
1337
1492
|
// setAttribute. Skip reactive updates while the element has focus so user
|
|
1338
1493
|
// input is never interrupted and focus is never lost due to signal writes.
|
|
1339
1494
|
if (key === 'value' && (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA')) {
|
|
@@ -1343,7 +1498,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1343
1498
|
// console.log('apply inpit props', v, element);
|
|
1344
1499
|
while (typeof v === 'function' && v.length === 0)
|
|
1345
1500
|
v = v();
|
|
1346
|
-
// Only update if element is
|
|
1501
|
+
// Only update if element is not currently focused: don't fight user input
|
|
1347
1502
|
if (document.activeElement !== element) {
|
|
1348
1503
|
element.value = v ?? '';
|
|
1349
1504
|
}
|
|
@@ -1369,7 +1524,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1369
1524
|
let v = val();
|
|
1370
1525
|
while (typeof v === 'function' && v.length === 0)
|
|
1371
1526
|
v = v();
|
|
1372
|
-
// Only update if element is
|
|
1527
|
+
// Only update if element is not currently focused: don't fight user input
|
|
1373
1528
|
if (document.activeElement !== element) {
|
|
1374
1529
|
element.checked = v ?? '';
|
|
1375
1530
|
}
|
|
@@ -1377,7 +1532,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1377
1532
|
onCleanup(() => { dispose(); });
|
|
1378
1533
|
}
|
|
1379
1534
|
else {
|
|
1380
|
-
// Plain value possibly behind a reactive getter
|
|
1535
|
+
// Plain value possibly behind a reactive getter: re-read live. See the
|
|
1381
1536
|
// value branch above.
|
|
1382
1537
|
createRenderEffect(() => {
|
|
1383
1538
|
if (document.activeElement !== element) {
|
|
@@ -1387,8 +1542,8 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1387
1542
|
}
|
|
1388
1543
|
continue;
|
|
1389
1544
|
}
|
|
1390
|
-
// Special case: <select value
|
|
1391
|
-
//
|
|
1545
|
+
// Special case: <select value=...>: must assign .value as DOM property
|
|
1546
|
+
// after options are in the DOM. We defer the initial run via queueMicrotask
|
|
1392
1547
|
// (children/options are inserted synchronously after applyProps returns).
|
|
1393
1548
|
if (key === 'value' && element.tagName === 'SELECT') {
|
|
1394
1549
|
if (typeof val === 'function') {
|
|
@@ -1408,7 +1563,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1408
1563
|
onCleanup(() => { dispose(); });
|
|
1409
1564
|
}
|
|
1410
1565
|
else {
|
|
1411
|
-
// Plain value possibly behind a reactive getter
|
|
1566
|
+
// Plain value possibly behind a reactive getter: re-read live, keeping the
|
|
1412
1567
|
// first assignment deferred until the options are in the DOM.
|
|
1413
1568
|
let firstRun = true;
|
|
1414
1569
|
createRenderEffect(() => {
|
|
@@ -1448,19 +1603,19 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1448
1603
|
}
|
|
1449
1604
|
}
|
|
1450
1605
|
/**
|
|
1451
|
-
* Merge several (possibly reactive) prop sources into one, last-source-wins
|
|
1452
|
-
* source's `undefined` does
|
|
1606
|
+
* Merge several (possibly reactive) prop sources into one, last-source-wins, but a later
|
|
1607
|
+
* source's `undefined` does not clobber an earlier defined value; it falls through to it. This
|
|
1453
1608
|
* is what makes `mergeProps(defaults, props)` work, and why the compiler can emit
|
|
1454
1609
|
* `mergeProps(...spreads, baseProps)` for JSX spreads (`<div {...a} {...b} />`) without a missing
|
|
1455
1610
|
* `b.x` wiping `a.x`.
|
|
1456
1611
|
*
|
|
1457
1612
|
* Like Solid's mergeProps (and `splitProps` above), the result is a Proxy that resolves each key
|
|
1458
|
-
* LAZILY off the sources
|
|
1613
|
+
* LAZILY off the sources: getter/signal-backed props stay live (re-read on every access, not
|
|
1459
1614
|
* snapshotted), and the key set is the union across all sources (so dynamic prop objects work).
|
|
1460
1615
|
*/
|
|
1461
1616
|
export function mergeProps(...sources) {
|
|
1462
1617
|
// Last defined (non-undefined) value wins; scan sources from the end so a later `undefined`
|
|
1463
|
-
// falls through to an earlier source. Reads `source[key]` live each call
|
|
1618
|
+
// falls through to an earlier source. Reads `source[key]` live each call, no snapshot.
|
|
1464
1619
|
const resolve = (key) => {
|
|
1465
1620
|
for (let i = sources.length - 1; i >= 0; i--) {
|
|
1466
1621
|
const s = sources[i];
|
|
@@ -1493,7 +1648,7 @@ export function mergeProps(...sources) {
|
|
|
1493
1648
|
}
|
|
1494
1649
|
/**
|
|
1495
1650
|
* Split a (possibly reactive) props object into groups of keys plus a `rest`,
|
|
1496
|
-
* preserving reactivity
|
|
1651
|
+
* preserving reactivity, each returned object is a Proxy whose `get` forwards
|
|
1497
1652
|
* to the original `props`, so a getter/signal-backed prop stays live.
|
|
1498
1653
|
*
|
|
1499
1654
|
* const [local, rest] = splitProps(props, ['class', 'id']);
|
|
@@ -1536,6 +1691,12 @@ export function splitProps(props, ...keys) {
|
|
|
1536
1691
|
export function handleRef(ref, element) {
|
|
1537
1692
|
if (!ref)
|
|
1538
1693
|
return;
|
|
1694
|
+
// Which variable ends up holding the element is the question a ref raises, and only the
|
|
1695
|
+
// compiler's mark on the value can answer it.
|
|
1696
|
+
// The compiler's mark names the variable it is stored in; a function's own name is the
|
|
1697
|
+
// next best thing.
|
|
1698
|
+
const held = ref[HOLE_TAG]?.name;
|
|
1699
|
+
reportReactiveDirective(element, 'ref', held || (typeof ref === 'function' ? ref.name || 'ref' : 'ref'), ref);
|
|
1539
1700
|
// Swapped-argument guard
|
|
1540
1701
|
if (isDomNode(ref)) {
|
|
1541
1702
|
const actualRef = element;
|
|
@@ -1624,7 +1785,7 @@ export function isPromise(v) {
|
|
|
1624
1785
|
v instanceof Promise);
|
|
1625
1786
|
}
|
|
1626
1787
|
// ============================================================================
|
|
1627
|
-
// isAccessor
|
|
1788
|
+
// isAccessor: deprecated, kept for backward compat
|
|
1628
1789
|
// ============================================================================
|
|
1629
1790
|
export function isAccessor(value) {
|
|
1630
1791
|
return typeof value === 'function' && value.length === 0 && isSignal(value);
|
|
@@ -1641,7 +1802,7 @@ export function render(element, container, context) {
|
|
|
1641
1802
|
// This is critical for context propagation (Provider/useContext).
|
|
1642
1803
|
createRoot((dispose) => {
|
|
1643
1804
|
batch(() => {
|
|
1644
|
-
// Clear container (but not when hydrating
|
|
1805
|
+
// Clear container (but not when hydrating: we reuse the server DOM).
|
|
1645
1806
|
if (!hydration.active) {
|
|
1646
1807
|
container.textContent = '';
|
|
1647
1808
|
}
|
|
@@ -1667,15 +1828,15 @@ export function render(element, container, context) {
|
|
|
1667
1828
|
* Normalize what `bind:` was given into a `[read, write]` pair.
|
|
1668
1829
|
*
|
|
1669
1830
|
* `createSignal` hands back that pair already; `signal` hands back `{ get, set }`. Both
|
|
1670
|
-
* are the same primitive, so `bind:value` has to take either
|
|
1831
|
+
* are the same primitive, so `bind:value` has to take either, steering people to the
|
|
1671
1832
|
* new API and then refusing it in a directive would be its own kind of broken.
|
|
1672
1833
|
*/
|
|
1673
1834
|
export function bindPair(source) {
|
|
1674
1835
|
if (Array.isArray(source))
|
|
1675
1836
|
return source;
|
|
1676
|
-
// `signal(
|
|
1837
|
+
// `signal(...)` is the accessor itself with `set` attached.
|
|
1677
1838
|
if (typeof source === 'function' && typeof source.set === 'function') {
|
|
1678
1839
|
return [source, source.set];
|
|
1679
1840
|
}
|
|
1680
|
-
throw new TypeError('[fluixi] bind: needs a signal
|
|
1841
|
+
throw new TypeError('[fluixi] bind: needs a signal, either `signal(...)` or the `[get, set]` pair from `createSignal(...)`.');
|
|
1681
1842
|
}
|