package bonsai
A library for building dynamic webapps, using Js_of_ocaml
Install
Dune Dependency
Authors
Maintainers
Sources
bonsai-v0.16.0.tar.gz
sha256=1d68aab713659951eba5b85f21d6f9382e0efa8579a02c3be65d9071c6e86303
doc/src/bonsai.vdom_node_with_map_children/vdom_node_with_map_children.ml.html
Source file vdom_node_with_map_children.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
open! Core open! Bonsai_web open! Js_of_ocaml open! Bonsai.Let_syntax module Input = struct type ('k, 'cmp) t = { children : ('k, Vdom.Node.t, 'cmp) Map.t ; tag : string ; attr : Vdom.Attr.t option } let to_empty_vdom { tag; attr; children = _ } = Vdom.Node.create tag ?attrs:(Option.map attr ~f:(fun attr -> [ attr ])) [] ;; end let diff_patch ~prev ~next ~element = let diff = Vdom.Node.Patch.create ~previous:prev ~current:next in let (_ : Js_of_ocaml.Dom_html.element Js.t) = Vdom.Node.Patch.apply diff element in () ;; module Widget (K : Comparator.S) = struct type dom = Dom_html.element module State = struct type t = { elements : (Vdom.Node.t * Dom_html.element Js.t) Map.M(K).t ; me : dom Js.t } let sexp_of_t = sexp_of_opaque end let name = "vdom-node-with-map-children" (* This function diffs two vdom nodes. It is notable for being happy to diff nodes that have different keys. *) let do_change ~key_before ~key_after ~elements current_vdom = let old_vdom, old_element = Map.find_exn elements key_before in let patch = Vdom.Node.Patch.create ~previous:old_vdom ~current:current_vdom in let current_element = (* if there isn't a patch, we can use the old element without issue *) if Vdom.Node.Patch.is_empty patch then old_element else Vdom.Node.Patch.apply patch old_element in let elements = let elements = (* This branch potentially saves a second tree traversal on the common case where both the before and after key are equal. *) match phys_equal key_before key_after with | true -> elements | false -> Map.remove elements key_before in Map.set elements ~key:key_after ~data:(current_vdom, current_element) in elements, current_element ;; module Acc = struct (* On every frame, we build an Acc.t, and keep track of which rows were added and removed ("changes" are just immediately diffed/patched without winding up in this structure). After all added and removed nodes are visited, we "finalize" the structure by attempting to pair up rows to diff and re-insert. *) type t = { added : (K.t * Vdom.Node.t) list ; removed : (K.t * Vdom.Node.t * Dom_html.element Js.t) list ; state : State.t } let create ~state = { added = []; removed = []; state } let add t ~key ~vdom = let added = (key, vdom) :: t.added in { t with added } ;; let remove t ~key = let vdom, dom_node = Map.find_exn t.state.elements key in let removed = (key, vdom, dom_node) :: t.removed in { t with removed } ;; (* perform a reconciliation here becuase these vdom nodes share a key, so it's likely that they have similar vdom contents *) let change t ~key ~current_vdom = let elements, _new_element = do_change ~key_before:key ~key_after:key ~elements:t.state.elements current_vdom in { t with state = { t.state with elements } } ;; (* This empty div with a key is used to delete nodes by diffing/patching them against it. Because no other node will have this specific key, they'll always be removed. The reason we do this instead of just deleting the node out of the dom is so that their widget and hook "destroy" functions are called. The "key" is a random guid to avoid collisions. *) let empty_node = Vdom.Node.div ~key:"4b75966c-60a2-46f5-a2bb-4939e2cb4c52" [] let rec finalize_loop ({ added; removed; state } as t) ~to_re_insert = match added, removed with | [], [] -> t, to_re_insert | [], (key, _, _) :: removed -> (* we only have nodes to remove *) let elements, empty_element = do_change ~key_before:key ~key_after:key ~elements:state.elements empty_node in let elements = Map.remove elements key in Dom.removeChild state.me empty_element; finalize_loop { added = []; removed; state = { state with elements } } ~to_re_insert | (key, vdom) :: added, [] -> (* we only have nodes to insert *) let child = Vdom.Node.to_dom vdom in let elements = Map.set state.elements ~key ~data:(vdom, child) in let to_re_insert = Map.set to_re_insert ~key ~data:child in finalize_loop { added; removed = []; state = { state with elements } } ~to_re_insert | (key_after, current_vdom) :: added, (key_before, _, _dom) :: removed -> (* we have a node to remove and a node to add, diff them *) let elements, new_dom = do_change ~key_before ~key_after ~elements:state.elements current_vdom in let to_re_insert = Map.set to_re_insert ~key:key_after ~data:new_dom in finalize_loop { added; removed; state = { state with elements } } ~to_re_insert ;; let prepend (element : Dom.element Js.t) (node : Dom.node Js.t) : unit = (* NOTE: This is done because Js_of_ocaml does not have bindings to the prepend method yet. https://developer.mozilla.org/en-US/docs/Web/API/Element/prepend *) Js.Unsafe.meth_call element "prepend" [| Js.Unsafe.inject node |] ;; let finalize t = let { state; _ }, to_re_insert = finalize_loop t ~to_re_insert:(Map.empty (module K)) in (* re-insertions are kept in a map so that they are visited in ascending order. *) Map.iteri to_re_insert ~f:(fun ~key ~data:node -> (* both [insertBefore] and [prepend] will move the existing element, if it exists in the DOM. *) match Map.closest_key state.elements `Less_than key with | Some (_k, (_vdom, prior)) -> state.me##insertBefore (node :> Dom.node Js.t) prior##.nextSibling |> (ignore : Dom.node Js.t -> unit) | None -> let me = (state.me :> Dom.element Js.t) in prepend me (node :> Dom.node Js.t)); state ;; end let create input = let dom = Vdom.Node.to_dom (Input.to_empty_vdom input) in let elements = Map.map input.Input.children ~f:(fun vdom -> let element = Vdom.Node.to_dom vdom in dom##appendChild (element :> Dom.node Js.t) |> (ignore : Dom.node Js.t -> unit); vdom, element) in { State.elements; me = dom }, dom ;; let destroy ~prev_input ~state ~element = (* destroying this widget is accomplished by running through the whole map and removing each child individually. *) let acc = Map.fold prev_input.Input.children ~init:(Acc.create ~state) ~f:(fun ~key ~data:_ acc -> Acc.remove acc ~key) in let _state = Acc.finalize acc in (* If our previous input had a hook in its attr, then we need to patch in a version without the attrs in order to run the hook destruction logic. *) if Option.is_some prev_input.attr then diff_patch ~prev:(Input.to_empty_vdom prev_input) ~next:(Input.to_empty_vdom { prev_input with attr = None }) ~element ;; let update ~prev_input ~input ~state ~element = if not (String.equal prev_input.Input.tag input.Input.tag) then ( (* we can't do an in-place diff / patch on two elements that have different tags, so let's just obliterate the previous one and re-create *) destroy ~prev_input ~state ~element; create input) else ( if not (Option.equal phys_equal prev_input.attr input.attr) then diff_patch ~prev:(Input.to_empty_vdom prev_input) ~next:(Input.to_empty_vdom input) ~element; let init = Acc.create ~state in let acc = Map.fold_symmetric_diff ~data_equal:phys_equal prev_input.Input.children input.Input.children ~init ~f:(fun acc -> function | key, `Left _vdom -> Acc.remove acc ~key | key, `Right vdom -> Acc.add acc ~key ~vdom | key, `Unequal (_old_vdom, current_vdom) -> Acc.change acc ~key ~current_vdom) in Acc.finalize acc, element) ;; let to_vdom_for_testing = `Custom (fun { Input.children; attr; tag } -> Vdom.Node.create tag ?attrs:(Option.map attr ~f:(fun attr -> [ attr ])) (Map.data children)) ;; module Input = struct type t = (K.t, K.comparator_witness) Input.t let sexp_of_t = sexp_of_opaque end end (* js-only: The instancer is a weak-map from comparator objects to a widget-implemented vdom function. This is so that each kind of map will get its own widget for safe diffing. The implementation of this function is quite scary, no doubt, but Carl claims that physical-equality of comparator objects is proof that the key and comparator_witness types are guaranteed to be equal *) module Instancer : sig val get : map:('k, Vdom.Node.t, 'cmp) Map.t -> ('k, 'cmp) Input.t -> Vdom.Node.t end = struct module Weak_map = Jsoo_weak_collections.Weak_map let instances : (Obj.t, Obj.t) Weak_map.t = Weak_map.create () let get (type k cmp) ~(map : (k, Vdom.Node.t, cmp) Map.t) : (k, cmp) Input.t -> Vdom.Node.t = let key = (Obj.repr : _ Core.Comparator.t -> Obj.t) (Map.comparator map) in match Weak_map.get instances key with | Some i -> (Obj.obj : Obj.t -> (k, cmp) Input.t -> Vdom.Node.t) i | None -> let module M = Widget (struct include (val Map.comparator_s map) end) in let f = unstage (Vdom.Node.widget_of_module (module M)) in Weak_map.set instances key ((Obj.repr : (_ Input.t -> Vdom.Node.t) -> Obj.t) f); f ;; end let make ~tag ?attr children = let f = Instancer.get ~map:children in f { Input.children; tag; attr } ;;
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>