@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.
Files changed (108) hide show
  1. package/dist/build.d.ts +56 -0
  2. package/dist/build.d.ts.map +1 -0
  3. package/dist/build.js +76 -0
  4. package/dist/context.d.ts +25 -0
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/csr-adapter.d.ts +26 -0
  7. package/dist/csr-adapter.d.ts.map +1 -0
  8. package/dist/forward-props.d.ts +17 -0
  9. package/dist/forward-props.d.ts.map +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +154 -0
  13. package/dist/reactive.d.ts +150 -0
  14. package/dist/reactive.d.ts.map +1 -0
  15. package/dist/reactive.js +215 -0
  16. package/dist/runtime/apply-rest-attrs.d.ts +16 -0
  17. package/dist/runtime/apply-rest-attrs.d.ts.map +1 -0
  18. package/dist/runtime/branch-slot.d.ts +22 -0
  19. package/dist/runtime/branch-slot.d.ts.map +1 -0
  20. package/dist/runtime/client-marker.d.ts +21 -0
  21. package/dist/runtime/client-marker.d.ts.map +1 -0
  22. package/dist/runtime/component.d.ts +99 -0
  23. package/dist/runtime/component.d.ts.map +1 -0
  24. package/dist/runtime/context.d.ts +40 -0
  25. package/dist/runtime/context.d.ts.map +1 -0
  26. package/dist/runtime/hydrate.d.ts +100 -0
  27. package/dist/runtime/hydrate.d.ts.map +1 -0
  28. package/dist/runtime/hydration-state.d.ts +13 -0
  29. package/dist/runtime/hydration-state.d.ts.map +1 -0
  30. package/dist/runtime/index.d.ts +27 -0
  31. package/dist/runtime/index.d.ts.map +1 -0
  32. package/dist/runtime/index.js +2093 -0
  33. package/dist/runtime/insert.d.ts +75 -0
  34. package/dist/runtime/insert.d.ts.map +1 -0
  35. package/dist/runtime/list.d.ts +21 -0
  36. package/dist/runtime/list.d.ts.map +1 -0
  37. package/dist/runtime/map-array.d.ts +32 -0
  38. package/dist/runtime/map-array.d.ts.map +1 -0
  39. package/dist/runtime/portal.d.ts +96 -0
  40. package/dist/runtime/portal.d.ts.map +1 -0
  41. package/dist/runtime/qsa-item.d.ts +52 -0
  42. package/dist/runtime/qsa-item.d.ts.map +1 -0
  43. package/dist/runtime/query.d.ts +86 -0
  44. package/dist/runtime/query.d.ts.map +1 -0
  45. package/dist/runtime/reconcile-elements.d.ts +44 -0
  46. package/dist/runtime/reconcile-elements.d.ts.map +1 -0
  47. package/dist/runtime/registry.d.ts +53 -0
  48. package/dist/runtime/registry.d.ts.map +1 -0
  49. package/dist/runtime/render.d.ts +35 -0
  50. package/dist/runtime/render.d.ts.map +1 -0
  51. package/dist/runtime/scope.d.ts +28 -0
  52. package/dist/runtime/scope.d.ts.map +1 -0
  53. package/dist/runtime/slot-resolver.d.ts +36 -0
  54. package/dist/runtime/slot-resolver.d.ts.map +1 -0
  55. package/dist/runtime/spread-attrs.d.ts +19 -0
  56. package/dist/runtime/spread-attrs.d.ts.map +1 -0
  57. package/dist/runtime/standalone.js +2278 -0
  58. package/dist/runtime/streaming.d.ts +36 -0
  59. package/dist/runtime/streaming.d.ts.map +1 -0
  60. package/dist/runtime/style.d.ts +17 -0
  61. package/dist/runtime/style.d.ts.map +1 -0
  62. package/dist/runtime/template.d.ts +39 -0
  63. package/dist/runtime/template.d.ts.map +1 -0
  64. package/dist/runtime/types.d.ts +26 -0
  65. package/dist/runtime/types.d.ts.map +1 -0
  66. package/dist/shims.d.ts +21 -0
  67. package/dist/shims.d.ts.map +1 -0
  68. package/dist/slot.d.ts +14 -0
  69. package/dist/slot.d.ts.map +1 -0
  70. package/dist/split-props.d.ts +26 -0
  71. package/dist/split-props.d.ts.map +1 -0
  72. package/dist/unwrap.d.ts +16 -0
  73. package/dist/unwrap.d.ts.map +1 -0
  74. package/package.json +71 -0
  75. package/src/build.ts +92 -0
  76. package/src/context.ts +33 -0
  77. package/src/csr-adapter.ts +134 -0
  78. package/src/forward-props.ts +43 -0
  79. package/src/index.ts +42 -0
  80. package/src/reactive.ts +411 -0
  81. package/src/runtime/apply-rest-attrs.ts +109 -0
  82. package/src/runtime/branch-slot.ts +32 -0
  83. package/src/runtime/client-marker.ts +46 -0
  84. package/src/runtime/component.ts +501 -0
  85. package/src/runtime/context.ts +111 -0
  86. package/src/runtime/hydrate.ts +311 -0
  87. package/src/runtime/hydration-state.ts +13 -0
  88. package/src/runtime/index.ts +96 -0
  89. package/src/runtime/insert.ts +407 -0
  90. package/src/runtime/list.ts +47 -0
  91. package/src/runtime/map-array.ts +381 -0
  92. package/src/runtime/portal.ts +174 -0
  93. package/src/runtime/qsa-item.ts +128 -0
  94. package/src/runtime/query.ts +632 -0
  95. package/src/runtime/reconcile-elements.ts +391 -0
  96. package/src/runtime/registry.ts +160 -0
  97. package/src/runtime/render.ts +105 -0
  98. package/src/runtime/scope.ts +46 -0
  99. package/src/runtime/slot-resolver.ts +66 -0
  100. package/src/runtime/spread-attrs.ts +88 -0
  101. package/src/runtime/streaming.ts +65 -0
  102. package/src/runtime/style.ts +27 -0
  103. package/src/runtime/template.ts +53 -0
  104. package/src/runtime/types.ts +27 -0
  105. package/src/shims.ts +54 -0
  106. package/src/slot.ts +23 -0
  107. package/src/split-props.ts +86 -0
  108. 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
+ }