@fluixi/dom 1.0.0-alpha.83 → 1.0.0-alpha.85
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 +2 -2
- 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.js +3 -3
- 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 +11 -11
- package/dist/lib/dom/island.mjs +769 -1
- package/dist/lib/dom/runtime.cjs +1432 -1
- package/dist/lib/dom/runtime.d.ts +60 -17
- package/dist/lib/dom/runtime.d.ts.map +1 -1
- package/dist/lib/dom/runtime.js +122 -79
- 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 +6 -0
- package/dist/lib/dom/server/host.d.ts.map +1 -1
- package/dist/lib/dom/server/host.js +7 -1
- 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 +23 -15
- 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 +2 -2
- 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 +3 -3
- 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 +3 -3
- 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 +3 -3
- 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 +4 -115
- 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 +3 -3
- 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 +6 -6
- 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,7 +17,7 @@ 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
|
-
//
|
|
20
|
+
// not in the reactive core (e.g. createComponent).
|
|
22
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';
|
|
@@ -84,14 +83,14 @@ function located(element, at) {
|
|
|
84
83
|
// Template cache
|
|
85
84
|
const templateCache = new Map();
|
|
86
85
|
// ============================================================================
|
|
87
|
-
// Event Delegation
|
|
86
|
+
// Event Delegation. SolidJS $$eventname property pattern
|
|
88
87
|
//
|
|
89
88
|
// Handlers are stored directly on DOM nodes as `node.$$click = handler`.
|
|
90
|
-
// The capture listener reads `node.$$click` directly
|
|
89
|
+
// The capture listener reads `node.$$click` directly, no Map lookups needed.
|
|
91
90
|
// This approach correctly handles e.preventDefault() and stopPropagation.
|
|
92
91
|
// ============================================================================
|
|
93
92
|
const delegatedSet = new Set();
|
|
94
|
-
// Events that don't bubble
|
|
93
|
+
// Events that don't bubble: must use direct addEventListener
|
|
95
94
|
const NON_DELEGATED = new Set(['scroll', 'focus', 'blur', 'load', 'error', 'resize', 'mouseenter', 'mouseleave', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture']);
|
|
96
95
|
/**
|
|
97
96
|
* Register global delegated event listener for the given event names.
|
|
@@ -125,12 +124,12 @@ function delegatedEventHandler(e) {
|
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
/**
|
|
128
|
-
* Add a delegated event handler
|
|
127
|
+
* Add a delegated event handler: stores as $$eventname property
|
|
129
128
|
*/
|
|
130
129
|
export function addDelegatedEventListener(element, name, handler) {
|
|
131
130
|
element[`$$${name}`] = handler;
|
|
132
131
|
if (NON_DELEGATED.has(name)) {
|
|
133
|
-
// Non-bubbling event (focus/blur/pointerenter
|
|
132
|
+
// Non-bubbling event (focus/blur/pointerenter/...): document delegation can't observe it, so
|
|
134
133
|
// attach a direct listener once. A stable forwarder reads $$name so updating the handler
|
|
135
134
|
// doesn't churn addEventListener/removeEventListener.
|
|
136
135
|
const key = `__fx_${name}`;
|
|
@@ -175,7 +174,7 @@ export function isFunction(value) {
|
|
|
175
174
|
function createTextNode(value, anchor = false) {
|
|
176
175
|
if (isServer())
|
|
177
176
|
return getServerNodes().createText(value);
|
|
178
|
-
// Anchors/placeholders (insert markers) are client-only insertion points
|
|
177
|
+
// Anchors/placeholders (insert markers) are client-only insertion points, the server
|
|
179
178
|
// serialized nothing for them, so they must never adopt a server text node. Only real
|
|
180
179
|
// content goes through the hydration walker.
|
|
181
180
|
if (hydration.active && !anchor) {
|
|
@@ -195,6 +194,17 @@ function createMarker(value) {
|
|
|
195
194
|
}
|
|
196
195
|
return document.createComment(value);
|
|
197
196
|
}
|
|
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
|
+
*/
|
|
198
208
|
export function createNativeElement(tag, isSVG = false, at) {
|
|
199
209
|
if (isServer())
|
|
200
210
|
return getServerNodes().createElement(tag, isSVG);
|
|
@@ -205,6 +215,13 @@ export function createNativeElement(tag, isSVG = false, at) {
|
|
|
205
215
|
}
|
|
206
216
|
return located(isSVG ? document.createElementNS(SVG_NAMESPACE, tag) : document.createElement(tag), at);
|
|
207
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
|
+
*/
|
|
208
225
|
export function createTemplate(html, isSVG = false) {
|
|
209
226
|
const cached = templateCache.get(html);
|
|
210
227
|
if (cached)
|
|
@@ -224,14 +241,14 @@ export function createTemplate(html, isSVG = false) {
|
|
|
224
241
|
* needs it.
|
|
225
242
|
*
|
|
226
243
|
* The three cases mirror {@link createNativeElement}, which is the point: a template has
|
|
227
|
-
* 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,
|
|
228
245
|
* which is exactly how it failed before this existed.
|
|
229
246
|
*
|
|
230
|
-
* - **server
|
|
247
|
+
* - **server**: carried as text and written out verbatim. Parsing it into elements
|
|
231
248
|
* only to serialize them straight back would be pure cost.
|
|
232
|
-
* - **hydrating
|
|
249
|
+
* - **hydrating**: adopt the server's existing subtree and skip past it. The subtree
|
|
233
250
|
* is inert, so there is nothing inside to wire up.
|
|
234
|
-
* - **client
|
|
251
|
+
* - **client**: parse once (cached by HTML) and clone per instance.
|
|
235
252
|
*/
|
|
236
253
|
export function templateNode(html, tag, holes = false, isSVG = false, at) {
|
|
237
254
|
if (isServer()) {
|
|
@@ -253,7 +270,7 @@ export function templateNode(html, tag, holes = false, isSVG = false, at) {
|
|
|
253
270
|
* Write each element's own source position onto it.
|
|
254
271
|
*
|
|
255
272
|
* The whole template is one string, so a source map traces every element in it back to the
|
|
256
|
-
* line the root opens at
|
|
273
|
+
* line the root opens at: devtools would name the component and nothing finer. This is the
|
|
257
274
|
* one moment the two line up: the tree holds exactly the elements the compiler listed, in
|
|
258
275
|
* the same order, because holes are filled after this returns.
|
|
259
276
|
*
|
|
@@ -276,7 +293,7 @@ function stampPositions(node, at) {
|
|
|
276
293
|
* for *that hole*.
|
|
277
294
|
*
|
|
278
295
|
* Without this, only the hole's top level adopts. `templateNode` moves the cursor past the
|
|
279
|
-
* whole template
|
|
296
|
+
* whole template, the subtree is inert, so there is nothing in it to walk, which leaves
|
|
280
297
|
* the cursor in the wrong place for anything the hole builds internally. A component in a
|
|
281
298
|
* hole would then recreate its own elements: no duplication, but every server node thrown
|
|
282
299
|
* away, which is most of what hydration is for.
|
|
@@ -287,13 +304,13 @@ export function holeScope(start, produce) {
|
|
|
287
304
|
if (!hydration.active)
|
|
288
305
|
return produce();
|
|
289
306
|
// `produce` is called with the cursor already placed, because the value may be built
|
|
290
|
-
// eagerly
|
|
307
|
+
// eagerly: `props.children` is a getter, and reading it constructs its nodes there and
|
|
291
308
|
// then. Wrapping the result instead would be too late: the DOM would already exist,
|
|
292
309
|
// built against whatever cursor happened to be current, and the server's nodes would be
|
|
293
310
|
// left behind as duplicates.
|
|
294
311
|
const value = runHydrationAt(start.nextSibling, produce);
|
|
295
312
|
// A memo or accessor builds nothing until it is read, which happens inside `insert`'s
|
|
296
|
-
// effect
|
|
313
|
+
// effect, so it needs the cursor placed again at that moment. The guard matters: once
|
|
297
314
|
// hydration is over, later updates must render normally rather than re-enter it.
|
|
298
315
|
if (typeof value === 'function') {
|
|
299
316
|
const accessor = value;
|
|
@@ -307,7 +324,7 @@ export function holeScope(start, produce) {
|
|
|
307
324
|
/**
|
|
308
325
|
* The compiler tags a hole's accessor with where it was written, and devtools reads the tag
|
|
309
326
|
* off whatever finally reaches `insert`. Under hydration that is this wrapper, not the
|
|
310
|
-
* accessor, so the tag has to come along
|
|
327
|
+
* accessor, so the tag has to come along: otherwise the binding reports the frame it was
|
|
311
328
|
* created from, which is a line in this file.
|
|
312
329
|
*/
|
|
313
330
|
function carryHoleTag(from, to) {
|
|
@@ -336,7 +353,7 @@ export function holeEnd(start) {
|
|
|
336
353
|
*
|
|
337
354
|
* On a fresh clone the markers are adjacent and this is empty, so `insert` builds the
|
|
338
355
|
* content. When hydrating, the server's rendered content sits between them and is handed
|
|
339
|
-
* back
|
|
356
|
+
* back: `insertExpression` then reuses those nodes (setting `.data` on a text node
|
|
340
357
|
* rather than creating one), which is what adoption means here.
|
|
341
358
|
*
|
|
342
359
|
* Paired markers are what make this exact: a hole's content is *by definition* the nodes
|
|
@@ -374,12 +391,12 @@ export function fnName(fn) {
|
|
|
374
391
|
return fn._name || fn.name || 'anonymous';
|
|
375
392
|
}
|
|
376
393
|
/**
|
|
377
|
-
* iterateFn
|
|
394
|
+
* iterateFn: resolves a value by calling through plain function wrappers.
|
|
378
395
|
*
|
|
379
396
|
* Must stop at signals/memos/resources ($SIGNAL marker) and
|
|
380
397
|
* component thunks (fluixi-component marker). Without this:
|
|
381
398
|
* - components returning memos get unwrapped to static values, losing reactivity
|
|
382
|
-
* - lazy component createMemo re-triggers C(props)
|
|
399
|
+
* - lazy component createMemo re-triggers C(props) -> infinite recursion
|
|
383
400
|
*/
|
|
384
401
|
export function iterateFn(item) {
|
|
385
402
|
// Stop immediately if it's a signal, memo, or resource
|
|
@@ -390,7 +407,7 @@ export function iterateFn(item) {
|
|
|
390
407
|
return item;
|
|
391
408
|
if (typeof item !== 'function')
|
|
392
409
|
return item;
|
|
393
|
-
// Unwrap plain function wrappers
|
|
410
|
+
// Unwrap plain function wrappers: stop at signals and component thunks
|
|
394
411
|
let content = item();
|
|
395
412
|
while (typeof content === 'function' &&
|
|
396
413
|
!isSignal(content) &&
|
|
@@ -408,7 +425,7 @@ const MAX_INSERT_DEPTH = 200;
|
|
|
408
425
|
// _createInsertEffect
|
|
409
426
|
//
|
|
410
427
|
// Creates a reactive render effect for DOM insertion.
|
|
411
|
-
// 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:
|
|
412
429
|
// same thunk function reference always returns the same output across re-runs.
|
|
413
430
|
// ============================================================================
|
|
414
431
|
function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCurrent) {
|
|
@@ -416,7 +433,7 @@ function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCur
|
|
|
416
433
|
const componentCache = new Map();
|
|
417
434
|
return createRenderEffect(() => {
|
|
418
435
|
// The accessor, never its result: it carries the compiler's hole position, and calling it
|
|
419
|
-
// again here builds the dom twice
|
|
436
|
+
// again here builds the dom twice, during hydration, a duplicated subtree.
|
|
420
437
|
//
|
|
421
438
|
// Every run, because what this effect writes into moves as the value does.
|
|
422
439
|
reportReactiveBinding(marker ?? parent, 'content', undefined, getAccessor);
|
|
@@ -489,9 +506,22 @@ function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCur
|
|
|
489
506
|
// ============================================================================
|
|
490
507
|
// insert
|
|
491
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
|
+
*/
|
|
492
522
|
export function insert(parent, accessor, marker, init) {
|
|
493
523
|
if (_insertDepth > MAX_INSERT_DEPTH) {
|
|
494
|
-
console.error('[insert] Exceeded max call depth
|
|
524
|
+
console.error('[insert] Exceeded max call depth, possible infinite loop.');
|
|
495
525
|
return () => null;
|
|
496
526
|
}
|
|
497
527
|
if (marker !== undefined && !marker) {
|
|
@@ -510,7 +540,7 @@ export function insert(parent, accessor, marker, init) {
|
|
|
510
540
|
if (typeof accessor !== 'function' && !isReactiveArray(accessor)) {
|
|
511
541
|
_insertDepth++;
|
|
512
542
|
try {
|
|
513
|
-
// `current
|
|
543
|
+
// `current`, not null. The caller passes what is already in this slot, and for a
|
|
514
544
|
// hydrating template hole that is the server's own nodes (`holeContent`). Dropping it
|
|
515
545
|
// meant a static value never adopted: insertExpression saw an empty slot and appended
|
|
516
546
|
// a second copy beside the server's, so `{cmd}` rendered the string twice.
|
|
@@ -595,14 +625,14 @@ export function insertExpression(parent, value, marker, current, componentCache)
|
|
|
595
625
|
}
|
|
596
626
|
else if (!isAdopted(parent, node)) {
|
|
597
627
|
// during hydration createTextNode reuses the in-place server text node and
|
|
598
|
-
// already advanced the cursor
|
|
628
|
+
// already advanced the cursor: don't re-insert (that would reorder).
|
|
599
629
|
insertBefore(parent, node, marker);
|
|
600
630
|
}
|
|
601
631
|
return node;
|
|
602
632
|
}
|
|
603
633
|
if (isDomNode(value)) {
|
|
604
634
|
// Hydration: the node was already produced from the server DOM (reused by
|
|
605
|
-
// createNativeElement) and sits in `parent` in place
|
|
635
|
+
// createNativeElement) and sits in `parent` in place: adopt it without
|
|
606
636
|
// re-inserting, and ascend the cursor past its (already-walked) subtree.
|
|
607
637
|
if (isAdopted(parent, value)) {
|
|
608
638
|
ascendPast(value);
|
|
@@ -646,13 +676,13 @@ export function insertExpression(parent, value, marker, current, componentCache)
|
|
|
646
676
|
const reactiveSlots = [];
|
|
647
677
|
flattenArray(value, array, parent, marker, true, componentCache, reactiveSlots);
|
|
648
678
|
const reconciled = reconcileArrays(parent, currentArray, array, marker);
|
|
649
|
-
// Markers are now in the DOM
|
|
679
|
+
// Markers are now in the DOM: wire a per-slot reactive insert. insert() sets up a nested
|
|
650
680
|
// render effect that re-inserts the member's content before its marker on change, so a bare
|
|
651
681
|
// control-flow component return (<Show>/<Switch>/<For>) updates in place.
|
|
652
682
|
//
|
|
653
683
|
// The per-slot content lives OUTSIDE the reconcile array (only the marker is tracked), so if
|
|
654
|
-
// this enclosing effect re-runs with a shorter array
|
|
655
|
-
// 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
|
|
656
686
|
// content. Register a cleanup that removes the slot's nodes when its effect is torn down. This
|
|
657
687
|
// only fires when the enclosing effect re-runs (the array itself changed); an in-place update
|
|
658
688
|
// (e.g. <Show> toggling) never re-runs this effect, so it keeps updating in place.
|
|
@@ -678,12 +708,12 @@ export function insertExpression(parent, value, marker, current, componentCache)
|
|
|
678
708
|
}
|
|
679
709
|
return cleanChildren(parent, current, marker);
|
|
680
710
|
}
|
|
681
|
-
// flattenArray
|
|
711
|
+
// flattenArray: flatten a children array to DOM nodes. A signal whose result is a
|
|
682
712
|
// component thunk (e.g. a router memo returning a SuspenseThunk) is recursed through
|
|
683
713
|
// flattenArray, not read inline, so the thunk goes through componentCache and stays
|
|
684
714
|
// stable across navigations.
|
|
685
715
|
function flattenArray(arr, result, parent, marker, track = true, componentCache,
|
|
686
|
-
// When provided, a reactive member is
|
|
716
|
+
// When provided, a reactive member is not read inline (which would subscribe the enclosing
|
|
687
717
|
// render effect and re-render the whole subtree on change). Instead it emits a placeholder
|
|
688
718
|
// marker and records itself here; the array caller wires a fine-grained insert() per slot
|
|
689
719
|
// after the markers are placed. This is what makes a bare control-flow component return
|
|
@@ -704,14 +734,14 @@ reactiveSlots) {
|
|
|
704
734
|
if (typeof item === 'function') {
|
|
705
735
|
let resolved;
|
|
706
736
|
if (isComponent(item) && !isSignal(item)) {
|
|
707
|
-
// Pure component thunk (e.g. () => <div>)
|
|
737
|
+
// Pure component thunk (e.g. () => <div>): call exactly once and cache so
|
|
708
738
|
// re-runs of the enclosing render effect return the same DOM nodes (keeps
|
|
709
739
|
// focus etc. stable across navigations).
|
|
710
740
|
// must check !isSignal. A memo stamped as a component (e.g. an
|
|
711
741
|
// Outlet/Routes/Suspense output returned via $component/asComponent) is BOTH
|
|
712
742
|
// isComponent AND isSignal; caching it would freeze its first value (so a
|
|
713
|
-
// boundary that later switches from a single node to an array
|
|
714
|
-
// 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.
|
|
715
745
|
if (!componentCache)
|
|
716
746
|
componentCache = new Map();
|
|
717
747
|
if (!componentCache.has(item))
|
|
@@ -719,17 +749,17 @@ reactiveSlots) {
|
|
|
719
749
|
resolved = componentCache.get(item);
|
|
720
750
|
}
|
|
721
751
|
else {
|
|
722
|
-
// Signal/memo (or memo-stamped component)
|
|
752
|
+
// Signal/memo (or memo-stamped component): read to track reactivity.
|
|
723
753
|
resolved = item();
|
|
724
754
|
}
|
|
725
755
|
// Unwrap plain wrappers; stop at signals and component thunks.
|
|
726
|
-
// Cache calls by function reference
|
|
756
|
+
// Cache calls by function reference, a plain function returned by a
|
|
727
757
|
// signal/memo (e.g. `createMemo(() => cond ? () => <JSX> : ...)`) is a
|
|
728
758
|
// stable reference as long as the memo doesn't re-evaluate. Calling it
|
|
729
759
|
// once and caching the result means re-runs of the outer render effect
|
|
730
760
|
// (triggered by unrelated signals like `activeLevel()`) hit the cache and
|
|
731
761
|
// return the same DOM nodes. reconcileArrays then sees identical references
|
|
732
|
-
// and skips the slot
|
|
762
|
+
// and skips the slot, the focused <input> is never removed.
|
|
733
763
|
while (typeof resolved === 'function' &&
|
|
734
764
|
!isPromise(resolved) &&
|
|
735
765
|
!isSignal(resolved) &&
|
|
@@ -753,8 +783,8 @@ reactiveSlots) {
|
|
|
753
783
|
reactiveSlots.push({ marker: slot, sig: resolved });
|
|
754
784
|
continue;
|
|
755
785
|
}
|
|
756
|
-
// Read the signal
|
|
757
|
-
// Use flattenArray recursively (
|
|
786
|
+
// Read the signal: tracked inside the enclosing render effect.
|
|
787
|
+
// Use flattenArray recursively (not normalizeValue) so
|
|
758
788
|
// component thunks inside the result go through componentCache.
|
|
759
789
|
let val = resolved();
|
|
760
790
|
while (typeof val === 'function' && isSignal(val))
|
|
@@ -768,7 +798,7 @@ reactiveSlots) {
|
|
|
768
798
|
flattenArray([val], result, parent, marker, track, componentCache);
|
|
769
799
|
continue;
|
|
770
800
|
}
|
|
771
|
-
// Other function type
|
|
801
|
+
// Other function type: skip
|
|
772
802
|
continue;
|
|
773
803
|
}
|
|
774
804
|
// resolved is not a function
|
|
@@ -834,15 +864,15 @@ reactiveSlots) {
|
|
|
834
864
|
// ============================================================================
|
|
835
865
|
function reconcileArrays(parent, a, b, marker) {
|
|
836
866
|
// Pre-process: clone any node already owned by a different parent.
|
|
837
|
-
// The Babel plugin hoists static elements to module-level
|
|
867
|
+
// The Babel plugin hoists static elements to module-level, the same node
|
|
838
868
|
// can appear in `b` for multiple parents simultaneously. Clone to avoid
|
|
839
869
|
// stealing it from its current owner; future reconciles track the clone.
|
|
840
870
|
//
|
|
841
|
-
// A DocumentFragment is
|
|
871
|
+
// A DocumentFragment is not such an owner. `cloneTemplate` hands back the clone still
|
|
842
872
|
// sitting in its fragment, so a template-cloned element arrives here with a non-null
|
|
843
873
|
// parent that nothing else refers to. Cloning it again threw away the node whose props
|
|
844
|
-
// had just been applied
|
|
845
|
-
// 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
|
|
846
876
|
// inserted as part of an array. Moving it out of the fragment is exactly right.
|
|
847
877
|
for (let i = 0; i < b.length; i++) {
|
|
848
878
|
const owner = b[i].parentNode;
|
|
@@ -859,18 +889,18 @@ function reconcileArrays(parent, a, b, marker) {
|
|
|
859
889
|
? a[aEnd - 1].nextSibling ?? undefined
|
|
860
890
|
: marker, map = null;
|
|
861
891
|
while (aStart < aEnd || bStart < bEnd) {
|
|
862
|
-
// 1. Common prefix
|
|
892
|
+
// 1. Common prefix: already in the right place, skip.
|
|
863
893
|
if (a[aStart] === b[bStart]) {
|
|
864
894
|
aStart++;
|
|
865
895
|
bStart++;
|
|
866
896
|
continue;
|
|
867
897
|
}
|
|
868
|
-
// 2. Common suffix
|
|
898
|
+
// 2. Common suffix: already in the right place, shrink window.
|
|
869
899
|
while (aEnd > aStart && bEnd > bStart && a[aEnd - 1] === b[bEnd - 1]) {
|
|
870
900
|
aEnd--;
|
|
871
901
|
bEnd--;
|
|
872
902
|
}
|
|
873
|
-
// 3. Old list exhausted
|
|
903
|
+
// 3. Old list exhausted: insert remaining new nodes.
|
|
874
904
|
if (aEnd === aStart) {
|
|
875
905
|
const node = bEnd < bLength
|
|
876
906
|
? bStart
|
|
@@ -880,7 +910,7 @@ function reconcileArrays(parent, a, b, marker) {
|
|
|
880
910
|
while (bStart < bEnd)
|
|
881
911
|
insertBefore(parent, b[bStart++], node);
|
|
882
912
|
}
|
|
883
|
-
// 4. New list exhausted
|
|
913
|
+
// 4. New list exhausted: remove remaining old nodes.
|
|
884
914
|
else if (bEnd === bStart) {
|
|
885
915
|
while (aStart < aEnd) {
|
|
886
916
|
if ((!map || !map.has(a[aStart])) && a[aStart].parentNode === parent)
|
|
@@ -918,7 +948,7 @@ function reconcileArrays(parent, a, b, marker) {
|
|
|
918
948
|
insertBefore(parent, b[bStart++], node);
|
|
919
949
|
}
|
|
920
950
|
else {
|
|
921
|
-
// Guard replaceChild
|
|
951
|
+
// Guard replaceChild, if a[aStart] was moved out by another effect, insert instead.
|
|
922
952
|
if (a[aStart].parentNode === parent) {
|
|
923
953
|
parent.replaceChild(b[bStart++], a[aStart++]);
|
|
924
954
|
}
|
|
@@ -1113,7 +1143,7 @@ export function setClassName(element, value) {
|
|
|
1113
1143
|
return;
|
|
1114
1144
|
}
|
|
1115
1145
|
if (typeof value === 'function') {
|
|
1116
|
-
// A render effect, like every other attribute binding
|
|
1146
|
+
// A render effect, like every other attribute binding: `spread` already wraps
|
|
1117
1147
|
// class this way, so going through `applyProps` shouldn't schedule it later.
|
|
1118
1148
|
createRenderEffect(() => {
|
|
1119
1149
|
reportReactiveBinding(element, 'attribute', 'class', value);
|
|
@@ -1142,7 +1172,7 @@ export function setClassName(element, value) {
|
|
|
1142
1172
|
element.setAttribute('class', String(value));
|
|
1143
1173
|
}
|
|
1144
1174
|
/**
|
|
1145
|
-
* Apply a `classList`-style map `{ 'a b': condition }
|
|
1175
|
+
* Apply a `classList`-style map `{ 'a b': condition }`, toggling each class on
|
|
1146
1176
|
* the element. A condition may be a value or a reactive accessor; when
|
|
1147
1177
|
* `disposers` is provided, accessor entries are wired with a render effect so
|
|
1148
1178
|
* they update fine-grained (Solid-style `classList`).
|
|
@@ -1192,6 +1222,19 @@ export function setStyle(element, _value) {
|
|
|
1192
1222
|
// ============================================================================
|
|
1193
1223
|
// spread
|
|
1194
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
|
+
*/
|
|
1195
1238
|
export function spread(options) {
|
|
1196
1239
|
const { element, props, prevProps = EMPTY_OBJ, isSVG = false, skipChildren = false, } = options;
|
|
1197
1240
|
if (!element || !isDomElement(element)) {
|
|
@@ -1204,7 +1247,7 @@ export function spread(options) {
|
|
|
1204
1247
|
}
|
|
1205
1248
|
// Trust the element's real namespace over the compile-time isSVG flag: the codegen only marks
|
|
1206
1249
|
// some SVG elements (e.g. <svg>/<defs>) and misses nested ones (<linearGradient>, <stop>). For
|
|
1207
|
-
// 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
|
|
1208
1251
|
// property branch below would throw "Cannot set property x1". Detecting SVG by namespaceURI keeps
|
|
1209
1252
|
// those on the setAttribute path. (Matters under hydration, where we adopt the real, correctly
|
|
1210
1253
|
// namespaced server node.)
|
|
@@ -1238,7 +1281,7 @@ export function spread(options) {
|
|
|
1238
1281
|
continue;
|
|
1239
1282
|
}
|
|
1240
1283
|
// Forced bindings: prop:x always assigns the property, attr:x always sets the
|
|
1241
|
-
// attribute, bool:x toggles a boolean attribute
|
|
1284
|
+
// attribute, bool:x toggles a boolean attribute, skipping the heuristic below.
|
|
1242
1285
|
if (key.startsWith('prop:')) {
|
|
1243
1286
|
reportReactiveDirective(element, 'prop', key.slice(5), value);
|
|
1244
1287
|
disposers.push(setDynamicProperty(element, key.slice(5), value));
|
|
@@ -1308,12 +1351,12 @@ export function spread(options) {
|
|
|
1308
1351
|
: setDynamicAttribute(element, key, value, elementIsSVG));
|
|
1309
1352
|
}
|
|
1310
1353
|
else {
|
|
1311
|
-
// 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:
|
|
1312
1355
|
// component props compile to `get x() {...}`, and splitProps/mergeProps
|
|
1313
1356
|
// forward those getters. Reading `props[key]` once (above) snapshots the
|
|
1314
1357
|
// current value and drops the dependency, so the DOM never updates. Re-read
|
|
1315
1358
|
// it inside a render effect so the getter's reads are tracked. (A truly
|
|
1316
|
-
// static value just runs the effect once with no deps
|
|
1359
|
+
// static value just runs the effect once with no deps: negligible.)
|
|
1317
1360
|
disposers.push(createRenderEffect(() => asProperty
|
|
1318
1361
|
? setProperty(element, key, props[key])
|
|
1319
1362
|
: setAttribute(element, key, props[key], elementIsSVG)));
|
|
@@ -1356,7 +1399,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1356
1399
|
addNativeEventListener(element, key.slice(3), val);
|
|
1357
1400
|
continue;
|
|
1358
1401
|
}
|
|
1359
|
-
// Forced bindings
|
|
1402
|
+
// Forced bindings: see spread().
|
|
1360
1403
|
if (key.startsWith('prop:')) {
|
|
1361
1404
|
reportReactiveDirective(element, 'prop', key.slice(5), val);
|
|
1362
1405
|
setDynamicProperty(element, key.slice(5), val);
|
|
@@ -1376,7 +1419,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1376
1419
|
const name = key.slice(2).toLowerCase();
|
|
1377
1420
|
if (typeof val === 'function') {
|
|
1378
1421
|
if (NON_DELEGATED.has(name)) {
|
|
1379
|
-
// Non-bubbling events (blur, focus, scroll
|
|
1422
|
+
// Non-bubbling events (blur, focus, scroll...): store handler on the element
|
|
1380
1423
|
// and attach a single stable forwarder listener. This avoids
|
|
1381
1424
|
// addEventListener/removeEventListener churn when a reactive scope re-runs,
|
|
1382
1425
|
// which would silently drop the listener via onCleanup before re-adding it.
|
|
@@ -1414,7 +1457,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1414
1457
|
}
|
|
1415
1458
|
if (key === 'style') {
|
|
1416
1459
|
// An accessor reaches applyProps whenever a component forwards a computed
|
|
1417
|
-
// 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,
|
|
1418
1461
|
// which is what every polymorphic/`as` wrapper does. class and classList
|
|
1419
1462
|
// already handled that; style applied once and never updated.
|
|
1420
1463
|
reportKeys(element, 'style', val);
|
|
@@ -1428,9 +1471,9 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1428
1471
|
continue;
|
|
1429
1472
|
}
|
|
1430
1473
|
if (key === 'innerHTML') {
|
|
1431
|
-
// Accept a static string
|
|
1474
|
+
// Accept a static string or a reactive accessor. A function is wrapped in an effect so
|
|
1432
1475
|
// updates apply; a plain value is set once. (The previous code always called val(), which
|
|
1433
|
-
// threw on a static string
|
|
1476
|
+
// threw on a static string, wiping content during hydration.)
|
|
1434
1477
|
if (typeof val === 'function') {
|
|
1435
1478
|
const dispose = createEffect(() => {
|
|
1436
1479
|
let v = val();
|
|
@@ -1445,7 +1488,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1445
1488
|
}
|
|
1446
1489
|
continue;
|
|
1447
1490
|
}
|
|
1448
|
-
// Special case: <input>/<textarea> value
|
|
1491
|
+
// Special case: <input>/<textarea> value: use DOM property assignment, not
|
|
1449
1492
|
// setAttribute. Skip reactive updates while the element has focus so user
|
|
1450
1493
|
// input is never interrupted and focus is never lost due to signal writes.
|
|
1451
1494
|
if (key === 'value' && (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA')) {
|
|
@@ -1455,7 +1498,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1455
1498
|
// console.log('apply inpit props', v, element);
|
|
1456
1499
|
while (typeof v === 'function' && v.length === 0)
|
|
1457
1500
|
v = v();
|
|
1458
|
-
// Only update if element is
|
|
1501
|
+
// Only update if element is not currently focused: don't fight user input
|
|
1459
1502
|
if (document.activeElement !== element) {
|
|
1460
1503
|
element.value = v ?? '';
|
|
1461
1504
|
}
|
|
@@ -1481,7 +1524,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1481
1524
|
let v = val();
|
|
1482
1525
|
while (typeof v === 'function' && v.length === 0)
|
|
1483
1526
|
v = v();
|
|
1484
|
-
// Only update if element is
|
|
1527
|
+
// Only update if element is not currently focused: don't fight user input
|
|
1485
1528
|
if (document.activeElement !== element) {
|
|
1486
1529
|
element.checked = v ?? '';
|
|
1487
1530
|
}
|
|
@@ -1489,7 +1532,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1489
1532
|
onCleanup(() => { dispose(); });
|
|
1490
1533
|
}
|
|
1491
1534
|
else {
|
|
1492
|
-
// Plain value possibly behind a reactive getter
|
|
1535
|
+
// Plain value possibly behind a reactive getter: re-read live. See the
|
|
1493
1536
|
// value branch above.
|
|
1494
1537
|
createRenderEffect(() => {
|
|
1495
1538
|
if (document.activeElement !== element) {
|
|
@@ -1499,8 +1542,8 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1499
1542
|
}
|
|
1500
1543
|
continue;
|
|
1501
1544
|
}
|
|
1502
|
-
// Special case: <select value
|
|
1503
|
-
//
|
|
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
|
|
1504
1547
|
// (children/options are inserted synchronously after applyProps returns).
|
|
1505
1548
|
if (key === 'value' && element.tagName === 'SELECT') {
|
|
1506
1549
|
if (typeof val === 'function') {
|
|
@@ -1520,7 +1563,7 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1520
1563
|
onCleanup(() => { dispose(); });
|
|
1521
1564
|
}
|
|
1522
1565
|
else {
|
|
1523
|
-
// Plain value possibly behind a reactive getter
|
|
1566
|
+
// Plain value possibly behind a reactive getter: re-read live, keeping the
|
|
1524
1567
|
// first assignment deferred until the options are in the DOM.
|
|
1525
1568
|
let firstRun = true;
|
|
1526
1569
|
createRenderEffect(() => {
|
|
@@ -1560,19 +1603,19 @@ export function applyProps(element, props, isSVG = false) {
|
|
|
1560
1603
|
}
|
|
1561
1604
|
}
|
|
1562
1605
|
/**
|
|
1563
|
-
* Merge several (possibly reactive) prop sources into one, last-source-wins
|
|
1564
|
-
* 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
|
|
1565
1608
|
* is what makes `mergeProps(defaults, props)` work, and why the compiler can emit
|
|
1566
1609
|
* `mergeProps(...spreads, baseProps)` for JSX spreads (`<div {...a} {...b} />`) without a missing
|
|
1567
1610
|
* `b.x` wiping `a.x`.
|
|
1568
1611
|
*
|
|
1569
1612
|
* Like Solid's mergeProps (and `splitProps` above), the result is a Proxy that resolves each key
|
|
1570
|
-
* LAZILY off the sources
|
|
1613
|
+
* LAZILY off the sources: getter/signal-backed props stay live (re-read on every access, not
|
|
1571
1614
|
* snapshotted), and the key set is the union across all sources (so dynamic prop objects work).
|
|
1572
1615
|
*/
|
|
1573
1616
|
export function mergeProps(...sources) {
|
|
1574
1617
|
// Last defined (non-undefined) value wins; scan sources from the end so a later `undefined`
|
|
1575
|
-
// 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.
|
|
1576
1619
|
const resolve = (key) => {
|
|
1577
1620
|
for (let i = sources.length - 1; i >= 0; i--) {
|
|
1578
1621
|
const s = sources[i];
|
|
@@ -1605,7 +1648,7 @@ export function mergeProps(...sources) {
|
|
|
1605
1648
|
}
|
|
1606
1649
|
/**
|
|
1607
1650
|
* Split a (possibly reactive) props object into groups of keys plus a `rest`,
|
|
1608
|
-
* preserving reactivity
|
|
1651
|
+
* preserving reactivity, each returned object is a Proxy whose `get` forwards
|
|
1609
1652
|
* to the original `props`, so a getter/signal-backed prop stays live.
|
|
1610
1653
|
*
|
|
1611
1654
|
* const [local, rest] = splitProps(props, ['class', 'id']);
|
|
@@ -1742,7 +1785,7 @@ export function isPromise(v) {
|
|
|
1742
1785
|
v instanceof Promise);
|
|
1743
1786
|
}
|
|
1744
1787
|
// ============================================================================
|
|
1745
|
-
// isAccessor
|
|
1788
|
+
// isAccessor: deprecated, kept for backward compat
|
|
1746
1789
|
// ============================================================================
|
|
1747
1790
|
export function isAccessor(value) {
|
|
1748
1791
|
return typeof value === 'function' && value.length === 0 && isSignal(value);
|
|
@@ -1759,7 +1802,7 @@ export function render(element, container, context) {
|
|
|
1759
1802
|
// This is critical for context propagation (Provider/useContext).
|
|
1760
1803
|
createRoot((dispose) => {
|
|
1761
1804
|
batch(() => {
|
|
1762
|
-
// Clear container (but not when hydrating
|
|
1805
|
+
// Clear container (but not when hydrating: we reuse the server DOM).
|
|
1763
1806
|
if (!hydration.active) {
|
|
1764
1807
|
container.textContent = '';
|
|
1765
1808
|
}
|
|
@@ -1785,15 +1828,15 @@ export function render(element, container, context) {
|
|
|
1785
1828
|
* Normalize what `bind:` was given into a `[read, write]` pair.
|
|
1786
1829
|
*
|
|
1787
1830
|
* `createSignal` hands back that pair already; `signal` hands back `{ get, set }`. Both
|
|
1788
|
-
* 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
|
|
1789
1832
|
* new API and then refusing it in a directive would be its own kind of broken.
|
|
1790
1833
|
*/
|
|
1791
1834
|
export function bindPair(source) {
|
|
1792
1835
|
if (Array.isArray(source))
|
|
1793
1836
|
return source;
|
|
1794
|
-
// `signal(
|
|
1837
|
+
// `signal(...)` is the accessor itself with `set` attached.
|
|
1795
1838
|
if (typeof source === 'function' && typeof source.set === 'function') {
|
|
1796
1839
|
return [source, source.set];
|
|
1797
1840
|
}
|
|
1798
|
-
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(...)`.');
|
|
1799
1842
|
}
|