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/README.md +90 -10
- package/dist/xote.cjs +10 -10
- package/dist/xote.mjs +1491 -1330
- package/dist/xote.umd.js +8 -8
- package/package.json +16 -1
- package/rescript.json +2 -0
- package/src/Html.res +13 -13
- package/src/Html.res.mjs +13 -13
- package/src/Hydration.res +134 -79
- package/src/Hydration.res.mjs +255 -186
- package/src/Node.res +2 -594
- package/src/Node.res.mjs +31 -535
- package/src/Prop.res +16 -0
- package/src/Prop.res.mjs +35 -0
- package/src/ReactiveProp.res +2 -14
- package/src/ReactiveProp.res.mjs +7 -20
- package/src/Route.res +4 -0
- package/src/Route.res.mjs +9 -0
- package/src/Router.res +25 -49
- package/src/Router.res.mjs +22 -34
- package/src/RuntimeAttr.res +21 -0
- package/src/RuntimeAttr.res.mjs +42 -0
- package/src/RuntimeDom.res +95 -0
- package/src/RuntimeDom.res.mjs +101 -0
- package/src/RuntimeHtml.res +27 -0
- package/src/RuntimeHtml.res.mjs +34 -0
- package/src/RuntimeHydrationMarkers.res +24 -0
- package/src/RuntimeHydrationMarkers.res.mjs +68 -0
- package/src/RuntimeJsxProp.res +46 -0
- package/src/RuntimeJsxProp.res.mjs +52 -0
- package/src/RuntimeOwner.res +43 -0
- package/src/RuntimeOwner.res.mjs +51 -0
- package/src/SSR.res +25 -93
- package/src/SSR.res.mjs +59 -126
- package/src/SSRState.res +3 -0
- package/src/SSRState.res.mjs +8 -2
- package/src/View.res +599 -0
- package/src/View.res.mjs +614 -0
- package/src/XoteJSX.res +64 -118
- package/src/XoteJSX.res.mjs +79 -118
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
|
+
}
|