xote 6.1.1 → 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/Node.res CHANGED
@@ -1,578 +1,2 @@
1
- /* ============================================================================
2
- * DOM Bindings
3
- * ============================================================================ */
4
-
5
- let svgNamespace = "http://www.w3.org/2000/svg"
6
-
7
- let svgTags = [
8
- "svg",
9
- "path",
10
- "circle",
11
- "ellipse",
12
- "line",
13
- "polygon",
14
- "polyline",
15
- "rect",
16
- "g",
17
- "defs",
18
- "clipPath",
19
- "mask",
20
- "pattern",
21
- "marker",
22
- "symbol",
23
- "use",
24
- "text",
25
- "tspan",
26
- "image",
27
- "foreignObject",
28
- "linearGradient",
29
- "radialGradient",
30
- "stop",
31
- "filter",
32
- "feBlend",
33
- "feColorMatrix",
34
- "feComposite",
35
- "feFlood",
36
- "feGaussianBlur",
37
- "feMerge",
38
- "feMergeNode",
39
- "feOffset",
40
- "animate",
41
- "animateTransform",
42
- "desc",
43
- "title",
44
- "metadata",
45
- ]
46
-
47
- let svgTagSet: Dict.t<bool> = {
48
- let d = Dict.make()
49
- svgTags->Array.forEach(tag => d->Dict.set(tag, true))
50
- d
51
- }
52
-
53
- let isSvgTag = (tag: string): bool => svgTagSet->Dict.get(tag)->Option.isSome
54
-
55
- module DOM = {
56
- /* Creation */
57
- @val @scope("document") external createElement: string => Dom.element = "createElement"
58
- @val @scope("document")
59
- external createElementNS: (string, string) => Dom.element = "createElementNS"
60
- @val @scope("document") external createTextNode: string => Dom.element = "createTextNode"
61
- @val @scope("document")
62
- external createDocumentFragment: unit => Dom.element = "createDocumentFragment"
63
- @val @scope("document") external createComment: string => Dom.element = "createComment"
64
- @val @scope("document")
65
- external getElementById: string => Nullable.t<Dom.element> = "getElementById"
66
-
67
- /* Accessors */
68
- @get external getNextSibling: Dom.element => Nullable.t<Dom.element> = "nextSibling"
69
- @get external getParentNode: Dom.element => Nullable.t<Dom.element> = "parentNode"
70
-
71
- /* Mutations */
72
- @send
73
- external addEventListener: (Dom.element, string, Dom.event => unit) => unit = "addEventListener"
74
- @send external appendChild: (Dom.element, Dom.element) => unit = "appendChild"
75
- @send external setAttribute: (Dom.element, string, string) => unit = "setAttribute"
76
- @send external removeAttribute: (Dom.element, string) => unit = "removeAttribute"
77
- @send external replaceChild: (Dom.element, Dom.element, Dom.element) => unit = "replaceChild"
78
- @send external insertBefore: (Dom.element, Dom.element, Dom.element) => unit = "insertBefore"
79
- @set external setTextContent: (Dom.element, string) => unit = "textContent"
80
- @set external setValue: (Dom.element, string) => unit = "value"
81
- @set external setChecked: (Dom.element, bool) => unit = "checked"
82
- @set external setDisabled: (Dom.element, bool) => unit = "disabled"
83
-
84
- /* Set attribute or property depending on attribute name */
85
- let setAttrOrProp = (el: Dom.element, key: string, value: string): unit => {
86
- switch key {
87
- | "value" => setValue(el, value)
88
- | "checked" => setChecked(el, value == "true")
89
- | "disabled" => setDisabled(el, value == "true")
90
- /* Boolean attributes that should be added/removed based on value */
91
- | "required"
92
- | "readonly"
93
- | "multiple"
94
- | "aria-hidden"
95
- | "aria-expanded"
96
- | "aria-selected"
97
- | "draggable"
98
- | "hidden"
99
- | "contenteditable"
100
- | "spellcheck"
101
- | "autofocus" =>
102
- if value == "true" {
103
- setAttribute(el, key, "")
104
- } else {
105
- removeAttribute(el, key)
106
- }
107
- | _ => setAttribute(el, key, value)
108
- }
109
- }
110
- }
111
-
112
- /* ============================================================================
113
- * Reactivity / Owner System
114
- * ============================================================================ */
115
-
116
- module Reactivity = {
117
- /* Owner tracks reactive state for a component scope */
118
- type owner = {
119
- disposers: array<Effect.disposer>,
120
- mutable computeds: array<Obj.t>,
121
- }
122
-
123
- /* Global owner stack */
124
- let currentOwner: ref<option<owner>> = ref(None)
125
-
126
- /* Create a new owner */
127
- let createOwner = (): owner => {
128
- disposers: [],
129
- computeds: [],
130
- }
131
-
132
- /* Run function with owner context */
133
- let runWithOwner = (owner: owner, fn: unit => 'a): 'a => {
134
- let previousOwner = currentOwner.contents
135
- currentOwner := Some(owner)
136
- let result = fn()
137
- currentOwner := previousOwner
138
- result
139
- }
140
-
141
- /* Add disposer to owner */
142
- let addDisposer = (owner: owner, disposer: Effect.disposer): unit => {
143
- owner.disposers->Array.push(disposer)->ignore
144
- }
145
-
146
- /* Dispose owner and all its reactive state */
147
- let disposeOwner = (owner: owner): unit => {
148
- /* Dispose all effects */
149
- owner.disposers->Array.forEach(disposer => disposer.dispose())
150
-
151
- /* Dispose all computeds */
152
- owner.computeds->Array.forEach(computed => {
153
- let c: Signal.t<Obj.t> = Obj.magic(computed)
154
- Computed.dispose(c)
155
- })
156
- }
157
-
158
- /* Owner storage on DOM elements */
159
- @warning("-27")
160
- let setOwner = (element: Dom.element, owner: owner): unit => {
161
- %raw(`element["__xote_owner__"] = owner`)
162
- }
163
-
164
- @warning("-27")
165
- let getOwner = (element: Dom.element): option<owner> => {
166
- let owner: Nullable.t<owner> = %raw(`element["__xote_owner__"]`)
167
- owner->Nullable.toOption
168
- }
169
- }
170
-
171
- /* ============================================================================
172
- * Type Definitions
173
- * ============================================================================ */
174
-
175
- /* Attribute value source */
176
- type attrValue =
177
- | Static(string)
178
- | SignalValue(Signal.t<string>)
179
- | Compute(unit => string)
180
-
181
- /* Virtual node types */
182
- type rec node =
183
- | Element({
184
- tag: string,
185
- attrs: array<(string, attrValue)>,
186
- events: array<(string, Dom.event => unit)>,
187
- children: array<node>,
188
- })
189
- | Text(string)
190
- | SignalText(Signal.t<string>)
191
- | Fragment(array<node>)
192
- | SignalFragment(Signal.t<array<node>>)
193
- | LazyComponent(unit => node)
194
- | KeyedList({signal: Signal.t<array<Obj.t>>, keyFn: Obj.t => string, renderItem: Obj.t => node})
195
-
196
- /* ============================================================================
197
- * Attribute Helpers
198
- * ============================================================================ */
199
-
200
- module Attributes = {
201
- let static = (key: string, value: string): (string, attrValue) => (key, Static(value))
202
-
203
- let signal = (key: string, signal: Signal.t<string>): (string, attrValue) => (
204
- key,
205
- SignalValue(signal),
206
- )
207
-
208
- let computed = (key: string, compute: unit => string): (string, attrValue) => (
209
- key,
210
- Compute(compute),
211
- )
212
- }
213
-
214
- /* Public API for attributes */
215
- let attr = Attributes.static
216
- let signalAttr = Attributes.signal
217
- let computedAttr = Attributes.computed
218
-
219
- /* ============================================================================
220
- * Rendering
221
- * ============================================================================ */
222
-
223
- module Render = {
224
- open Reactivity
225
-
226
- /* Type for tracking keyed list items */
227
- type keyedItem<'a> = {
228
- key: string,
229
- item: 'a,
230
- element: Dom.element,
231
- }
232
-
233
- /* Dispose an element and its reactive state */
234
- let rec disposeElement = (el: Dom.element): unit => {
235
- /* Dispose the owner if it exists */
236
- switch getOwner(el) {
237
- | Some(owner) => disposeOwner(owner)
238
- | None => ()
239
- }
240
-
241
- /* Recursively dispose children */
242
- let childNodes: array<Dom.element> = %raw(`Array.from(el.childNodes || [])`)
243
- childNodes->Array.forEach(disposeElement)
244
- }
245
-
246
- /* Render a virtual node to a DOM element */
247
- let rec render = (node: node): Dom.element => {
248
- switch node {
249
- | Text(content) => DOM.createTextNode(content)
250
-
251
- | SignalText(signal) => {
252
- let textNode = DOM.createTextNode(Signal.peek(signal))
253
- let owner = createOwner()
254
- setOwner(textNode, owner)
255
-
256
- runWithOwner(owner, () => {
257
- let disposer = Effect.runWithDisposer(() => {
258
- DOM.setTextContent(textNode, Signal.get(signal))
259
- None
260
- })
261
- addDisposer(owner, disposer)
262
- })
263
-
264
- textNode
265
- }
266
-
267
- | Fragment(children) => {
268
- let fragment = DOM.createDocumentFragment()
269
- children->Array.forEach(child => {
270
- let childEl = render(child)
271
- fragment->DOM.appendChild(childEl)
272
- })
273
- fragment
274
- }
275
-
276
- | SignalFragment(signal) => {
277
- let owner = createOwner()
278
- let container = DOM.createElement("div")
279
- DOM.setAttribute(container, "style", "display: contents")
280
- setOwner(container, owner)
281
-
282
- runWithOwner(owner, () => {
283
- let disposer = Effect.runWithDisposer(() => {
284
- let children = Signal.get(signal)
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
- let _ = (%raw(`container.innerHTML = ''`): unit)
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
- None
302
- })
303
-
304
- addDisposer(owner, disposer)
305
- })
306
-
307
- container
308
- }
309
-
310
- | Element({tag, attrs, events, children}) => {
311
- let el = if isSvgTag(tag) {
312
- DOM.createElementNS(svgNamespace, tag)
313
- } else {
314
- DOM.createElement(tag)
315
- }
316
- let owner = createOwner()
317
- setOwner(el, owner)
318
-
319
- runWithOwner(owner, () => {
320
- /* Set attributes */
321
- attrs->Array.forEach(((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
- /* Attach event listeners */
347
- events->Array.forEach(((eventName, handler)) => {
348
- el->DOM.addEventListener(eventName, handler)
349
- })
350
-
351
- /* Append children */
352
- children->Array.forEach(child => {
353
- let childEl = render(child)
354
- el->DOM.appendChild(childEl)
355
- })
356
- })
357
-
358
- el
359
- }
360
-
361
- | LazyComponent(fn) => {
362
- let owner = createOwner()
363
- let childNode = runWithOwner(owner, fn)
364
- let el = render(childNode)
365
- setOwner(el, owner)
366
- el
367
- }
368
-
369
- | KeyedList({signal, keyFn, renderItem}) => {
370
- let owner = createOwner()
371
- let startAnchor = DOM.createComment(" keyed-list-start ")
372
- let endAnchor = DOM.createComment(" keyed-list-end ")
373
-
374
- setOwner(startAnchor, owner)
375
-
376
- let keyedItems: Dict.t<keyedItem<Obj.t>> = Dict.make()
377
-
378
- /* Reconciliation logic */
379
- let reconcile = (): unit => {
380
- let parentOpt = DOM.getParentNode(endAnchor)->Nullable.toOption
381
-
382
- switch parentOpt {
383
- | None => ()
384
- | Some(parent) => {
385
- let newItems = Signal.get(signal)
386
-
387
- let newKeyMap: Dict.t<Obj.t> = Dict.make()
388
- newItems->Array.forEach(item => {
389
- newKeyMap->Dict.set(keyFn(item), item)
390
- })
391
-
392
- /* Phase 1: Remove */
393
- let keysToRemove = []
394
- keyedItems
395
- ->Dict.keysToArray
396
- ->Array.forEach(key => {
397
- switch newKeyMap->Dict.get(key) {
398
- | None => keysToRemove->Array.push(key)->ignore
399
- | Some(_) => ()
400
- }
401
- })
402
-
403
- keysToRemove->Array.forEach(key => {
404
- switch keyedItems->Dict.get(key) {
405
- | Some(keyedItem) => {
406
- disposeElement(keyedItem.element)
407
- let _ = (%raw(`keyedItem.element.remove()`): unit)
408
- keyedItems->Dict.delete(key)->ignore
409
- }
410
- | None => ()
411
- }
412
- })
413
-
414
- /* Phase 2: Build new order */
415
- let newOrder: array<keyedItem<Obj.t>> = []
416
- let elementsToReplace: Dict.t<bool> = Dict.make()
417
-
418
- newItems->Array.forEach(item => {
419
- let key = keyFn(item)
420
-
421
- switch keyedItems->Dict.get(key) {
422
- | Some(existing) =>
423
- if existing.item !== item {
424
- elementsToReplace->Dict.set(key, true)
425
- let node = renderItem(item)
426
- let element = render(node)
427
- let keyedItem = {key, item, element}
428
- newOrder->Array.push(keyedItem)->ignore
429
- keyedItems->Dict.set(key, keyedItem)
430
- } else {
431
- newOrder->Array.push(existing)->ignore
432
- }
433
- | None => {
434
- let node = renderItem(item)
435
- let element = render(node)
436
- let keyedItem = {key, item, element}
437
- newOrder->Array.push(keyedItem)->ignore
438
- keyedItems->Dict.set(key, keyedItem)
439
- }
440
- }
441
- })
442
-
443
- /* Phase 3: Reconcile DOM */
444
- let marker = ref(DOM.getNextSibling(startAnchor))
445
-
446
- newOrder->Array.forEach(keyedItem => {
447
- let currentElement = marker.contents
448
-
449
- switch currentElement->Nullable.toOption {
450
- | Some(elem) if elem === endAnchor =>
451
- DOM.insertBefore(parent, keyedItem.element, endAnchor)
452
- | Some(elem) if elem === keyedItem.element => marker := DOM.getNextSibling(elem)
453
- | Some(elem) => {
454
- let needsReplacement =
455
- elementsToReplace->Dict.get(keyedItem.key)->Option.getOr(false)
456
-
457
- if needsReplacement {
458
- disposeElement(elem)
459
- DOM.replaceChild(parent, keyedItem.element, elem)
460
- marker := DOM.getNextSibling(keyedItem.element)
461
- } else {
462
- DOM.insertBefore(parent, keyedItem.element, elem)
463
- marker := DOM.getNextSibling(keyedItem.element)
464
- }
465
- }
466
- | None => DOM.insertBefore(parent, keyedItem.element, endAnchor)
467
- }
468
- })
469
- }
470
- }
471
- }
472
-
473
- /* Initial render */
474
- let fragment = DOM.createDocumentFragment()
475
- fragment->DOM.appendChild(startAnchor)
476
-
477
- let initialItems = Signal.peek(signal)
478
- initialItems->Array.forEach(item => {
479
- let key = keyFn(item)
480
- let node = renderItem(item)
481
- let element = render(node)
482
- let keyedItem = {key, item, element}
483
- keyedItems->Dict.set(key, keyedItem)
484
- fragment->DOM.appendChild(element)
485
- })
486
-
487
- fragment->DOM.appendChild(endAnchor)
488
-
489
- runWithOwner(owner, () => {
490
- let disposer = Effect.runWithDisposer(() => {
491
- reconcile()
492
- None
493
- })
494
- addDisposer(owner, disposer)
495
- })
496
-
497
- fragment
498
- }
499
- }
500
- }
501
- }
502
-
503
- /* ============================================================================
504
- * Public API
505
- * ============================================================================ */
506
-
507
- /* Text nodes */
508
- let text = (content: string): node => Text(content)
509
-
510
- let signalText = (compute: unit => string): node => {
511
- let signal = Computed.make(compute)
512
- SignalText(signal)
513
- }
514
-
515
- let signalInt = (compute: unit => int): node => {
516
- let signal = Computed.make(() => compute()->Int.toString)
517
- SignalText(signal)
518
- }
519
-
520
- let signalFloat = (compute: unit => float): node => {
521
- let signal = Computed.make(() => compute()->Float.toString)
522
- SignalText(signal)
523
- }
524
-
525
- /* Static text nodes with type-specific helpers */
526
- let int = (value: int): node => Text(Int.toString(value))
527
-
528
- let float = (value: float): node => Text(Float.toString(value))
529
-
530
- /* Fragments */
531
- let fragment = (children: array<node>): node => Fragment(children)
532
-
533
- let signalFragment = (signal: Signal.t<array<node>>): node => SignalFragment(signal)
534
-
535
- /* Lists */
536
- let list = (signal: Signal.t<array<'a>>, renderItem: 'a => node): node => {
537
- let nodesSignal = Computed.make(() => {
538
- Signal.get(signal)->Array.map(renderItem)
539
- })
540
- SignalFragment(nodesSignal)
541
- }
542
-
543
- let keyedList = (
544
- signal: Signal.t<array<'a>>,
545
- keyFn: 'a => string,
546
- renderItem: 'a => node,
547
- ): node => {
548
- KeyedList({
549
- signal: Obj.magic(signal),
550
- keyFn: Obj.magic(keyFn),
551
- renderItem: Obj.magic(renderItem),
552
- })
553
- }
554
-
555
- /* Element constructor */
556
- let element = (
557
- tag: string,
558
- ~attrs: array<(string, attrValue)>=[]->Array.map(x => x),
559
- ~events: array<(string, Dom.event => unit)>=[]->Array.map(x => x),
560
- ~children: array<node>=[]->Array.map(x => x),
561
- (),
562
- ): node => Element({tag, attrs, events, children})
563
-
564
- /* Null representation */
565
- let null = () => text("")
566
-
567
- /* Mounting */
568
- let mount = (node: node, container: Dom.element): unit => {
569
- let el = Render.render(node)
570
- container->DOM.appendChild(el)
571
- }
572
-
573
- let mountById = (node: node, containerId: string): unit => {
574
- switch DOM.getElementById(containerId)->Nullable.toOption {
575
- | Some(container) => mount(node, container)
576
- | None => Console.error("Container element not found: " ++ containerId)
577
- }
578
- }
1
+ @deprecated("Use View instead. Node is deprecated and will be removed in a future release.")
2
+ include View