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/View.res ADDED
@@ -0,0 +1,599 @@
1
+ module DOM = RuntimeDom
2
+ module Reactivity = RuntimeOwner
3
+
4
+ /* ============================================================================
5
+ * Type Definitions
6
+ * ============================================================================ */
7
+
8
+ /* Attribute value source */
9
+ type attrValue =
10
+ | Static(string)
11
+ | SignalValue(Signal.t<string>)
12
+ | Compute(unit => string)
13
+
14
+ /* Virtual node types */
15
+ type rec node =
16
+ | Element({
17
+ tag: string,
18
+ attrs: array<(string, attrValue)>,
19
+ events: array<(string, Dom.event => unit)>,
20
+ children: array<node>,
21
+ })
22
+ | Text(string)
23
+ | SignalText(Signal.t<string>)
24
+ | Fragment(array<node>)
25
+ | SignalFragment(Signal.t<array<node>>)
26
+ | Keyed({key: string, identity: Obj.t, child: node})
27
+ | LazyComponent(unit => node)
28
+ | KeyedList({signal: Signal.t<array<Obj.t>>, keyFn: Obj.t => string, renderItem: Obj.t => node})
29
+
30
+ /* ============================================================================
31
+ * Attribute Helpers
32
+ * ============================================================================ */
33
+
34
+ module Attributes = {
35
+ let static = (key: string, value: string): (string, attrValue) => (key, Static(value))
36
+
37
+ let signal = (key: string, signal: Signal.t<string>): (string, attrValue) => (
38
+ key,
39
+ SignalValue(signal),
40
+ )
41
+
42
+ let computed = (key: string, compute: unit => string): (string, attrValue) => (
43
+ key,
44
+ Compute(compute),
45
+ )
46
+ }
47
+
48
+ /* Public API for attributes */
49
+ let attr = Attributes.static
50
+ let signalAttr = Attributes.signal
51
+ let computedAttr = Attributes.computed
52
+
53
+ module Attr = {
54
+ let string = attr
55
+ let signal = signalAttr
56
+ let compute = computedAttr
57
+ }
58
+
59
+ /* ============================================================================
60
+ * Rendering
61
+ * ============================================================================ */
62
+
63
+ module Render = {
64
+ open Reactivity
65
+
66
+ /* Type for tracking keyed list items */
67
+ type keyedItem<'a> = {
68
+ key: string,
69
+ item: 'a,
70
+ element: Dom.element,
71
+ }
72
+
73
+ type keyedChild = {
74
+ key: string,
75
+ identity: Obj.t,
76
+ child: node,
77
+ }
78
+
79
+ /* Dispose an element and its reactive state */
80
+ let rec disposeElement = (el: Dom.element): unit => {
81
+ /* Dispose the owner if it exists */
82
+ switch getOwner(el) {
83
+ | Some(owner) => disposeOwner(owner)
84
+ | None => ()
85
+ }
86
+
87
+ /* Recursively dispose children */
88
+ let childNodes: array<Dom.element> = %raw(`Array.from(el.childNodes || [])`)
89
+ childNodes->Array.forEach(disposeElement)
90
+ }
91
+
92
+ let shallowEqualIdentity = (a: Obj.t, b: Obj.t): bool =>
93
+ if a === b {
94
+ true
95
+ } else if typeof(a) != #object || typeof(b) != #object {
96
+ false
97
+ } else {
98
+ let dictA: Dict.t<Obj.t> = Obj.magic(a)
99
+ let dictB: Dict.t<Obj.t> = Obj.magic(b)
100
+ let keysA = dictA->Dict.keysToArray
101
+ let keysB = dictB->Dict.keysToArray
102
+
103
+ if keysA->Array.length !== keysB->Array.length {
104
+ false
105
+ } else {
106
+ keysA->Array.every(key =>
107
+ switch (dictA->Dict.get(key), dictB->Dict.get(key)) {
108
+ | (Some(valueA), Some(valueB)) => valueA === valueB
109
+ | _ => false
110
+ }
111
+ )
112
+ }
113
+ }
114
+
115
+ let clearKeyedItems = (keyedItems: Dict.t<keyedItem<Obj.t>>): unit => {
116
+ keyedItems->Dict.keysToArray->Array.forEach(key => keyedItems->Dict.delete(key)->ignore)
117
+ }
118
+
119
+ let getKeyedChildren = (children: array<node>): option<array<keyedChild>> => {
120
+ let keyedChildren = []
121
+ let allKeyed = ref(true)
122
+
123
+ children->Array.forEach(child => {
124
+ switch child {
125
+ | Keyed({key, identity, child}) =>
126
+ keyedChildren->Array.push({key, identity, child})->ignore
127
+ | _ => allKeyed := false
128
+ }
129
+ })
130
+
131
+ if allKeyed.contents {
132
+ Some(keyedChildren)
133
+ } else {
134
+ None
135
+ }
136
+ }
137
+
138
+ let rec reconcileKeyedChildren = (
139
+ ~keyedChildren: array<keyedChild>,
140
+ ~keyedItems: Dict.t<keyedItem<Obj.t>>,
141
+ ~parent: Dom.element,
142
+ ): unit => {
143
+ let newKeyMap: Dict.t<keyedChild> = Dict.make()
144
+ keyedChildren->Array.forEach(child => newKeyMap->Dict.set(child.key, child))
145
+
146
+ let keysToRemove = []
147
+ keyedItems
148
+ ->Dict.keysToArray
149
+ ->Array.forEach(key => {
150
+ switch newKeyMap->Dict.get(key) {
151
+ | None => keysToRemove->Array.push(key)->ignore
152
+ | Some(_) => ()
153
+ }
154
+ })
155
+
156
+ keysToRemove->Array.forEach(key => {
157
+ switch keyedItems->Dict.get(key) {
158
+ | Some(keyedItem) => {
159
+ disposeElement(keyedItem.element)
160
+ keyedItem.element->DOM.remove
161
+ keyedItems->Dict.delete(key)->ignore
162
+ }
163
+ | None => ()
164
+ }
165
+ })
166
+
167
+ let newOrder: array<keyedItem<Obj.t>> = []
168
+ let elementsToReplace: Dict.t<Dom.element> = Dict.make()
169
+
170
+ keyedChildren->Array.forEach(keyedChild => {
171
+ switch keyedItems->Dict.get(keyedChild.key) {
172
+ | Some(existing) =>
173
+ if shallowEqualIdentity(existing.item, keyedChild.identity) {
174
+ newOrder->Array.push(existing)->ignore
175
+ } else {
176
+ let element = render(keyedChild.child)
177
+ let keyedItem: keyedItem<Obj.t> = {
178
+ key: keyedChild.key,
179
+ item: keyedChild.identity,
180
+ element,
181
+ }
182
+ elementsToReplace->Dict.set(keyedChild.key, existing.element)
183
+ newOrder->Array.push(keyedItem)->ignore
184
+ keyedItems->Dict.set(keyedChild.key, keyedItem)
185
+ }
186
+ | None => {
187
+ let element = render(keyedChild.child)
188
+ let keyedItem: keyedItem<Obj.t> = {
189
+ key: keyedChild.key,
190
+ item: keyedChild.identity,
191
+ element,
192
+ }
193
+ newOrder->Array.push(keyedItem)->ignore
194
+ keyedItems->Dict.set(keyedChild.key, keyedItem)
195
+ }
196
+ }
197
+ })
198
+
199
+ let marker = ref(
200
+ switch DOM.getFirstChild(parent)->Nullable.toOption {
201
+ | Some(node) => Some(node)
202
+ | None => None
203
+ },
204
+ )
205
+
206
+ newOrder->Array.forEach(keyedItem => {
207
+ let currentElement = marker.contents
208
+
209
+ switch currentElement {
210
+ | Some(elem) if elem === keyedItem.element =>
211
+ marker := DOM.getNextSibling(elem)->Nullable.toOption
212
+ | Some(elem) => {
213
+ switch elementsToReplace->Dict.get(keyedItem.key) {
214
+ | Some(previousElement) if elem === previousElement => {
215
+ disposeElement(previousElement)
216
+ DOM.replaceChild(parent, keyedItem.element, previousElement)
217
+ marker := DOM.getNextSibling(keyedItem.element)->Nullable.toOption
218
+ }
219
+ | _ => {
220
+ DOM.insertBefore(parent, keyedItem.element, elem)
221
+ marker := DOM.getNextSibling(keyedItem.element)->Nullable.toOption
222
+ }
223
+ }
224
+ }
225
+ | None => {
226
+ switch elementsToReplace->Dict.get(keyedItem.key) {
227
+ | Some(previousElement) => {
228
+ disposeElement(previousElement)
229
+ previousElement->DOM.remove
230
+ parent->DOM.appendChild(keyedItem.element)
231
+ }
232
+ | None => parent->DOM.appendChild(keyedItem.element)
233
+ }
234
+ }
235
+ }
236
+ })
237
+ }
238
+
239
+ /* Render a virtual node to a DOM element */
240
+ and render = (node: node): Dom.element => {
241
+ switch node {
242
+ | Text(content) => DOM.createTextNode(content)
243
+
244
+ | SignalText(signal) => {
245
+ let textNode = DOM.createTextNode(Signal.peek(signal))
246
+ let owner = createOwner()
247
+ setOwner(textNode, owner)
248
+
249
+ runWithOwner(owner, () => {
250
+ let disposer = Effect.runWithDisposer(() => {
251
+ DOM.setTextContent(textNode, Signal.get(signal))
252
+ None
253
+ })
254
+ addDisposer(owner, disposer)
255
+ })
256
+
257
+ textNode
258
+ }
259
+
260
+ | Fragment(children) => {
261
+ let fragment = DOM.createDocumentFragment()
262
+ children->Array.forEach(child => {
263
+ let childEl = render(child)
264
+ fragment->DOM.appendChild(childEl)
265
+ })
266
+ fragment
267
+ }
268
+
269
+ | SignalFragment(signal) => {
270
+ let owner = createOwner()
271
+ let container = DOM.createElement("div")
272
+ DOM.setAttribute(container, "style", "display: contents")
273
+ setOwner(container, owner)
274
+ let keyedItems: Dict.t<keyedItem<Obj.t>> = Dict.make()
275
+
276
+ runWithOwner(owner, () => {
277
+ let disposer = Effect.runWithDisposer(() => {
278
+ let children = Signal.get(signal)
279
+
280
+ switch getKeyedChildren(children) {
281
+ | Some(keyedChildren) =>
282
+ reconcileKeyedChildren(~keyedChildren, ~keyedItems, ~parent=container)
283
+ | None => {
284
+ clearKeyedItems(keyedItems)
285
+
286
+ /* Dispose existing children */
287
+ let childNodes: array<Dom.element> = %raw(`Array.from(container.childNodes || [])`)
288
+ childNodes->Array.forEach(disposeElement)
289
+
290
+ /* Clear existing children */
291
+ DOM.setInnerHTML(container, "")
292
+
293
+ /* Render and append new children */
294
+ children->Array.forEach(
295
+ child => {
296
+ let childEl = render(child)
297
+ container->DOM.appendChild(childEl)
298
+ },
299
+ )
300
+ }
301
+ }
302
+
303
+ None
304
+ })
305
+
306
+ addDisposer(owner, disposer)
307
+ })
308
+
309
+ container
310
+ }
311
+
312
+ | Element({tag, attrs, events, children}) => {
313
+ let el = DOM.createElementForTag(tag)
314
+ let owner = createOwner()
315
+ setOwner(el, owner)
316
+
317
+ runWithOwner(owner, () => {
318
+ let shouldDeferAttrUntilAfterChildren = ((key, _value)) =>
319
+ tag == "select" && key == "value"
320
+
321
+ let applyAttr = ((key, value)) => {
322
+ switch value {
323
+ | Static(v) => DOM.setAttrOrProp(el, key, v)
324
+ | SignalValue(signal) => {
325
+ DOM.setAttrOrProp(el, key, Signal.peek(signal))
326
+ let disposer = Effect.runWithDisposer(
327
+ () => {
328
+ DOM.setAttrOrProp(el, key, Signal.get(signal))
329
+ None
330
+ },
331
+ )
332
+ addDisposer(owner, disposer)
333
+ }
334
+ | Compute(compute) => {
335
+ let disposer = Effect.runWithDisposer(
336
+ () => {
337
+ DOM.setAttrOrProp(el, key, compute())
338
+ None
339
+ },
340
+ )
341
+ addDisposer(owner, disposer)
342
+ }
343
+ }
344
+ }
345
+
346
+ /* Set attributes that do not depend on mounted children */
347
+ attrs->Array.forEach(attr => {
348
+ if !shouldDeferAttrUntilAfterChildren(attr) {
349
+ applyAttr(attr)
350
+ }
351
+ })
352
+
353
+ /* Attach event listeners */
354
+ events->Array.forEach(((eventName, handler)) => {
355
+ el->DOM.addEventListener(eventName, handler)
356
+ })
357
+
358
+ /* Append children */
359
+ children->Array.forEach(child => {
360
+ let childEl = render(child)
361
+ el->DOM.appendChild(childEl)
362
+ })
363
+
364
+ /* Some DOM properties need the child tree to exist before the browser can resolve them */
365
+ attrs->Array.forEach(attr => {
366
+ if shouldDeferAttrUntilAfterChildren(attr) {
367
+ applyAttr(attr)
368
+ }
369
+ })
370
+ })
371
+
372
+ el
373
+ }
374
+
375
+ | Keyed({child, key: _, identity: _}) => render(child)
376
+
377
+ | LazyComponent(fn) => {
378
+ let owner = createOwner()
379
+ let childNode = runWithOwner(owner, fn)
380
+ let el = render(childNode)
381
+ setOwner(el, owner)
382
+ el
383
+ }
384
+
385
+ | KeyedList({signal, keyFn, renderItem}) => {
386
+ let owner = createOwner()
387
+ let startAnchor = DOM.createComment(" keyed-list-start ")
388
+ let endAnchor = DOM.createComment(" keyed-list-end ")
389
+
390
+ setOwner(startAnchor, owner)
391
+
392
+ let keyedItems: Dict.t<keyedItem<Obj.t>> = Dict.make()
393
+
394
+ /* Reconciliation logic */
395
+ let reconcile = (): unit => {
396
+ let parentOpt = DOM.getParentNode(endAnchor)->Nullable.toOption
397
+
398
+ switch parentOpt {
399
+ | None => ()
400
+ | Some(parent) => {
401
+ let newItems = Signal.get(signal)
402
+
403
+ let newKeyMap: Dict.t<Obj.t> = Dict.make()
404
+ newItems->Array.forEach(item => {
405
+ newKeyMap->Dict.set(keyFn(item), item)
406
+ })
407
+
408
+ /* Phase 1: Remove */
409
+ let keysToRemove = []
410
+ keyedItems
411
+ ->Dict.keysToArray
412
+ ->Array.forEach(key => {
413
+ switch newKeyMap->Dict.get(key) {
414
+ | None => keysToRemove->Array.push(key)->ignore
415
+ | Some(_) => ()
416
+ }
417
+ })
418
+
419
+ keysToRemove->Array.forEach(key => {
420
+ switch keyedItems->Dict.get(key) {
421
+ | Some(keyedItem) => {
422
+ disposeElement(keyedItem.element)
423
+ keyedItem.element->DOM.remove
424
+ keyedItems->Dict.delete(key)->ignore
425
+ }
426
+ | None => ()
427
+ }
428
+ })
429
+
430
+ /* Phase 2: Build new order */
431
+ let newOrder: array<keyedItem<Obj.t>> = []
432
+ let elementsToReplace: Dict.t<bool> = Dict.make()
433
+
434
+ newItems->Array.forEach(item => {
435
+ let key = keyFn(item)
436
+
437
+ switch keyedItems->Dict.get(key) {
438
+ | Some(existing) =>
439
+ if existing.item !== item {
440
+ elementsToReplace->Dict.set(key, true)
441
+ let node = renderItem(item)
442
+ let element = render(node)
443
+ let keyedItem = {key, item, element}
444
+ newOrder->Array.push(keyedItem)->ignore
445
+ keyedItems->Dict.set(key, keyedItem)
446
+ } else {
447
+ newOrder->Array.push(existing)->ignore
448
+ }
449
+ | None => {
450
+ let node = renderItem(item)
451
+ let element = render(node)
452
+ let keyedItem = {key, item, element}
453
+ newOrder->Array.push(keyedItem)->ignore
454
+ keyedItems->Dict.set(key, keyedItem)
455
+ }
456
+ }
457
+ })
458
+
459
+ /* Phase 3: Reconcile DOM */
460
+ let marker = ref(DOM.getNextSibling(startAnchor))
461
+
462
+ newOrder->Array.forEach(keyedItem => {
463
+ let currentElement = marker.contents
464
+
465
+ switch currentElement->Nullable.toOption {
466
+ | Some(elem) if elem === endAnchor =>
467
+ DOM.insertBefore(parent, keyedItem.element, endAnchor)
468
+ | Some(elem) if elem === keyedItem.element => marker := DOM.getNextSibling(elem)
469
+ | Some(elem) => {
470
+ let needsReplacement =
471
+ elementsToReplace->Dict.get(keyedItem.key)->Option.getOr(false)
472
+
473
+ if needsReplacement {
474
+ disposeElement(elem)
475
+ DOM.replaceChild(parent, keyedItem.element, elem)
476
+ marker := DOM.getNextSibling(keyedItem.element)
477
+ } else {
478
+ DOM.insertBefore(parent, keyedItem.element, elem)
479
+ marker := DOM.getNextSibling(keyedItem.element)
480
+ }
481
+ }
482
+ | None => DOM.insertBefore(parent, keyedItem.element, endAnchor)
483
+ }
484
+ })
485
+ }
486
+ }
487
+ }
488
+
489
+ /* Initial render */
490
+ let fragment = DOM.createDocumentFragment()
491
+ fragment->DOM.appendChild(startAnchor)
492
+
493
+ let initialItems = Signal.peek(signal)
494
+ initialItems->Array.forEach(item => {
495
+ let key = keyFn(item)
496
+ let node = renderItem(item)
497
+ let element = render(node)
498
+ let keyedItem = {key, item, element}
499
+ keyedItems->Dict.set(key, keyedItem)
500
+ fragment->DOM.appendChild(element)
501
+ })
502
+
503
+ fragment->DOM.appendChild(endAnchor)
504
+
505
+ runWithOwner(owner, () => {
506
+ let disposer = Effect.runWithDisposer(() => {
507
+ reconcile()
508
+ None
509
+ })
510
+ addDisposer(owner, disposer)
511
+ })
512
+
513
+ fragment
514
+ }
515
+ }
516
+ }
517
+ }
518
+
519
+ /* ============================================================================
520
+ * Public API
521
+ * ============================================================================ */
522
+
523
+ /* Text nodes */
524
+ let text = (content: string): node => Text(content)
525
+
526
+ let signalText = (compute: unit => string): node => {
527
+ let signal = Computed.make(compute)
528
+ SignalText(signal)
529
+ }
530
+
531
+ let signalInt = (compute: unit => int): node => {
532
+ let signal = Computed.make(() => compute()->Int.toString)
533
+ SignalText(signal)
534
+ }
535
+
536
+ let signalFloat = (compute: unit => float): node => {
537
+ let signal = Computed.make(() => compute()->Float.toString)
538
+ SignalText(signal)
539
+ }
540
+
541
+ /* Static text nodes with type-specific helpers */
542
+ let int = (value: int): node => Text(Int.toString(value))
543
+
544
+ let float = (value: float): node => Text(Float.toString(value))
545
+
546
+ /* Fragments */
547
+ let fragment = (children: array<node>): node => Fragment(children)
548
+
549
+ let signalFragment = (signal: Signal.t<array<node>>): node => SignalFragment(signal)
550
+
551
+ /* Lists */
552
+ let each = (signal: Signal.t<array<'a>>, renderItem: 'a => node): node => {
553
+ let nodesSignal = Computed.make(() => {
554
+ Signal.get(signal)->Array.map(renderItem)
555
+ })
556
+ SignalFragment(nodesSignal)
557
+ }
558
+
559
+ let list = each
560
+
561
+ let eachWithKey = (
562
+ signal: Signal.t<array<'a>>,
563
+ keyFn: 'a => string,
564
+ renderItem: 'a => node,
565
+ ): node => {
566
+ KeyedList({
567
+ signal: Obj.magic(signal),
568
+ keyFn: Obj.magic(keyFn),
569
+ renderItem: Obj.magic(renderItem),
570
+ })
571
+ }
572
+
573
+ let keyedList = eachWithKey
574
+
575
+ /* Element constructor */
576
+ let element = (
577
+ tag: string,
578
+ ~attrs: array<(string, attrValue)>=[]->Array.map(x => x),
579
+ ~events: array<(string, Dom.event => unit)>=[]->Array.map(x => x),
580
+ ~children: array<node>=[]->Array.map(x => x),
581
+ (),
582
+ ): node => Element({tag, attrs, events, children})
583
+
584
+ /* Null representation */
585
+ let null = () => text("")
586
+ let empty = null
587
+
588
+ /* Mounting */
589
+ let mount = (node: node, container: Dom.element): unit => {
590
+ let el = Render.render(node)
591
+ container->DOM.appendChild(el)
592
+ }
593
+
594
+ let mountById = (node: node, containerId: string): unit => {
595
+ switch DOM.getElementById(containerId)->Nullable.toOption {
596
+ | Some(container) => mount(node, container)
597
+ | None => Console.error("Container element not found: " ++ containerId)
598
+ }
599
+ }