xote 6.1.2 → 6.2.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/src/Hydration.res CHANGED
@@ -1,5 +1,6 @@
1
- module DOM = Node.DOM
2
- module Reactivity = Node.Reactivity
1
+ module DOM = View.DOM
2
+ module Reactivity = View.Reactivity
3
+ module Markers = RuntimeHydrationMarkers
3
4
 
4
5
  /* ============================================================================
5
6
  * Hydration Options
@@ -15,7 +16,7 @@ type hydrateOptions = {
15
16
  * ============================================================================ */
16
17
 
17
18
  module DOMWalker = {
18
- /* Node types */
19
+ /* View types */
19
20
  let elementNode = 1
20
21
  let textNode = 3
21
22
  let commentNode = 8
@@ -66,7 +67,8 @@ module DOMWalker = {
66
67
  let extractKey = (node: Dom.element): option<string> => {
67
68
  if nodeType(node) == commentNode {
68
69
  switch nodeValue(node)->Nullable.toOption {
69
- | Some(value) if String.startsWith(value, "k:") => Some(String.slice(value, ~start=2))
70
+ | Some(value) if String.startsWith(value, Markers.keyedItemPrefixContent) =>
71
+ Some(String.slice(value, ~start=String.length(Markers.keyedItemPrefixContent)))
70
72
  | _ => None
71
73
  }
72
74
  } else {
@@ -155,12 +157,12 @@ let logHydrationWarning = (msg: string): unit => {
155
157
  * ============================================================================ */
156
158
 
157
159
  /* Hydrate a single node, attaching reactivity to existing DOM */
158
- let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
160
+ let rec hydrateNode = (node: View.node, domNode: Dom.element): unit => {
159
161
  switch node {
160
- | Node.Text(_content) => /* Static text - nothing to hydrate, DOM already has the content */
162
+ | View.Text(_content) => /* Static text - nothing to hydrate, DOM already has the content */
161
163
  ()
162
164
 
163
- | Node.SignalText(signal) => {
165
+ | View.SignalText(signal) => {
164
166
  /*
165
167
  * Server rendered: <!--$-->text<!--/$-->
166
168
  * We need to find the text node between markers and attach an effect
@@ -177,7 +179,7 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
177
179
  })
178
180
  }
179
181
 
180
- | Node.Fragment(children) => {
182
+ | View.Fragment(children) => {
181
183
  /* Fragment children are directly in the parent - hydrate each */
182
184
  let walker = DOMWalker.make(domNode)
183
185
  children->Array.forEach(child => {
@@ -185,36 +187,60 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
185
187
  })
186
188
  }
187
189
 
188
- | Node.SignalFragment(signal) => {
190
+ | View.SignalFragment(signal) => {
189
191
  /*
190
192
  * Server rendered: <!--#-->...children...<!--/#-->
191
193
  * We need to replace the content when the signal changes
192
194
  */
193
195
  let owner = Reactivity.createOwner()
194
196
  Reactivity.setOwner(domNode, owner)
197
+ let keyedItems: Dict.t<View.Render.keyedItem<Obj.t>> = Dict.make()
198
+ let initialized = ref(false)
195
199
 
196
200
  Reactivity.runWithOwner(owner, () => {
197
201
  let disposer = Effect.runWithDisposer(() => {
198
202
  let children = Signal.get(signal)
199
203
 
200
- /* Clear existing children */
201
- let childNodes: array<Dom.element> = %raw(`Array.from(domNode.childNodes || [])`)
202
- childNodes->Array.forEach(
203
- child => {
204
- Reactivity.disposeOwner(
205
- Reactivity.getOwner(child)->Option.getOr(Reactivity.createOwner()),
204
+ switch View.Render.getKeyedChildren(children) {
205
+ | Some(keyedChildren) if initialized.contents =>
206
+ View.Render.reconcileKeyedChildren(~keyedChildren, ~keyedItems, ~parent=domNode)
207
+ | keyedChildrenOpt => {
208
+ View.Render.clearKeyedItems(keyedItems)
209
+
210
+ /* Clear existing children */
211
+ let childNodes: array<Dom.element> = %raw(`Array.from(domNode.childNodes || [])`)
212
+ childNodes->Array.forEach(
213
+ child => {
214
+ Reactivity.disposeOwner(
215
+ Reactivity.getOwner(child)->Option.getOr(Reactivity.createOwner()),
216
+ )
217
+ },
206
218
  )
207
- },
208
- )
209
- let _ = (%raw(`domNode.innerHTML = ''`): unit)
210
-
211
- /* Render and append new children */
212
- children->Array.forEach(
213
- child => {
214
- let childEl = Node.Render.render(child)
215
- domNode->DOM.appendChild(childEl)
216
- },
217
- )
219
+ DOM.setInnerHTML(domNode, "")
220
+
221
+ switch keyedChildrenOpt {
222
+ | Some(keyedChildren) =>
223
+ keyedChildren->Array.forEach(keyedChild => {
224
+ let childEl = View.Render.render(keyedChild.child)
225
+ keyedItems->Dict.set(keyedChild.key, {
226
+ key: keyedChild.key,
227
+ item: keyedChild.identity,
228
+ element: childEl,
229
+ })
230
+ domNode->DOM.appendChild(childEl)
231
+ })
232
+ | None =>
233
+ children->Array.forEach(
234
+ child => {
235
+ let childEl = View.Render.render(child)
236
+ domNode->DOM.appendChild(childEl)
237
+ },
238
+ )
239
+ }
240
+
241
+ initialized := true
242
+ }
243
+ }
218
244
 
219
245
  None
220
246
  })
@@ -222,7 +248,9 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
222
248
  })
223
249
  }
224
250
 
225
- | Node.Element({attrs, events, children}) => {
251
+ | View.Keyed({child, key: _, identity: _}) => hydrateNode(child, domNode)
252
+
253
+ | View.Element({attrs, events, children}) => {
226
254
  let owner = Reactivity.createOwner()
227
255
  Reactivity.setOwner(domNode, owner)
228
256
 
@@ -230,8 +258,8 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
230
258
  /* Hydrate reactive attributes */
231
259
  attrs->Array.forEach(((key, value)) => {
232
260
  switch value {
233
- | Node.Static(_) => () /* Already rendered, nothing to do */
234
- | Node.SignalValue(signal) => {
261
+ | View.Static(_) => () /* Already rendered, nothing to do */
262
+ | View.SignalValue(signal) => {
235
263
  let disposer = Effect.runWithDisposer(
236
264
  () => {
237
265
  DOM.setAttrOrProp(domNode, key, Signal.get(signal))
@@ -240,7 +268,7 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
240
268
  )
241
269
  Reactivity.addDisposer(owner, disposer)
242
270
  }
243
- | Node.Compute(compute) => {
271
+ | View.Compute(compute) => {
244
272
  let disposer = Effect.runWithDisposer(
245
273
  () => {
246
274
  DOM.setAttrOrProp(domNode, key, compute())
@@ -265,7 +293,7 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
265
293
  })
266
294
  }
267
295
 
268
- | Node.LazyComponent(fn) => {
296
+ | View.LazyComponent(fn) => {
269
297
  /* Execute lazy component and hydrate its result */
270
298
  let owner = Reactivity.createOwner()
271
299
  let childNode = Reactivity.runWithOwner(owner, fn)
@@ -273,7 +301,7 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
273
301
  hydrateNode(childNode, domNode)
274
302
  }
275
303
 
276
- | Node.KeyedList({signal, keyFn, renderItem}) => {
304
+ | View.KeyedList({signal, keyFn, renderItem}) => {
277
305
  /*
278
306
  * Server rendered: <!--kl--><!--k:key1-->item1<!--/k--><!--k:key2-->item2<!--/k--><!--/kl-->
279
307
  * We need to set up the reconciliation effect
@@ -282,21 +310,21 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
282
310
  Reactivity.setOwner(domNode, owner)
283
311
 
284
312
  /* Build initial key -> element map from existing DOM */
285
- let keyedItems: Dict.t<Node.Render.keyedItem<Obj.t>> = Dict.make()
313
+ let keyedItems: Dict.t<View.Render.keyedItem<Obj.t>> = Dict.make()
286
314
  let walker = DOMWalker.make(domNode)
287
315
 
288
316
  /* Skip to list start marker */
289
- let _ = DOMWalker.skipUntilMarker(walker, "kl")
317
+ let _ = DOMWalker.skipUntilMarker(walker, Markers.keyedListStartContent)
290
318
 
291
319
  /* Parse existing keyed items */
292
320
  let rec parseKeyedItems = () => {
293
321
  switch DOMWalker.peek(walker) {
294
- | Some(node) if DOMWalker.isMarkerPrefix(node, "k:") => {
322
+ | Some(node) if DOMWalker.isMarkerPrefix(node, Markers.keyedItemPrefixContent) => {
295
323
  let key = DOMWalker.extractKey(node)->Option.getOr("")
296
324
  let _ = DOMWalker.next(walker) // consume start marker
297
325
 
298
326
  /* Collect item elements until end marker */
299
- let itemElements = DOMWalker.collectUntilMarker(walker, "/k")
327
+ let itemElements = DOMWalker.collectUntilMarker(walker, Markers.keyedItemEndContent)
300
328
 
301
329
  /* Get the first actual element (skip text nodes) */
302
330
  switch itemElements->Array.find(el => DOMWalker.nodeType(el) == DOMWalker.elementNode) {
@@ -311,7 +339,7 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
311
339
 
312
340
  parseKeyedItems()
313
341
  }
314
- | Some(node) if DOMWalker.isMarker(node, "/kl") => {
342
+ | Some(node) if DOMWalker.isMarker(node, Markers.keyedListEndContent) => {
315
343
  let _ = DOMWalker.next(walker) // consume end marker
316
344
  }
317
345
  | _ => ()
@@ -353,8 +381,8 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
353
381
  keysToRemove->Array.forEach(key => {
354
382
  switch keyedItems->Dict.get(key) {
355
383
  | Some(keyedItem) => {
356
- Node.Render.disposeElement(keyedItem.element)
357
- let _ = (%raw(`keyedItem.element.remove()`): unit)
384
+ View.Render.disposeElement(keyedItem.element)
385
+ keyedItem.element->DOM.remove
358
386
  keyedItems->Dict.delete(key)->ignore
359
387
  }
360
388
  | None => ()
@@ -362,7 +390,7 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
362
390
  })
363
391
 
364
392
  /* Build new order */
365
- let newOrder: array<Node.Render.keyedItem<Obj.t>> = []
393
+ let newOrder: array<View.Render.keyedItem<Obj.t>> = []
366
394
  let elementsToReplace: Dict.t<bool> = Dict.make()
367
395
 
368
396
  newItems->Array.forEach(item => {
@@ -373,8 +401,8 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
373
401
  if existing.item !== item {
374
402
  elementsToReplace->Dict.set(key, true)
375
403
  let node = renderItem(item)
376
- let element = Node.Render.render(node)
377
- let keyedItem: Node.Render.keyedItem<Obj.t> = {key, item, element}
404
+ let element = View.Render.render(node)
405
+ let keyedItem: View.Render.keyedItem<Obj.t> = {key, item, element}
378
406
  newOrder->Array.push(keyedItem)->ignore
379
407
  keyedItems->Dict.set(key, keyedItem)
380
408
  } else {
@@ -382,8 +410,8 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
382
410
  }
383
411
  | None => {
384
412
  let node = renderItem(item)
385
- let element = Node.Render.render(node)
386
- let keyedItem: Node.Render.keyedItem<Obj.t> = {key, item, element}
413
+ let element = View.Render.render(node)
414
+ let keyedItem: View.Render.keyedItem<Obj.t> = {key, item, element}
387
415
  newOrder->Array.push(keyedItem)->ignore
388
416
  keyedItems->Dict.set(key, keyedItem)
389
417
  }
@@ -405,7 +433,7 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
405
433
  elementsToReplace->Dict.get(keyedItem.key)->Option.getOr(false)
406
434
 
407
435
  if needsReplacement {
408
- Node.Render.disposeElement(elem)
436
+ View.Render.disposeElement(elem)
409
437
  DOM.replaceChild(domNode, keyedItem.element, elem)
410
438
  marker := DOM.getNextSibling(keyedItem.element)
411
439
  } else {
@@ -429,16 +457,16 @@ let rec hydrateNode = (node: Node.node, domNode: Dom.element): unit => {
429
457
  }
430
458
 
431
459
  /* Hydrate using a walker (for traversing children) */
432
- and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
460
+ and hydrateNodeWithWalker = (node: View.node, walker: DOMWalker.t): unit => {
433
461
  switch node {
434
- | Node.Text(_) => {
462
+ | View.Text(_) => {
435
463
  /* Skip text node in DOM */
436
464
  let _ = DOMWalker.next(walker)
437
465
  }
438
466
 
439
- | Node.SignalText(signal) => {
467
+ | View.SignalText(signal) => {
440
468
  /* Find the marker, then hydrate the text node */
441
- let _ = DOMWalker.skipUntilMarker(walker, "$")
469
+ let _ = DOMWalker.skipUntilMarker(walker, Markers.signalTextStartContent)
442
470
 
443
471
  /* Get the text node */
444
472
  switch DOMWalker.next(walker) {
@@ -455,24 +483,24 @@ and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
455
483
  })
456
484
 
457
485
  /* Skip end marker */
458
- let _ = DOMWalker.skipUntilMarker(walker, "/$")
486
+ let _ = DOMWalker.skipUntilMarker(walker, Markers.signalTextEndContent)
459
487
  }
460
488
  | None => logHydrationWarning("Missing text node for SignalText")
461
489
  }
462
490
  }
463
491
 
464
- | Node.Fragment(children) =>
492
+ | View.Fragment(children) =>
465
493
  /* Fragment children are inline - hydrate each */
466
494
  children->Array.forEach(child => {
467
495
  hydrateNodeWithWalker(child, walker)
468
496
  })
469
497
 
470
- | Node.SignalFragment(signal) => {
498
+ | View.SignalFragment(signal) => {
471
499
  /* Find the container (div with display:contents in SSR, markers in comments) */
472
- let _ = DOMWalker.skipUntilMarker(walker, "#")
500
+ let _ = DOMWalker.skipUntilMarker(walker, Markers.signalFragmentStartContent)
473
501
 
474
502
  /* Collect all nodes until end marker - these become the container's content */
475
- let contentNodes = DOMWalker.collectUntilMarker(walker, "/#")
503
+ let contentNodes = DOMWalker.collectUntilMarker(walker, Markers.signalFragmentEndContent)
476
504
 
477
505
  /* Create a container div to hold the signal fragment */
478
506
  let container = DOM.createElement("div")
@@ -502,22 +530,47 @@ and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
502
530
  /* Set up reactivity */
503
531
  let owner = Reactivity.createOwner()
504
532
  Reactivity.setOwner(container, owner)
533
+ let keyedItems: Dict.t<View.Render.keyedItem<Obj.t>> = Dict.make()
534
+ let initialized = ref(false)
505
535
 
506
536
  Reactivity.runWithOwner(owner, () => {
507
537
  let disposer = Effect.runWithDisposer(() => {
508
538
  let children = Signal.get(signal)
509
539
 
510
- /* Clear and re-render */
511
- let childNodes: array<Dom.element> = %raw(`Array.from(container.childNodes || [])`)
512
- childNodes->Array.forEach(Node.Render.disposeElement)
513
- let _ = (%raw(`container.innerHTML = ''`): unit)
540
+ switch View.Render.getKeyedChildren(children) {
541
+ | Some(keyedChildren) if initialized.contents =>
542
+ View.Render.reconcileKeyedChildren(~keyedChildren, ~keyedItems, ~parent=container)
543
+ | keyedChildrenOpt => {
544
+ View.Render.clearKeyedItems(keyedItems)
545
+
546
+ /* Clear and re-render */
547
+ let childNodes: array<Dom.element> = %raw(`Array.from(container.childNodes || [])`)
548
+ childNodes->Array.forEach(View.Render.disposeElement)
549
+ DOM.setInnerHTML(container, "")
550
+
551
+ switch keyedChildrenOpt {
552
+ | Some(keyedChildren) =>
553
+ keyedChildren->Array.forEach(keyedChild => {
554
+ let childEl = View.Render.render(keyedChild.child)
555
+ keyedItems->Dict.set(keyedChild.key, {
556
+ key: keyedChild.key,
557
+ item: keyedChild.identity,
558
+ element: childEl,
559
+ })
560
+ container->DOM.appendChild(childEl)
561
+ })
562
+ | None =>
563
+ children->Array.forEach(
564
+ child => {
565
+ let childEl = View.Render.render(child)
566
+ container->DOM.appendChild(childEl)
567
+ },
568
+ )
569
+ }
514
570
 
515
- children->Array.forEach(
516
- child => {
517
- let childEl = Node.Render.render(child)
518
- container->DOM.appendChild(childEl)
519
- },
520
- )
571
+ initialized := true
572
+ }
573
+ }
521
574
 
522
575
  None
523
576
  })
@@ -525,7 +578,9 @@ and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
525
578
  })
526
579
  }
527
580
 
528
- | Node.Element({attrs, events, children}) =>
581
+ | View.Keyed({child, key: _, identity: _}) => hydrateNodeWithWalker(child, walker)
582
+
583
+ | View.Element({attrs, events, children}) =>
529
584
  switch DOMWalker.next(walker) {
530
585
  | Some(domNode) => {
531
586
  let owner = Reactivity.createOwner()
@@ -535,8 +590,8 @@ and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
535
590
  /* Hydrate reactive attributes */
536
591
  attrs->Array.forEach(((key, value)) => {
537
592
  switch value {
538
- | Node.Static(_) => ()
539
- | Node.SignalValue(signal) => {
593
+ | View.Static(_) => ()
594
+ | View.SignalValue(signal) => {
540
595
  let disposer = Effect.runWithDisposer(
541
596
  () => {
542
597
  DOM.setAttrOrProp(domNode, key, Signal.get(signal))
@@ -545,7 +600,7 @@ and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
545
600
  )
546
601
  Reactivity.addDisposer(owner, disposer)
547
602
  }
548
- | Node.Compute(compute) => {
603
+ | View.Compute(compute) => {
549
604
  let disposer = Effect.runWithDisposer(
550
605
  () => {
551
606
  DOM.setAttrOrProp(domNode, key, compute())
@@ -572,30 +627,30 @@ and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
572
627
  | None => logHydrationWarning("Missing DOM element for Element node")
573
628
  }
574
629
 
575
- | Node.LazyComponent(fn) => {
630
+ | View.LazyComponent(fn) => {
576
631
  /* Skip the lazy component markers and hydrate the content */
577
- let _ = DOMWalker.skipUntilMarker(walker, "lc")
632
+ let _ = DOMWalker.skipUntilMarker(walker, Markers.lazyComponentStartContent)
578
633
 
579
634
  let childNode = fn()
580
635
  hydrateNodeWithWalker(childNode, walker)
581
636
 
582
- let _ = DOMWalker.skipUntilMarker(walker, "/lc")
637
+ let _ = DOMWalker.skipUntilMarker(walker, Markers.lazyComponentEndContent)
583
638
  }
584
639
 
585
- | Node.KeyedList({signal, keyFn, renderItem: _}) => {
640
+ | View.KeyedList({signal, keyFn, renderItem: _}) => {
586
641
  /* Find the keyed list in the DOM */
587
- let _ = DOMWalker.skipUntilMarker(walker, "kl")
642
+ let _ = DOMWalker.skipUntilMarker(walker, Markers.keyedListStartContent)
588
643
 
589
644
  /* Parse existing keyed items from DOM */
590
- let keyedItems: Dict.t<Node.Render.keyedItem<Obj.t>> = Dict.make()
645
+ let keyedItems: Dict.t<View.Render.keyedItem<Obj.t>> = Dict.make()
591
646
 
592
647
  let rec parseKeyedItems = () => {
593
648
  switch DOMWalker.peek(walker) {
594
- | Some(node) if DOMWalker.isMarkerPrefix(node, "k:") => {
649
+ | Some(node) if DOMWalker.isMarkerPrefix(node, Markers.keyedItemPrefixContent) => {
595
650
  let key = DOMWalker.extractKey(node)->Option.getOr("")
596
651
  let _ = DOMWalker.next(walker)
597
652
 
598
- let itemElements = DOMWalker.collectUntilMarker(walker, "/k")
653
+ let itemElements = DOMWalker.collectUntilMarker(walker, Markers.keyedItemEndContent)
599
654
 
600
655
  switch itemElements->Array.find(el => DOMWalker.nodeType(el) == DOMWalker.elementNode) {
601
656
  | Some(element) => {
@@ -609,7 +664,7 @@ and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
609
664
 
610
665
  parseKeyedItems()
611
666
  }
612
- | Some(node) if DOMWalker.isMarker(node, "/kl") => {
667
+ | Some(node) if DOMWalker.isMarker(node, Markers.keyedListEndContent) => {
613
668
  let _ = DOMWalker.next(walker)
614
669
  }
615
670
  | _ => ()
@@ -629,7 +684,7 @@ and hydrateNodeWithWalker = (node: Node.node, walker: DOMWalker.t): unit => {
629
684
 
630
685
  /* Hydrate a server-rendered component */
631
686
  let hydrate = (
632
- component: unit => Node.node,
687
+ component: unit => View.node,
633
688
  container: Dom.element,
634
689
  ~options: hydrateOptions={},
635
690
  ): unit => {
@@ -664,7 +719,7 @@ let hydrate = (
664
719
 
665
720
  /* Hydrate by element ID */
666
721
  let hydrateById = (
667
- component: unit => Node.node,
722
+ component: unit => View.node,
668
723
  containerId: string,
669
724
  ~options: hydrateOptions={},
670
725
  ): unit => {