@barefootjs/client 0.26.4 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/reactive.d.ts.map +1 -1
- package/dist/runtime/claim-slots.d.ts +222 -0
- package/dist/runtime/claim-slots.d.ts.map +1 -0
- package/dist/runtime/component.d.ts +40 -18
- package/dist/runtime/component.d.ts.map +1 -1
- package/dist/runtime/dynamic-text.d.ts +24 -1
- package/dist/runtime/dynamic-text.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +4 -5
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +467 -259
- package/dist/runtime/loop-markers.d.ts +26 -0
- package/dist/runtime/loop-markers.d.ts.map +1 -0
- package/dist/runtime/map-array-lazy.d.ts +164 -0
- package/dist/runtime/map-array-lazy.d.ts.map +1 -0
- package/dist/runtime/map-array.d.ts +35 -0
- package/dist/runtime/map-array.d.ts.map +1 -1
- package/dist/runtime/qsa-item.d.ts +7 -0
- package/dist/runtime/qsa-item.d.ts.map +1 -1
- package/dist/runtime/registry.d.ts.map +1 -1
- package/dist/runtime/standalone.js +455 -248
- package/package.json +2 -2
- package/src/reactive.ts +2 -1
- package/src/runtime/claim-slots.ts +647 -0
- package/src/runtime/component.ts +153 -70
- package/src/runtime/dynamic-text.ts +24 -1
- package/src/runtime/index.ts +20 -7
- package/src/runtime/insert.ts +1 -1
- package/src/runtime/loop-markers.ts +100 -0
- package/src/runtime/map-array-lazy.ts +470 -0
- package/src/runtime/map-array.ts +68 -11
- package/src/runtime/qsa-item.ts +9 -3
- package/src/runtime/registry.ts +5 -3
- package/dist/runtime/client-marker.d.ts +0 -21
- package/dist/runtime/client-marker.d.ts.map +0 -1
- package/dist/runtime/list.d.ts +0 -21
- package/dist/runtime/list.d.ts.map +0 -1
- package/dist/runtime/patch-slot-range.d.ts +0 -47
- package/dist/runtime/patch-slot-range.d.ts.map +0 -1
- package/dist/runtime/reconcile-elements.d.ts +0 -44
- package/dist/runtime/reconcile-elements.d.ts.map +0 -1
- package/src/runtime/client-marker.ts +0 -46
- package/src/runtime/list.ts +0 -47
- package/src/runtime/patch-slot-range.ts +0 -105
- package/src/runtime/reconcile-elements.ts +0 -391
package/src/runtime/component.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* BarefootJS - Component Creation
|
|
3
3
|
*
|
|
4
4
|
* Functions for dynamically creating component instances at runtime.
|
|
5
|
-
* Used by
|
|
5
|
+
* Used by mapArray()/mapArrayAnchored() when rendering components in loops.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getTemplate } from './template.ts'
|
|
@@ -25,14 +25,40 @@ export function setParentScopeId(id: string | null): void {
|
|
|
25
25
|
_parentScopeId = id
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
28
|
+
/** Where `mapArray` wants a fresh loop row connected before its `init` runs. */
|
|
29
|
+
export type RowMountPoint = { container: Node; anchor: Node | null }
|
|
30
|
+
|
|
31
|
+
// Ambient mount point for a loop row.
|
|
32
|
+
//
|
|
33
|
+
// A loop row created by `renderItem` has no placeholder to replace, so it
|
|
34
|
+
// cannot use `mountAt` and its `init` runs detached (see the known-limitation
|
|
35
|
+
// docstring in `__tests__/runtime/csr-loop-row-init-connected.test.ts`).
|
|
36
|
+
// `mapArray` sets this around the `renderItem` call so the OUTERMOST
|
|
37
|
+
// `createComponent` inside it connects the row at `container`/`anchor` before
|
|
38
|
+
// running `init` — same guarantee `mountAt` gives the child-slot path.
|
|
39
|
+
//
|
|
40
|
+
// Consumed once (take-and-clear) so nested `createComponent` calls made from
|
|
41
|
+
// the row's own init don't re-use the row's mount point.
|
|
42
|
+
//
|
|
43
|
+
// `setRowMountPoint` returns the previous value so a caller can restore it
|
|
44
|
+
// instead of clearing to `null`. That matters because the ambient is a single
|
|
45
|
+
// slot: a row whose own `init` drives a nested `mapArray` would otherwise have
|
|
46
|
+
// the inner list's teardown blank out an outer mount point that had not been
|
|
47
|
+
// consumed yet. Save-and-restore makes the slot behave like a stack without
|
|
48
|
+
// paying for one.
|
|
49
|
+
let _rowMountPoint: RowMountPoint | null = null
|
|
50
|
+
|
|
51
|
+
export function setRowMountPoint(p: RowMountPoint | null): RowMountPoint | null {
|
|
52
|
+
const prev = _rowMountPoint
|
|
53
|
+
_rowMountPoint = p
|
|
54
|
+
return prev
|
|
55
|
+
}
|
|
35
56
|
|
|
57
|
+
function takeRowMountPoint(): RowMountPoint | null {
|
|
58
|
+
const p = _rowMountPoint
|
|
59
|
+
_rowMountPoint = null
|
|
60
|
+
return p
|
|
61
|
+
}
|
|
36
62
|
|
|
37
63
|
/**
|
|
38
64
|
* Create a component instance with DOM element and initialized state.
|
|
@@ -47,6 +73,10 @@ const propsMap = new WeakMap<HTMLElement, Record<string, unknown>>()
|
|
|
47
73
|
* @param name - Component name (e.g., 'TodoItem')
|
|
48
74
|
* @param props - Props to pass to the component
|
|
49
75
|
* @param key - Optional key for list reconciliation
|
|
76
|
+
* @param slot - Slot-relationship markers stamped as `bf-h` / `bf-m`
|
|
77
|
+
* @param mountAt - Placeholder this component replaces. When given, the
|
|
78
|
+
* element is connected to the document *before* `init` runs — see
|
|
79
|
+
* "Connect before init" below.
|
|
50
80
|
* @returns Created DOM element
|
|
51
81
|
*
|
|
52
82
|
* @example
|
|
@@ -77,15 +107,49 @@ export function createComponent(
|
|
|
77
107
|
props: Record<string, unknown> = {},
|
|
78
108
|
key?: string | number,
|
|
79
109
|
slot?: CreateComponentSlotInfo,
|
|
110
|
+
mountAt?: Element | null,
|
|
111
|
+
): HTMLElement {
|
|
112
|
+
const element = materializeComponent(nameOrDef, props, key, slot, mountAt)
|
|
113
|
+
// `mountAt` is an unconditional obligation: callers used to run
|
|
114
|
+
// `ph.replaceWith(comp)` themselves on every outcome, so every path that
|
|
115
|
+
// did NOT consume the placeholder still owes the replacement — a missing or
|
|
116
|
+
// empty template (either mode), and the root-deferred-placeholder shape,
|
|
117
|
+
// which must stay detached so its self-replacement stays recoverable.
|
|
118
|
+
// `parentNode` (not `isConnected`) is the right "still unconsumed" probe: it
|
|
119
|
+
// survives a `mountAt` that was itself detached, which is the normal case
|
|
120
|
+
// during multi-root loop-body setup.
|
|
121
|
+
if (mountAt && mountAt.parentNode && element !== mountAt) {
|
|
122
|
+
mountAt.replaceWith(element)
|
|
123
|
+
}
|
|
124
|
+
return element
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build the element, connect it at `mountAt` when there is one, and run
|
|
129
|
+
* `init` — in that order, which is the whole point (see step 7b).
|
|
130
|
+
*
|
|
131
|
+
* Named "materialize" rather than anything with "unmounted" in it because it
|
|
132
|
+
* DOES mount: the connect has to happen inside, before `init`, and only the
|
|
133
|
+
* paths that cannot consume the placeholder leave that to `createComponent`.
|
|
134
|
+
*/
|
|
135
|
+
function materializeComponent(
|
|
136
|
+
nameOrDef: string | ComponentDef,
|
|
137
|
+
props: Record<string, unknown> = {},
|
|
138
|
+
key?: string | number,
|
|
139
|
+
slot?: CreateComponentSlotInfo,
|
|
140
|
+
mountAt?: Element | null,
|
|
80
141
|
): HTMLElement {
|
|
81
142
|
// A bare callable shim invoked from user code (e.g. an object-literal
|
|
82
143
|
// value `LOGOS[id]()` whose arrow the compiler hoisted into a component)
|
|
83
144
|
// reaches us with no props (#1663). Normalize to an empty object so the
|
|
84
145
|
// descriptor probes below don't throw on `undefined`.
|
|
85
146
|
if (props == null) props = {}
|
|
147
|
+
// Take the row mount point BEFORE any template eval / init can run, so the
|
|
148
|
+
// outermost call for the row is the only one that can consume it.
|
|
149
|
+
const rowMount = mountAt ? null : takeRowMountPoint()
|
|
86
150
|
// ComponentDef mode: use def directly instead of registry lookup
|
|
87
151
|
if (typeof nameOrDef !== 'string') {
|
|
88
|
-
return createComponentFromDef(nameOrDef, props, key)
|
|
152
|
+
return createComponentFromDef(nameOrDef, props, key, mountAt, rowMount)
|
|
89
153
|
}
|
|
90
154
|
|
|
91
155
|
const name = nameOrDef
|
|
@@ -182,6 +246,35 @@ export function createComponent(
|
|
|
182
246
|
element.setAttribute(BF_KEY, String(key))
|
|
183
247
|
}
|
|
184
248
|
|
|
249
|
+
// 7b. Connect before init.
|
|
250
|
+
//
|
|
251
|
+
// `initFn` resolves context by DOM position (`useContext` walks
|
|
252
|
+
// `parentElement` from the current scope) and may measure layout. Both
|
|
253
|
+
// need this element to be in the document, so when the caller told us
|
|
254
|
+
// which placeholder we replace, do the replacement NOW rather than
|
|
255
|
+
// after init. Running init detached made `useContext` fall through to
|
|
256
|
+
// the global, last-writer-wins context store, so a child materialised
|
|
257
|
+
// after a sibling provider had run picked up the wrong provider's
|
|
258
|
+
// value. This aligns the CSR path with the SSR one, where the
|
|
259
|
+
// doc-order walker only ever inits elements already in the document
|
|
260
|
+
// (`hydrate.ts`).
|
|
261
|
+
//
|
|
262
|
+
// A root-level deferred placeholder is excluded: its init replaces
|
|
263
|
+
// `element` itself, which the block below recovers via a throwaway
|
|
264
|
+
// wrapper. Connecting first would make that replacement happen in the
|
|
265
|
+
// live DOM with no handle on the result, so this shape keeps the
|
|
266
|
+
// detached behaviour.
|
|
267
|
+
const rootIsDeferredPlaceholder = element.hasAttribute(BF_PLACEHOLDER)
|
|
268
|
+
if (mountAt && !rootIsDeferredPlaceholder) {
|
|
269
|
+
mountAt.replaceWith(element)
|
|
270
|
+
} else if (rowMount && !rootIsDeferredPlaceholder) {
|
|
271
|
+
// Loop row: no placeholder exists, so connect at the position `mapArray`
|
|
272
|
+
// handed down. The reorder step may move the row afterwards; any position
|
|
273
|
+
// inside the container yields the same ancestor chain, which is all
|
|
274
|
+
// `useContext`'s parentElement walk needs.
|
|
275
|
+
rowMount.container.insertBefore(element, rowMount.anchor)
|
|
276
|
+
}
|
|
277
|
+
|
|
185
278
|
// 8. Set currentScope so provideContext/useContext are element-scoped.
|
|
186
279
|
// This allows context providers in initFn to store context on this element.
|
|
187
280
|
const prevScope = setCurrentScope(element)
|
|
@@ -193,7 +286,6 @@ export function createComponent(
|
|
|
193
286
|
// `replaceWith` — but a detached root node can't replace itself in
|
|
194
287
|
// place. Park it in a throwaway wrapper so the replacement lands
|
|
195
288
|
// somewhere we can recover, then return the materialised child.
|
|
196
|
-
const rootIsDeferredPlaceholder = element.hasAttribute(BF_PLACEHOLDER)
|
|
197
289
|
let placeholderWrapper: HTMLElement | null = null
|
|
198
290
|
if (rootIsDeferredPlaceholder) {
|
|
199
291
|
placeholderWrapper = parseHTML('<div></div>').firstChild as HTMLElement
|
|
@@ -214,14 +306,12 @@ export function createComponent(
|
|
|
214
306
|
if (materialised && !materialised.hasAttribute(BF_PLACEHOLDER)) {
|
|
215
307
|
// The deferred child was created in place of the placeholder.
|
|
216
308
|
// `materialised` is the child's OWN element, created via
|
|
217
|
-
// upsertChild -> createComponent, which already
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// the
|
|
223
|
-
// whose placeholder is already gone could not re-materialise. So just
|
|
224
|
-
// restore the scope and return the already-registered child.
|
|
309
|
+
// upsertChild -> createComponent, which already marked itself
|
|
310
|
+
// hydrated with its own props. We must NOT re-run this function's
|
|
311
|
+
// own registration steps on it here — that would re-run the
|
|
312
|
+
// *parent's* init on an element whose placeholder is already gone
|
|
313
|
+
// and could not re-materialise. So just restore the scope and
|
|
314
|
+
// return the already-registered child.
|
|
225
315
|
// (Parent-scope effects are unaffected: createEffect ownership lives
|
|
226
316
|
// in the EffectContext tree, not the discarded placeholder element.)
|
|
227
317
|
setCurrentScope(prevScope)
|
|
@@ -248,51 +338,9 @@ export function createComponent(
|
|
|
248
338
|
// 12. Mark element as initialized
|
|
249
339
|
hydratedScopes.add(element)
|
|
250
340
|
|
|
251
|
-
// 13. Store props and register update function for element reuse in reconcileList
|
|
252
|
-
propsMap.set(element, props)
|
|
253
|
-
registerPropsUpdate(element, name, props)
|
|
254
|
-
|
|
255
341
|
return element
|
|
256
342
|
}
|
|
257
343
|
|
|
258
|
-
/**
|
|
259
|
-
* Get the props stored for a component element.
|
|
260
|
-
* Used by reconcileList to pass props to an existing element.
|
|
261
|
-
*/
|
|
262
|
-
export function getComponentProps(element: HTMLElement): Record<string, unknown> | undefined {
|
|
263
|
-
return propsMap.get(element)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Register a props update function for a component element.
|
|
268
|
-
* When called, this function re-initializes the component with new props.
|
|
269
|
-
*/
|
|
270
|
-
function registerPropsUpdate(
|
|
271
|
-
element: HTMLElement,
|
|
272
|
-
name: string,
|
|
273
|
-
_initialProps: Record<string, unknown>
|
|
274
|
-
): void {
|
|
275
|
-
// Register update function that will be called by reconcileList
|
|
276
|
-
propsUpdateMap.set(element, (newProps: Record<string, unknown>) => {
|
|
277
|
-
// Re-initialize the component with new props
|
|
278
|
-
// This allows the component to capture new values (e.g., todo with editing: true)
|
|
279
|
-
// and set up new effects that reference the new values
|
|
280
|
-
const init = getComponentInit(name)
|
|
281
|
-
if (init) {
|
|
282
|
-
init(element, newProps)
|
|
283
|
-
}
|
|
284
|
-
})
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Get the props update function for an element.
|
|
289
|
-
* Used by reconcileList to update props when reusing an element.
|
|
290
|
-
*/
|
|
291
|
-
export function getPropsUpdateFn(element: HTMLElement): ((props: Record<string, unknown>) => void) | undefined {
|
|
292
|
-
return propsUpdateMap.get(element)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
344
|
/**
|
|
297
345
|
* Render a child component's template to an HTML string.
|
|
298
346
|
* Used by compiler-generated template functions when a stateless component
|
|
@@ -471,18 +519,44 @@ export function escapeAttr(value: unknown): string {
|
|
|
471
519
|
*
|
|
472
520
|
* A nullish value renders as empty text — the JSX/Solid semantics the Hono
|
|
473
521
|
* SSR reference follows (`{undefined}` / `{null}` produce no text), and
|
|
474
|
-
* what the reactive text-update path already does (`
|
|
475
|
-
* `
|
|
476
|
-
* escape site used to stringify `undefined` /
|
|
477
|
-
* "undefined" / "null" text, so a bare `{props.x}` on
|
|
478
|
-
* diverged from SSR at first paint (#2137). Non-nullish
|
|
479
|
-
* `0` and `false`) keep their `String()` form, matching
|
|
522
|
+
* what the reactive text-update path already does (`claim-slots.ts`'s
|
|
523
|
+
* `writeText`/`writeMarkup` and `dynamic-text.ts` all `String(value ?? '')`).
|
|
524
|
+
* Only this initial-render escape site used to stringify `undefined` /
|
|
525
|
+
* `null` into literal "undefined" / "null" text, so a bare `{props.x}` on
|
|
526
|
+
* an absent prop diverged from SSR at first paint (#2137). Non-nullish
|
|
527
|
+
* values (including `0` and `false`) keep their `String()` form, matching
|
|
528
|
+
* the reactive path.
|
|
480
529
|
*/
|
|
481
530
|
export function escapeText(value: unknown): string {
|
|
482
531
|
if (value == null) return ''
|
|
483
532
|
return escapeAttr(value)
|
|
484
533
|
}
|
|
485
534
|
|
|
535
|
+
/**
|
|
536
|
+
* `escapeText`'s counterpart for a claimed 'markup' slot's REACTIVE write
|
|
537
|
+
* (slot unification A3 follow-up), where the value is a plain-JS expression
|
|
538
|
+
* that may resolve to either a string or a live `Node` (e.g. `{cond &&
|
|
539
|
+
* logo(id)}`, a hoisted `renderNode` callback, #1213). `writeMarkup`
|
|
540
|
+
* (`claim-slots.ts`) inserts a string via `<template>.innerHTML =`, which —
|
|
541
|
+
* unlike the old `__bfText`'s plain `Text.nodeValue =` assignment — DOES
|
|
542
|
+
* interpret HTML, so a raw un-escaped string is an injection/corruption
|
|
543
|
+
* risk exactly where the initial SSR/CSR TEMPLATE already calls
|
|
544
|
+
* `escapeText` on the same expression (`html-template.ts`'s
|
|
545
|
+
* `escapeTextSlotExpr`). A live `Node`, by contrast, must pass through
|
|
546
|
+
* untouched — `escapeText(node)` would stringify it to garbage, and
|
|
547
|
+
* `writeMarkup`'s own `instanceof Node` check needs the real object to
|
|
548
|
+
* splice in by identity. This is the single call every "dynamic JSX/text
|
|
549
|
+
* slot, value may be a Node" emission site (`emit-reactive.ts`,
|
|
550
|
+
* `stringify/loop-child-arm.ts`, `stringify/insert.ts`) wraps the value in
|
|
551
|
+
* before handing it to a 'markup' writer — NOT the preamble-region case
|
|
552
|
+
* (`stringify/loop.ts`), whose value is already-built HTML from a nested
|
|
553
|
+
* compiled render and must stay unescaped.
|
|
554
|
+
*/
|
|
555
|
+
export function escapeTextOrNode(value: unknown): string | Node {
|
|
556
|
+
if (typeof Node !== 'undefined' && value instanceof Node) return value
|
|
557
|
+
return escapeText(value)
|
|
558
|
+
}
|
|
559
|
+
|
|
486
560
|
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
487
561
|
|
|
488
562
|
/**
|
|
@@ -559,7 +633,9 @@ function insertGetterChildren(element: HTMLElement, children: unknown): void {
|
|
|
559
633
|
function createComponentFromDef(
|
|
560
634
|
def: ComponentDef,
|
|
561
635
|
props: Record<string, unknown>,
|
|
562
|
-
key?: string | number
|
|
636
|
+
key?: string | number,
|
|
637
|
+
mountAt?: Element | null,
|
|
638
|
+
rowMount?: { container: Node; anchor: Node | null } | null,
|
|
563
639
|
): HTMLElement {
|
|
564
640
|
if (!def.template) {
|
|
565
641
|
throw new Error('[BarefootJS] createComponent with ComponentDef requires a template function')
|
|
@@ -587,14 +663,21 @@ function createComponentFromDef(
|
|
|
587
663
|
element.setAttribute(BF_KEY, String(key))
|
|
588
664
|
}
|
|
589
665
|
|
|
666
|
+
// Connect before init, for the same reason the registry path does (see
|
|
667
|
+
// `materializeComponent` step 7b): `def.init` may resolve context by DOM
|
|
668
|
+
// position or measure layout, and neither works detached. Keeps `mountAt`
|
|
669
|
+
// one contract across both modes instead of a registry-only guarantee.
|
|
670
|
+
if (mountAt) {
|
|
671
|
+
mountAt.replaceWith(element)
|
|
672
|
+
} else if (rowMount) {
|
|
673
|
+
rowMount.container.insertBefore(element, rowMount.anchor)
|
|
674
|
+
}
|
|
675
|
+
|
|
590
676
|
// Initialize
|
|
591
677
|
def.init(element, props)
|
|
592
678
|
|
|
593
679
|
// Mark as initialized
|
|
594
680
|
hydratedScopes.add(element)
|
|
595
681
|
|
|
596
|
-
// Store props for element reuse
|
|
597
|
-
propsMap.set(element, props)
|
|
598
|
-
|
|
599
682
|
return element
|
|
600
683
|
}
|
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dynamic text/JSX slot updater (#1663).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Slot unification A3 (`spec/slot-unification.md` §5-A3) replaced every
|
|
5
|
+
* OTHER `__bfText` call site with a claimed 'markup' slot writer
|
|
6
|
+
* (`claim-slots.ts`'s `writeMarkup` provides the identical Node/text
|
|
7
|
+
* contract). ONE emission site still calls `__bfText` directly and is
|
|
8
|
+
* deliberately deferred: `emitDynamicTextUpdates`'s `conditionalElems`
|
|
9
|
+
* path (`ir-to-client-js/emit-reactive.ts`) — a dynamic text/JSX
|
|
10
|
+
* expression nested inside a top-level (non-loop) conditional, tracked by
|
|
11
|
+
* an effect OUTSIDE the conditional's own `insert()` `bindEvents`. That
|
|
12
|
+
* effect re-resolves its anchor via `$t(__scope, slotId)` on EVERY run
|
|
13
|
+
* because `insert()` may swap the branch independently of this effect's own
|
|
14
|
+
* reruns — a cached `lazySlots` claim would go stale across such a swap, and
|
|
15
|
+
* a 'markup' slot's dedup `last` state can't safely survive being re-claimed
|
|
16
|
+
* fresh every run either: a fresh claim's `last` always starts `undefined`,
|
|
17
|
+
* so re-claiming per-run would throw away the dedup skip on every single
|
|
18
|
+
* run (every write would re-clear-and-reparse even when the value hasn't
|
|
19
|
+
* changed) — unlike the 'text'-kind conditional cases elsewhere in the
|
|
20
|
+
* compiler, which have no such state to go stale and so DO re-claim fresh
|
|
21
|
+
* each run safely. Moving this one case onto the claim-plan model needs the
|
|
22
|
+
* slot's claim door tied to the branch's OWN activation lifecycle instead of
|
|
23
|
+
* this separate effect's — real architectural work, not a mechanical swap —
|
|
24
|
+
* so it stays on `$t`/`__bfText` for now.
|
|
25
|
+
*
|
|
26
|
+
* The mechanism itself, for the reader who lands here from that one site:
|
|
27
|
+
* the compiler wraps reactive child expressions (`<div>{expr}</div>`) in a
|
|
5
28
|
* `createEffect` that writes the value into the text node sitting between
|
|
6
29
|
* the slot's `<!--bf:sX-->` / `<!--/-->` comment markers. That was a pure
|
|
7
30
|
* `nodeValue = String(value)` assignment, which is correct for primitives
|
package/src/runtime/index.ts
CHANGED
|
@@ -77,13 +77,21 @@ export {
|
|
|
77
77
|
type PortalChildren,
|
|
78
78
|
} from './portal.ts'
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
export {
|
|
80
|
+
// Loop boundary marker lookup (used by mapArray/mapArrayAnchored consumers
|
|
81
|
+
// and compiler-generated clearing code — see ./loop-markers.ts docstring)
|
|
82
|
+
export { getLoopChildren, getLoopNodes } from './loop-markers.ts'
|
|
83
83
|
export { qsaItem, upsertChildItem } from './qsa-item.ts'
|
|
84
84
|
export { mapArray, mapArrayAnchored } from './map-array.ts'
|
|
85
|
+
// Lazy row graph (slot unification §9, L2) — keyed list rendering with no
|
|
86
|
+
// per-row reactive resources; compiler targets it for eligible plain loops
|
|
87
|
+
// (L3). See ./map-array-lazy.ts for the pinned row-plan contract.
|
|
88
|
+
export { mapArrayLazy, type LazyRowEntry, type LazyRowPlan } from './map-array-lazy.ts'
|
|
85
89
|
export { patchLeaf } from './patch-leaf.ts'
|
|
86
|
-
|
|
90
|
+
|
|
91
|
+
// Claim-plan interpreter (slot unification A2/A3, spec/slot-unification.md)
|
|
92
|
+
// — the ONE content-slot update mechanism, wired up by the compiler in A3.
|
|
93
|
+
// Supersedes (deleted) `patchSlotRange` and `updateClientMarker`.
|
|
94
|
+
export { claimSlots, lazySlots, lazyClaimSlots, textOrNode, type SlotSpec, type ClaimPlan, type ClaimedSlots, type ClaimedSlotsRW, type SlotWriter } from './claim-slots.ts'
|
|
87
95
|
|
|
88
96
|
// Template registry
|
|
89
97
|
export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './template.ts'
|
|
@@ -92,11 +100,10 @@ export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './t
|
|
|
92
100
|
export {
|
|
93
101
|
createComponent,
|
|
94
102
|
renderChild,
|
|
95
|
-
getPropsUpdateFn,
|
|
96
|
-
getComponentProps,
|
|
97
103
|
parseHTML,
|
|
98
104
|
escapeAttr,
|
|
99
105
|
escapeText,
|
|
106
|
+
escapeTextOrNode,
|
|
100
107
|
} from './component.ts'
|
|
101
108
|
|
|
102
109
|
// Spread props helpers
|
|
@@ -111,8 +118,14 @@ export { hydrate, rehydrateAll, rehydrateScope, disposeScope, flushHydration, ge
|
|
|
111
118
|
export { registerComponent, getComponentInit, initChild, upsertChild } from './registry.ts'
|
|
112
119
|
export { insert, type BranchConfig, type BranchTemplateResult } from './insert.ts'
|
|
113
120
|
export { __bfSlot } from './branch-slot.ts'
|
|
121
|
+
// `__bfText` (dynamic-text.ts) and `$t` (query.ts) are kept: one narrow
|
|
122
|
+
// emission site — a `@client`-nested-inside-a-top-level-conditional dynamic
|
|
123
|
+
// text/JSX expression (`emitDynamicTextUpdates`'s `conditionalElems` path in
|
|
124
|
+
// `ir-to-client-js/emit-reactive.ts`) still uses them, deferred from slot
|
|
125
|
+
// unification A3 (see that function's docstring for why the claim-plan
|
|
126
|
+
// model doesn't fit that one case cleanly). `updateClientMarker` had no
|
|
127
|
+
// such holdout and is fully deleted.
|
|
114
128
|
export { __bfText } from './dynamic-text.ts'
|
|
115
|
-
export { updateClientMarker } from './client-marker.ts'
|
|
116
129
|
|
|
117
130
|
// Hydration state
|
|
118
131
|
export { hydratedScopes } from './hydration-state.ts'
|
package/src/runtime/insert.ts
CHANGED
|
@@ -338,7 +338,7 @@ export function insert(
|
|
|
338
338
|
function autoFocusConditionalElement(region: CondRegion, id: string): void {
|
|
339
339
|
// Use requestAnimationFrame to defer focus until after DOM updates.
|
|
340
340
|
// This is necessary because createComponent() may call insert() before
|
|
341
|
-
// the element is added to the document by
|
|
341
|
+
// the element is added to the document by mapArray()/mapArrayAnchored().
|
|
342
342
|
requestAnimationFrame(() => {
|
|
343
343
|
const condEl = region.anchor
|
|
344
344
|
? findCondElInRange(region.anchor, id)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS - Loop Boundary Marker Lookup
|
|
3
|
+
*
|
|
4
|
+
* `<!--bf-loop:<id>-->` / `<!--bf-/loop:<id>-->` comment markers delimit a
|
|
5
|
+
* loop's rendered range inside its container so `mapArray`/`mapArrayAnchored`
|
|
6
|
+
* (`./map-array.ts`) can reconcile only that range without disturbing
|
|
7
|
+
* non-loop siblings. These lookups used to also back the now-removed
|
|
8
|
+
* `reconcileElements`/`reconcileList` element-reconciler (slot unification
|
|
9
|
+
* A4); they remain as the compiler-facing marker API.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { BF_LOOP_START, BF_LOOP_END, loopStartMarker, loopEndMarker } from '@barefootjs/shared'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find loop boundary comment markers in a container.
|
|
16
|
+
*
|
|
17
|
+
* `markerId` scopes the lookup to `<!--bf-loop:<id>-->` / `<!--bf-/loop:<id>-->`
|
|
18
|
+
* so sibling loops under the same parent disambiguate (#1087). Without an id,
|
|
19
|
+
* accepts the legacy unscoped form too — used by tests that build containers
|
|
20
|
+
* without compiler-emitted markers.
|
|
21
|
+
*/
|
|
22
|
+
function findLoopMarkers(
|
|
23
|
+
container: HTMLElement,
|
|
24
|
+
markerId?: string,
|
|
25
|
+
): { startMarker: Comment | null; endMarker: Comment | null } {
|
|
26
|
+
let startMarker: Comment | null = null
|
|
27
|
+
let endMarker: Comment | null = null
|
|
28
|
+
if (markerId) {
|
|
29
|
+
const startVal = loopStartMarker(markerId)
|
|
30
|
+
const endVal = loopEndMarker(markerId)
|
|
31
|
+
for (const node of Array.from(container.childNodes)) {
|
|
32
|
+
if (node.nodeType !== Node.COMMENT_NODE) continue
|
|
33
|
+
const value = (node as Comment).nodeValue
|
|
34
|
+
if (value === startVal) startMarker = node as Comment
|
|
35
|
+
else if (value === endVal) endMarker = node as Comment
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const startPrefix = `${BF_LOOP_START}:`
|
|
39
|
+
const endPrefix = `${BF_LOOP_END}:`
|
|
40
|
+
for (const node of Array.from(container.childNodes)) {
|
|
41
|
+
if (node.nodeType !== Node.COMMENT_NODE) continue
|
|
42
|
+
const value = (node as Comment).nodeValue ?? ''
|
|
43
|
+
if (!startMarker && (value === BF_LOOP_START || value.startsWith(startPrefix))) {
|
|
44
|
+
startMarker = node as Comment
|
|
45
|
+
} else if (!endMarker && (value === BF_LOOP_END || value.startsWith(endPrefix))) {
|
|
46
|
+
endMarker = node as Comment
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (startMarker && endMarker) return { startMarker, endMarker }
|
|
51
|
+
return { startMarker: null, endMarker: null }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Get all Element nodes between start and end comment markers. */
|
|
55
|
+
function getElementsBetweenMarkers(start: Comment, end: Comment): Element[] {
|
|
56
|
+
const elements: Element[] = []
|
|
57
|
+
let node: Node | null = start.nextSibling
|
|
58
|
+
while (node && node !== end) {
|
|
59
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
60
|
+
elements.push(node as Element)
|
|
61
|
+
}
|
|
62
|
+
node = node.nextSibling
|
|
63
|
+
}
|
|
64
|
+
return elements
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get loop children from a container, respecting bf-loop boundary markers.
|
|
69
|
+
* When markers are present, returns only elements between them.
|
|
70
|
+
* When absent, returns all children (backward compatible).
|
|
71
|
+
* Exported for use by compiler-generated hydration code.
|
|
72
|
+
*/
|
|
73
|
+
export function getLoopChildren(container: HTMLElement, markerId?: string): HTMLElement[] {
|
|
74
|
+
const { startMarker, endMarker } = findLoopMarkers(container, markerId)
|
|
75
|
+
if (startMarker && endMarker) {
|
|
76
|
+
return getElementsBetweenMarkers(startMarker, endMarker) as HTMLElement[]
|
|
77
|
+
}
|
|
78
|
+
return Array.from(container.children) as HTMLElement[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Like {@link getLoopChildren}, but returns every node between the loop
|
|
83
|
+
* boundary markers — Comments (per-item `<!--bf-loop-i-->` markers) and
|
|
84
|
+
* text included. The branch-clearing path needs to remove the per-item
|
|
85
|
+
* marker comments alongside elements; otherwise stale markers would
|
|
86
|
+
* accumulate when a branch swap forces mapArray to start over (#1212).
|
|
87
|
+
*/
|
|
88
|
+
export function getLoopNodes(container: HTMLElement, markerId?: string): Node[] {
|
|
89
|
+
const { startMarker, endMarker } = findLoopMarkers(container, markerId)
|
|
90
|
+
const nodes: Node[] = []
|
|
91
|
+
if (startMarker && endMarker) {
|
|
92
|
+
let node: Node | null = startMarker.nextSibling
|
|
93
|
+
while (node && node !== endMarker) {
|
|
94
|
+
nodes.push(node)
|
|
95
|
+
node = node.nextSibling
|
|
96
|
+
}
|
|
97
|
+
return nodes
|
|
98
|
+
}
|
|
99
|
+
return Array.from(container.childNodes)
|
|
100
|
+
}
|