@barefootjs/client 0.1.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/build.d.ts +56 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +76 -0
- package/dist/context.d.ts +25 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/csr-adapter.d.ts +26 -0
- package/dist/csr-adapter.d.ts.map +1 -0
- package/dist/forward-props.d.ts +17 -0
- package/dist/forward-props.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -0
- package/dist/reactive.d.ts +150 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +215 -0
- package/dist/runtime/apply-rest-attrs.d.ts +16 -0
- package/dist/runtime/apply-rest-attrs.d.ts.map +1 -0
- package/dist/runtime/branch-slot.d.ts +22 -0
- package/dist/runtime/branch-slot.d.ts.map +1 -0
- package/dist/runtime/client-marker.d.ts +21 -0
- package/dist/runtime/client-marker.d.ts.map +1 -0
- package/dist/runtime/component.d.ts +99 -0
- package/dist/runtime/component.d.ts.map +1 -0
- package/dist/runtime/context.d.ts +40 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/hydrate.d.ts +100 -0
- package/dist/runtime/hydrate.d.ts.map +1 -0
- package/dist/runtime/hydration-state.d.ts +13 -0
- package/dist/runtime/hydration-state.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +27 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2093 -0
- package/dist/runtime/insert.d.ts +75 -0
- package/dist/runtime/insert.d.ts.map +1 -0
- package/dist/runtime/list.d.ts +21 -0
- package/dist/runtime/list.d.ts.map +1 -0
- package/dist/runtime/map-array.d.ts +32 -0
- package/dist/runtime/map-array.d.ts.map +1 -0
- package/dist/runtime/portal.d.ts +96 -0
- package/dist/runtime/portal.d.ts.map +1 -0
- package/dist/runtime/qsa-item.d.ts +52 -0
- package/dist/runtime/qsa-item.d.ts.map +1 -0
- package/dist/runtime/query.d.ts +86 -0
- package/dist/runtime/query.d.ts.map +1 -0
- package/dist/runtime/reconcile-elements.d.ts +44 -0
- package/dist/runtime/reconcile-elements.d.ts.map +1 -0
- package/dist/runtime/registry.d.ts +53 -0
- package/dist/runtime/registry.d.ts.map +1 -0
- package/dist/runtime/render.d.ts +35 -0
- package/dist/runtime/render.d.ts.map +1 -0
- package/dist/runtime/scope.d.ts +28 -0
- package/dist/runtime/scope.d.ts.map +1 -0
- package/dist/runtime/slot-resolver.d.ts +36 -0
- package/dist/runtime/slot-resolver.d.ts.map +1 -0
- package/dist/runtime/spread-attrs.d.ts +19 -0
- package/dist/runtime/spread-attrs.d.ts.map +1 -0
- package/dist/runtime/standalone.js +2278 -0
- package/dist/runtime/streaming.d.ts +36 -0
- package/dist/runtime/streaming.d.ts.map +1 -0
- package/dist/runtime/style.d.ts +17 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/template.d.ts +39 -0
- package/dist/runtime/template.d.ts.map +1 -0
- package/dist/runtime/types.d.ts +26 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/shims.d.ts +21 -0
- package/dist/shims.d.ts.map +1 -0
- package/dist/slot.d.ts +14 -0
- package/dist/slot.d.ts.map +1 -0
- package/dist/split-props.d.ts +26 -0
- package/dist/split-props.d.ts.map +1 -0
- package/dist/unwrap.d.ts +16 -0
- package/dist/unwrap.d.ts.map +1 -0
- package/package.json +71 -0
- package/src/build.ts +92 -0
- package/src/context.ts +33 -0
- package/src/csr-adapter.ts +134 -0
- package/src/forward-props.ts +43 -0
- package/src/index.ts +42 -0
- package/src/reactive.ts +411 -0
- package/src/runtime/apply-rest-attrs.ts +109 -0
- package/src/runtime/branch-slot.ts +32 -0
- package/src/runtime/client-marker.ts +46 -0
- package/src/runtime/component.ts +501 -0
- package/src/runtime/context.ts +111 -0
- package/src/runtime/hydrate.ts +311 -0
- package/src/runtime/hydration-state.ts +13 -0
- package/src/runtime/index.ts +96 -0
- package/src/runtime/insert.ts +407 -0
- package/src/runtime/list.ts +47 -0
- package/src/runtime/map-array.ts +381 -0
- package/src/runtime/portal.ts +174 -0
- package/src/runtime/qsa-item.ts +128 -0
- package/src/runtime/query.ts +632 -0
- package/src/runtime/reconcile-elements.ts +391 -0
- package/src/runtime/registry.ts +160 -0
- package/src/runtime/render.ts +105 -0
- package/src/runtime/scope.ts +46 -0
- package/src/runtime/slot-resolver.ts +66 -0
- package/src/runtime/spread-attrs.ts +88 -0
- package/src/runtime/streaming.ts +65 -0
- package/src/runtime/style.ts +27 -0
- package/src/runtime/template.ts +53 -0
- package/src/runtime/types.ts +27 -0
- package/src/shims.ts +54 -0
- package/src/slot.ts +23 -0
- package/src/split-props.ts +86 -0
- package/src/unwrap.ts +18 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS - Conditional Insert
|
|
3
|
+
*
|
|
4
|
+
* Handle conditional DOM updates using branch configurations.
|
|
5
|
+
* SolidJS-inspired replacement for legacy cond() that properly
|
|
6
|
+
* handles event binding for both branches.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createEffect, untrack } from '@barefootjs/client/reactive'
|
|
10
|
+
import { find } from './query'
|
|
11
|
+
import { setParentScopeId, parseHTML } from './component'
|
|
12
|
+
import { BF_COND, BF_SCOPE } from '@barefootjs/shared'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Result returned by a branch's `template()` when the template captures
|
|
16
|
+
* live DOM nodes via `__bfSlot` (#1213). `html` carries the marker-bearing
|
|
17
|
+
* HTML string; `slots[N]` is the actual `Node` referenced by the
|
|
18
|
+
* `<!--bf-slot:N-->` placeholder at the same index.
|
|
19
|
+
*/
|
|
20
|
+
export interface BranchTemplateResult {
|
|
21
|
+
html: string
|
|
22
|
+
slots: Node[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Branch configuration for conditional rendering.
|
|
27
|
+
* Contains template and event binding functions for each branch.
|
|
28
|
+
*/
|
|
29
|
+
export interface BranchConfig {
|
|
30
|
+
/**
|
|
31
|
+
* HTML template function for this branch. Returns either a plain HTML
|
|
32
|
+
* string (legacy) or a `{ html, slots }` pair for templates that
|
|
33
|
+
* captured live `Node` values via `__bfSlot`.
|
|
34
|
+
*
|
|
35
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
36
|
+
* INVARIANT — TEMPLATES RUN WITH REACTIVITY UNTRACKED.
|
|
37
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
38
|
+
*
|
|
39
|
+
* Every call site goes through `evalBranchTemplate()` in this file,
|
|
40
|
+
* which wraps the invocation in `untrack()`. Signal reads inside
|
|
41
|
+
* the template are therefore NOT registered as effect dependencies.
|
|
42
|
+
*
|
|
43
|
+
* Consequences for authors of new branch shapes:
|
|
44
|
+
*
|
|
45
|
+
* - `template()` must produce a function of state-at-call-time only.
|
|
46
|
+
* Any reactive portion of the rendered fragment is wired up
|
|
47
|
+
* afterwards by `bindEvents()` (events + per-binding effects) and
|
|
48
|
+
* `__bfSlot` (live-Node splicing for slot-captured signals).
|
|
49
|
+
*
|
|
50
|
+
* - A template such as `() => signalA() ? '<a>' : '<b>'` is a BUG:
|
|
51
|
+
* later changes to `signalA` will not re-evaluate the template,
|
|
52
|
+
* because the read was performed without tracking. Branch
|
|
53
|
+
* selection belongs in the `conditionFn` argument of `insert()`,
|
|
54
|
+
* not inside the template body.
|
|
55
|
+
*/
|
|
56
|
+
template: () => string | BranchTemplateResult
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Bind events and reactive effects to elements within the branch.
|
|
60
|
+
* Called both during hydration (for SSR elements) and after DOM swaps.
|
|
61
|
+
* @param scope - The scope element to search within for event targets
|
|
62
|
+
* @returns Optional cleanup function, called when the branch is deactivated.
|
|
63
|
+
* Used to dispose reactive effects scoped to this branch.
|
|
64
|
+
*/
|
|
65
|
+
bindEvents: (scope: Element, opts?: { isFirstRun?: boolean }) => (() => void) | void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const EMPTY_SLOTS: Node[] = []
|
|
69
|
+
|
|
70
|
+
function normalizeTemplate(value: string | BranchTemplateResult): BranchTemplateResult {
|
|
71
|
+
return typeof value === 'string' ? { html: value, slots: EMPTY_SLOTS } : value
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Single chokepoint for every `branch.template()` call in this module —
|
|
76
|
+
* routes the invocation through `untrack()` so the contract on
|
|
77
|
+
* `BranchConfig.template` cannot be locally bypassed.
|
|
78
|
+
*
|
|
79
|
+
* Reads inside the template would otherwise be attributed to whatever
|
|
80
|
+
* effect is the active Listener when `insert()` runs, causing duplicate
|
|
81
|
+
* inner constructs (notably duplicate `mapArray` instances) when an
|
|
82
|
+
* outer effect re-runs and re-invokes `insert()`.
|
|
83
|
+
*
|
|
84
|
+
* New `template()` call sites: route through here, never call directly.
|
|
85
|
+
*/
|
|
86
|
+
function evalBranchTemplate(branch: BranchConfig): BranchTemplateResult {
|
|
87
|
+
return untrack(() => normalizeTemplate(branch.template()))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handle conditional DOM updates using branch configurations.
|
|
93
|
+
*
|
|
94
|
+
* Key behaviors:
|
|
95
|
+
* - First run (hydration): Reuse SSR element, call branch.bindEvents() for current branch
|
|
96
|
+
* - Condition change: Create new element from template, call branch.bindEvents()
|
|
97
|
+
*
|
|
98
|
+
* @param scope - Component scope element
|
|
99
|
+
* @param id - Conditional slot ID (e.g., 's0')
|
|
100
|
+
* @param conditionFn - Function that returns current condition value
|
|
101
|
+
* @param whenTrue - Branch config for when condition is true
|
|
102
|
+
* @param whenFalse - Branch config for when condition is false
|
|
103
|
+
*/
|
|
104
|
+
export function insert(
|
|
105
|
+
scope: Element | null,
|
|
106
|
+
id: string,
|
|
107
|
+
conditionFn: () => boolean,
|
|
108
|
+
whenTrue: BranchConfig,
|
|
109
|
+
whenFalse: BranchConfig
|
|
110
|
+
): void {
|
|
111
|
+
if (!scope) return
|
|
112
|
+
|
|
113
|
+
// Extract parent scope ID for renderChild context.
|
|
114
|
+
// When branch templates call renderChild(), it needs the parent scope ID
|
|
115
|
+
// so child mounts can stamp `bf-h` / `bf-m` for slot-resolver lookups.
|
|
116
|
+
const parentScopeId = scope.getAttribute(BF_SCOPE)
|
|
117
|
+
|
|
118
|
+
// Check if either branch uses fragment conditional (comment markers).
|
|
119
|
+
// Both branches need to be checked because SSR may render either branch.
|
|
120
|
+
// try/catch absorbs TypeError from nullable access during the probe
|
|
121
|
+
// (e.g. `selectedMail().subject` when the branch is for the non-null case).
|
|
122
|
+
let isFragmentCond = false
|
|
123
|
+
try {
|
|
124
|
+
const sampleTrue = evalBranchTemplate(whenTrue)
|
|
125
|
+
isFragmentCond = sampleTrue.html.includes(`<!--bf-cond-start:${id}-->`)
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// Template may throw TypeError for nullable access (e.g., selectedMail().subject)
|
|
128
|
+
if (!(err instanceof TypeError)) throw err
|
|
129
|
+
}
|
|
130
|
+
if (!isFragmentCond) {
|
|
131
|
+
try {
|
|
132
|
+
const sampleFalse = evalBranchTemplate(whenFalse)
|
|
133
|
+
isFragmentCond = sampleFalse.html.includes(`<!--bf-cond-start:${id}-->`)
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (!(err instanceof TypeError)) throw err
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let prevCond: boolean | undefined
|
|
140
|
+
let branchCleanup: (() => void) | null = null
|
|
141
|
+
|
|
142
|
+
createEffect(() => {
|
|
143
|
+
let currCond: boolean
|
|
144
|
+
try {
|
|
145
|
+
currCond = Boolean(conditionFn())
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Condition evaluation may throw TypeError if parent branch is inactive
|
|
148
|
+
// (e.g., selectedMail().read when selectedMail() is null).
|
|
149
|
+
// Only swallow TypeErrors; rethrow unexpected errors to avoid hiding bugs.
|
|
150
|
+
if (err instanceof TypeError) {
|
|
151
|
+
currCond = false
|
|
152
|
+
} else {
|
|
153
|
+
throw err
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const isFirstRun = prevCond === undefined
|
|
157
|
+
const prevVal = prevCond
|
|
158
|
+
prevCond = currCond
|
|
159
|
+
|
|
160
|
+
// Select the appropriate branch
|
|
161
|
+
const branch = currCond ? whenTrue : whenFalse
|
|
162
|
+
|
|
163
|
+
if (isFirstRun) {
|
|
164
|
+
// Hydration mode: check if existing DOM matches expected branch.
|
|
165
|
+
// If not, swap first (e.g., SSR rendered whenFalse but now we need whenTrue).
|
|
166
|
+
setParentScopeId(parentScopeId)
|
|
167
|
+
let result: BranchTemplateResult
|
|
168
|
+
try { result = evalBranchTemplate(branch) } finally { setParentScopeId(null) }
|
|
169
|
+
const existingEl = find(scope, `[${BF_COND}="${id}"]`)
|
|
170
|
+
if (existingEl) {
|
|
171
|
+
// Compare full opening tag signatures to detect branch mismatch.
|
|
172
|
+
// Tag-name-only comparison fails when both branches use the same tag (e.g., <div>).
|
|
173
|
+
const expectedSig = getTemplateRootSignature(result.html)
|
|
174
|
+
const existingSig = existingEl.outerHTML.match(/^<[^>]+>/)?.[0] ?? null
|
|
175
|
+
|
|
176
|
+
if (isFragmentCond && (!expectedSig || existingSig !== expectedSig)) {
|
|
177
|
+
// Fragment conditional template but element conditional in DOM:
|
|
178
|
+
// CSR composite loops inline-evaluate conditionals into bf-c elements,
|
|
179
|
+
// but insert() manages them as fragment conditionals (comment markers).
|
|
180
|
+
// Replace the bf-c element with the fragment template content.
|
|
181
|
+
// Skip the swap when the SSR signature already matches the active
|
|
182
|
+
// branch — the SSR DOM is correct, and replacing it would re-render
|
|
183
|
+
// via the registered child template, which doesn't reproduce the
|
|
184
|
+
// bf-h / bf-m markers set by the parent's JSX scope chain.
|
|
185
|
+
updateFragmentConditional(scope, id, result)
|
|
186
|
+
} else if (!isFragmentCond && expectedSig && existingSig && expectedSig !== existingSig) {
|
|
187
|
+
// DOM doesn't match expected branch - need to swap
|
|
188
|
+
updateElementConditional(scope, id, result)
|
|
189
|
+
} else if (result.slots.length > 0) {
|
|
190
|
+
// Branch template captured live nodes via __bfSlot (#1213). The
|
|
191
|
+
// SSR DOM rendered Hono-stringified HTML, but the client now needs
|
|
192
|
+
// the live signal-bound nodes installed. Force a swap so the
|
|
193
|
+
// existing element is replaced with the slot-spliced template.
|
|
194
|
+
updateElementConditional(scope, id, result)
|
|
195
|
+
}
|
|
196
|
+
} else if (isFragmentCond) {
|
|
197
|
+
// For @client fragment conditionals, SSR renders only comment markers.
|
|
198
|
+
// We need to insert the actual content on first run.
|
|
199
|
+
updateFragmentConditional(scope, id, result)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Bind events to the (possibly updated) SSR element. Pass isFirstRun
|
|
203
|
+
// so branch composite loops can skip the wipe-then-rebuild path that
|
|
204
|
+
// is only needed for subsequent branch swaps (the SSR-rendered DOM
|
|
205
|
+
// already matches the data and mapArray reconciles by key from it).
|
|
206
|
+
const cleanup = branch.bindEvents(scope, { isFirstRun: true })
|
|
207
|
+
branchCleanup = typeof cleanup === 'function' ? cleanup : null
|
|
208
|
+
|
|
209
|
+
// Auto-focus on first run too (for components created via createComponent with editing=true)
|
|
210
|
+
autoFocusConditionalElement(scope, id)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Skip if condition hasn't changed.
|
|
215
|
+
// Reactive updates within a branch are handled by the effect system,
|
|
216
|
+
// not by DOM replacement. Only replace DOM when the branch switches.
|
|
217
|
+
if (currCond === prevVal) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Dispose previous branch's scoped effects before swapping DOM
|
|
222
|
+
if (branchCleanup) {
|
|
223
|
+
branchCleanup()
|
|
224
|
+
branchCleanup = null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Branch changed: swap DOM and bind events.
|
|
228
|
+
setParentScopeId(parentScopeId)
|
|
229
|
+
let result: BranchTemplateResult
|
|
230
|
+
try { result = evalBranchTemplate(branch) } finally { setParentScopeId(null) }
|
|
231
|
+
if (isFragmentCond) {
|
|
232
|
+
updateFragmentConditional(scope, id, result)
|
|
233
|
+
} else {
|
|
234
|
+
updateElementConditional(scope, id, result)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Bind events to the newly inserted element (branch swap: not first run).
|
|
238
|
+
const cleanup = branch.bindEvents(scope, { isFirstRun: false })
|
|
239
|
+
branchCleanup = typeof cleanup === 'function' ? cleanup : null
|
|
240
|
+
|
|
241
|
+
// Auto-focus elements with autofocus attribute (for dynamically created elements)
|
|
242
|
+
autoFocusConditionalElement(scope, id)
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Auto-focus elements with autofocus attribute within a conditional slot.
|
|
249
|
+
* Used by insert() to focus inputs when they become visible.
|
|
250
|
+
* Uses requestAnimationFrame to ensure element is in DOM before focusing.
|
|
251
|
+
*/
|
|
252
|
+
function autoFocusConditionalElement(scope: Element, id: string): void {
|
|
253
|
+
// Use requestAnimationFrame to defer focus until after DOM updates.
|
|
254
|
+
// This is necessary because createComponent() may call insert() before
|
|
255
|
+
// the element is added to the document by reconcileList().
|
|
256
|
+
requestAnimationFrame(() => {
|
|
257
|
+
const condEl = scope.querySelector(`[${BF_COND}="${id}"]`)
|
|
258
|
+
if (condEl) {
|
|
259
|
+
const autofocusEl = condEl.matches('[autofocus]')
|
|
260
|
+
? condEl
|
|
261
|
+
: condEl.querySelector('[autofocus]')
|
|
262
|
+
if (autofocusEl && typeof (autofocusEl as HTMLElement).focus === 'function') {
|
|
263
|
+
;(autofocusEl as HTMLElement).focus()
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Extract the root element's opening tag from an HTML template string.
|
|
271
|
+
* Returns the full opening tag (e.g., `<div class="foo" bf-c="s0">`) for comparison.
|
|
272
|
+
* This allows distinguishing between conditional branches that share the same tag name
|
|
273
|
+
* but differ in attributes (e.g., two different `<div>` branches).
|
|
274
|
+
*/
|
|
275
|
+
function getTemplateRootSignature(template: string): string | null {
|
|
276
|
+
const match = template.match(/^<[^>]+>/)
|
|
277
|
+
return match ? match[0] : null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Replace `<!--bf-slot:N-->` placeholder comments inside a parsed fragment
|
|
282
|
+
* with the live `Node` from `slots[N]` (#1213). Walks every comment in
|
|
283
|
+
* the fragment and substitutes by identity (no clone) so event bindings
|
|
284
|
+
* and signal effects on the slot node remain intact.
|
|
285
|
+
*
|
|
286
|
+
* Returns the same fragment for chaining.
|
|
287
|
+
*/
|
|
288
|
+
function spliceSlots(fragment: DocumentFragment, slots: Node[]): DocumentFragment {
|
|
289
|
+
if (slots.length === 0) return fragment
|
|
290
|
+
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_COMMENT)
|
|
291
|
+
const replacements: Array<[Comment, Node]> = []
|
|
292
|
+
while (walker.nextNode()) {
|
|
293
|
+
const c = walker.currentNode as Comment
|
|
294
|
+
const m = c.nodeValue?.match(/^bf-slot:(\d+)$/)
|
|
295
|
+
if (m) {
|
|
296
|
+
const idx = Number(m[1])
|
|
297
|
+
const node = slots[idx]
|
|
298
|
+
if (node) replacements.push([c, node])
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const [marker, node] of replacements) {
|
|
302
|
+
marker.parentNode?.replaceChild(node, marker)
|
|
303
|
+
}
|
|
304
|
+
return fragment
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Update fragment conditional (content between comment markers)
|
|
309
|
+
*/
|
|
310
|
+
function updateFragmentConditional(scope: Element, id: string, result: BranchTemplateResult): void {
|
|
311
|
+
const { html, slots } = result
|
|
312
|
+
// Find start comment marker
|
|
313
|
+
const startMarker = `bf-cond-start:${id}`
|
|
314
|
+
let startComment: Comment | null = null
|
|
315
|
+
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_COMMENT)
|
|
316
|
+
while (walker.nextNode()) {
|
|
317
|
+
if (walker.currentNode.nodeValue === startMarker) {
|
|
318
|
+
startComment = walker.currentNode as Comment
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const condEl = scope.querySelector(`[${BF_COND}="${id}"]`)
|
|
324
|
+
|
|
325
|
+
const endMarker = `bf-cond-end:${id}`
|
|
326
|
+
|
|
327
|
+
if (startComment) {
|
|
328
|
+
// Remove nodes between start and end markers
|
|
329
|
+
const nodesToRemove: Node[] = []
|
|
330
|
+
let node = startComment.nextSibling
|
|
331
|
+
while (node && !(node.nodeType === 8 && node.nodeValue === endMarker)) {
|
|
332
|
+
nodesToRemove.push(node)
|
|
333
|
+
node = node.nextSibling
|
|
334
|
+
}
|
|
335
|
+
const endComment = node
|
|
336
|
+
nodesToRemove.forEach(n => n.parentNode?.removeChild(n))
|
|
337
|
+
|
|
338
|
+
// Insert new content. Pass the actual insertion parent so SVG-context
|
|
339
|
+
// parsing kicks in for fragments mounted inside an `<svg>` (#135).
|
|
340
|
+
const insertParent = (startComment.parentNode instanceof Element)
|
|
341
|
+
? startComment.parentNode
|
|
342
|
+
: null
|
|
343
|
+
const fragment = spliceSlots(parseHTML(html, insertParent), slots)
|
|
344
|
+
// Move parsed nodes by identity rather than cloning. A slot Node
|
|
345
|
+
// nested inside an element wrapper (e.g. `<div>${__bfSlot(...)}</div>`)
|
|
346
|
+
// would otherwise be cloned along with its parent, dropping event
|
|
347
|
+
// listeners and reactive effects (#1213). The parsed fragment is
|
|
348
|
+
// freshly built per call, so consuming it by reference is safe.
|
|
349
|
+
let child = fragment.firstChild
|
|
350
|
+
while (child) {
|
|
351
|
+
const next: ChildNode | null = child.nextSibling
|
|
352
|
+
if (!(child.nodeType === 8 && child.nodeValue?.startsWith('bf-cond-'))) {
|
|
353
|
+
startComment!.parentNode?.insertBefore(child, endComment)
|
|
354
|
+
}
|
|
355
|
+
child = next
|
|
356
|
+
}
|
|
357
|
+
} else if (condEl) {
|
|
358
|
+
// Single element: replace with new content. The replacement's
|
|
359
|
+
// namespace is determined by the parent of the element being
|
|
360
|
+
// replaced.
|
|
361
|
+
const insertParent = (condEl.parentNode instanceof Element)
|
|
362
|
+
? condEl.parentNode
|
|
363
|
+
: null
|
|
364
|
+
const parsed = spliceSlots(parseHTML(html, insertParent), slots)
|
|
365
|
+
const firstChild = parsed.firstChild
|
|
366
|
+
|
|
367
|
+
if (firstChild?.nodeType === 8 && firstChild?.nodeValue === `bf-cond-start:${id}`) {
|
|
368
|
+
// Switching from element to fragment. Move parsed nodes by
|
|
369
|
+
// identity (see fragment branch above) so nested slot nodes keep
|
|
370
|
+
// their event/effect bindings (#1213).
|
|
371
|
+
const parent = condEl.parentNode
|
|
372
|
+
let n: ChildNode | null = parsed.firstChild
|
|
373
|
+
while (n) {
|
|
374
|
+
const next: ChildNode | null = n.nextSibling
|
|
375
|
+
parent?.insertBefore(n, condEl)
|
|
376
|
+
n = next
|
|
377
|
+
}
|
|
378
|
+
condEl.remove()
|
|
379
|
+
} else if (firstChild) {
|
|
380
|
+
// Replace the existing conditional element with the parsed root
|
|
381
|
+
// by reference; cloning would re-clone any slot nodes nested
|
|
382
|
+
// inside `firstChild` and break identity preservation (#1213).
|
|
383
|
+
condEl.replaceWith(firstChild)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Update element conditional (single element with bf-c)
|
|
390
|
+
*/
|
|
391
|
+
function updateElementConditional(scope: Element, id: string, result: BranchTemplateResult): void {
|
|
392
|
+
const condEl = scope.querySelector(`[${BF_COND}="${id}"]`)
|
|
393
|
+
if (!condEl) return
|
|
394
|
+
|
|
395
|
+
const { html, slots } = result
|
|
396
|
+
const insertParent = (condEl.parentNode instanceof Element)
|
|
397
|
+
? condEl.parentNode
|
|
398
|
+
: null
|
|
399
|
+
const fragment = spliceSlots(parseHTML(html, insertParent), slots)
|
|
400
|
+
const newEl = fragment.firstChild
|
|
401
|
+
if (newEl) {
|
|
402
|
+
// Move `newEl` into the DOM by identity. The fragment is discarded
|
|
403
|
+
// after this call, so cloning would only serve to break identity
|
|
404
|
+
// for any slot nodes nested inside `newEl` (#1213).
|
|
405
|
+
condEl.replaceWith(newEl)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS - List Reconciliation
|
|
3
|
+
*
|
|
4
|
+
* Key-based DOM reconciliation for efficient list updates.
|
|
5
|
+
* Delegates to reconcileElements for element-based rendering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { reconcileElements } from './reconcile-elements'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render function type for list items.
|
|
12
|
+
* Returns an HTMLElement for each item.
|
|
13
|
+
*/
|
|
14
|
+
export type RenderItemFn<T> = (item: T, index: number) => HTMLElement
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reconcile a list container with new items using key-based matching.
|
|
18
|
+
*
|
|
19
|
+
* @param container - The parent element containing list items
|
|
20
|
+
* @param items - Array of items to render
|
|
21
|
+
* @param getKey - Function to extract a unique key from each item (or null to use index)
|
|
22
|
+
* @param renderItem - Function to render an item as HTMLElement
|
|
23
|
+
*/
|
|
24
|
+
export function reconcileList<T>(
|
|
25
|
+
container: HTMLElement | null,
|
|
26
|
+
items: T[],
|
|
27
|
+
getKey: ((item: T, index: number) => string) | null,
|
|
28
|
+
renderItem: RenderItemFn<T>
|
|
29
|
+
): void {
|
|
30
|
+
if (!container || !items) return
|
|
31
|
+
|
|
32
|
+
if (items.length === 0) {
|
|
33
|
+
container.innerHTML = ''
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Pre-create first element to avoid duplicate creation inside reconcileElements
|
|
38
|
+
const firstElement = renderItem(items[0], 0)
|
|
39
|
+
|
|
40
|
+
reconcileElements(
|
|
41
|
+
container,
|
|
42
|
+
items,
|
|
43
|
+
getKey,
|
|
44
|
+
renderItem,
|
|
45
|
+
firstElement
|
|
46
|
+
)
|
|
47
|
+
}
|