Source file raw.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
open Base
open Js_of_ocaml

module Js_object = struct
  type t = Js.Unsafe.any

  let empty_obj () = Js.Unsafe.obj [||]
  let set_prop_ascii t name value = Js.Unsafe.set t (Js.string name) value
  let get_prop_ascii t name = Js.Unsafe.get t (Js.string name)
  let has_property t name = Js.Optdef.test (Js.Unsafe.get t (Js.string name))
  let is_undefined a = not (Js.Optdef.test a)
end

module Attrs = struct
  type t = Js_object.t

  let create () : t = Js_object.empty_obj ()

  let set_property : t -> string -> t -> unit =
    fun t name value -> Js_object.set_prop_ascii t name value
  ;;

  let has_property : t -> string -> bool = Js_object.has_property

  let has_attribute t name =
    Js_object.has_property t "attributes"
    && Js_object.has_property (Js_object.get_prop_ascii t "attributes") name
  ;;

  let set_attribute : t -> string -> t -> unit =
    fun t name value ->
      if Js_object.is_undefined (Js_object.get_prop_ascii t "attributes")
      then Js_object.set_prop_ascii t "attributes" (Js_object.empty_obj ());
      Js_object.set_prop_ascii (Js_object.get_prop_ascii t "attributes") name value
  ;;
end

type virtual_dom_node
type virtual_dom_patch

module Virtual_dom = struct
  class type virtual_dom = object
    method _VNode :
      (Js.js_string Js.t
       -> Attrs.t
       -> virtual_dom_node Js.t Js.js_array Js.t
       -> Js.js_string Js.t Js.optdef
       -> virtual_dom_node Js.t)
        Js.constr
        Js.readonly_prop

    method _VText :
      (Js.js_string Js.t -> virtual_dom_node Js.t) Js.constr Js.readonly_prop

    method createElement : virtual_dom_node Js.t -> Dom_html.element Js.t Js.meth

    method diff :
      virtual_dom_node Js.t -> virtual_dom_node Js.t -> virtual_dom_patch Js.t Js.meth

    method patch :
      Dom_html.element Js.t -> virtual_dom_patch Js.t -> Dom_html.element Js.t Js.meth

    method svg :
      (Js.js_string Js.t
       -> Attrs.t
       -> virtual_dom_node Js.t Js.js_array Js.t
       -> Js.js_string Js.t Js.optdef
       -> virtual_dom_node Js.t)
        Js.constr
        Js.readonly_prop
  end

  let virtual_dom : virtual_dom Js.t = Js.Unsafe.global ##. VirtualDom
end

module Node = struct
  open Virtual_dom

  type t = virtual_dom_node Js.t

  let to_dom : virtual_dom_node Js.t -> Dom_html.element Js.t =
    fun vnode -> virtual_dom##createElement vnode
  ;;

  let node
    :  string -> Attrs.t -> virtual_dom_node Js.t Js.js_array Js.t -> string option
      -> virtual_dom_node Js.t
    =
    fun tag attrs children key ->
      let tag = Js.string tag in
      let key =
        match key with
        | None -> Js.Optdef.empty
        | Some key -> Js.Optdef.return (Js_of_ocaml.Js.string key)
      in
      let vnode = virtual_dom##._VNode in
      new%js vnode tag attrs children key
  ;;

  let svg
    :  string -> Attrs.t -> virtual_dom_node Js.t Js.js_array Js.t -> string option
      -> virtual_dom_node Js.t
    =
    fun tag attrs children key ->
      let tag = Js.string tag in
      let key =
        match key with
        | None -> Js.Optdef.empty
        | Some key -> Js.Optdef.return (Js.string key)
      in
      let vsvg = virtual_dom##.svg in
      new%js vsvg tag attrs children key
  ;;

  let text s =
    let vtext = virtual_dom##._VText in
    new%js vtext (Js.string s)
  ;;
end

module Patch = struct
  open Virtual_dom

  type t = virtual_dom_patch Js.t

  let diff : virtual_dom_node Js.t -> virtual_dom_node Js.t -> virtual_dom_patch Js.t =
    fun a b -> virtual_dom##diff a b
  ;;

  let patch : Dom_html.element Js.t -> virtual_dom_patch Js.t -> Dom_html.element Js.t =
    fun element vnode -> virtual_dom##patch element vnode
  ;;

  let create ~previous ~current = diff previous current
  let apply = patch

  let is_empty : t -> bool =
    let f =
      Js.Unsafe.pure_js_expr
        {js|
        (function (patch) {
          for (var key in patch) {
            if (key !== 'a') return false
          }
          return true
        })
      |js}
    in
    fun (t : t) -> Js.Unsafe.fun_call f [| Js.Unsafe.inject t |] |> Js.to_bool
  ;;
end

module Widget = struct
  class type ['s, 'element] widget = object
    constraint 'element = #Dom_html.element Js.t
    method type_ : Js.js_string Js.t Js.writeonly_prop

    (* virtual-dom considers two widgets of being of the same "kind" if either
       of the following holds:

       1. They both have a "name" attribute and their "id" fields are equal.
       (I think this is probably a bug in virtual-dom and have field an issue
       on github: [https://github.com/Matt-Esch/virtual-dom/issues/380])

       2. Their [init] methods are "===" equal. This is true when using virtual-dom
       widgets in the usual style in Javascript, since the [init] method will be defined
       on a prototype, but is not true in this binding as it is redefined for each
       call to [widget].

       So, we go with option 1 and must have a trivial field called [name].
    *)
    method name : unit Js.writeonly_prop
    method id : ('s * 'element) Type_equal.Id.t Js.prop
    method state : 's Js.prop
    method vdomForTesting : Node.t Lazy.t option Js.prop
    method info : Sexp.t Lazy.t option Js.prop
    method destroy : ('element -> unit) Js.callback Js.writeonly_prop

    method update :
      (('other_state, 'other_element) widget Js.t -> 'element -> 'element) Js.callback
        Js.writeonly_prop

    method init : (unit -> 'element) Js.callback Js.writeonly_prop
  end

  (* We model JS level objects here so there is a lot of throwing away of type
     information.  We could possibly try to rediscover more of it.  Or maybe we
     should see if we can get rid Widget completely.
     the unit type parameters here are not actually unit, but part of
     the type info we have thrown away into our dance
     with JS *)
  type t = Node.t

  (* here is how we throw away type information.  Our good old friend Obj.magic,
     but constrained a little bit *)
  let node_of_widget : (_, _) widget Js.t -> Node.t = Stdlib.Obj.magic

  module State_keeper = struct
    type box = T : ('a * _) Type_equal.Id.t * 'a -> box

    let t : (Js.Unsafe.any, box) Js_map.t = Js_map.create ()
    let set ~id element state = Js_map.set t (Js.Unsafe.inject element) (T (id, state))

    let get : type a b. id:(a * b) Type_equal.Id.t -> _ -> a =
      fun ~id element ->
      let element = Js.Unsafe.inject element in
      match Js_map.get t element with
      | None ->
        let id_sexp = Type_equal.Id.sexp_of_t (fun _ -> Sexp.Atom "<opaque>") id in
        raise_s [%message "BUG: element state not found" (id_sexp : Sexp.t)]
      | Some (T (f_id, state)) ->
        let T = Type_equal.Id.same_witness_exn id f_id in
        state
    ;;

    let delete element = Js_map.delete t (Js.Unsafe.inject element)
  end

  let create
        (type s)
        ?(vdom_for_testing : Node.t Lazy.t option)
        ?(destroy : s -> 'element -> unit = fun _ _ -> ())
        ?(update : s -> 'element -> s * 'element = fun s elt -> s, elt)
        ~(id : (s * 'element) Type_equal.Id.t)
        ~(init : unit -> s * 'element)
        ()
    =
    let obj : (s, _) widget Js.t = Js.Unsafe.obj [||] in
    obj##.type_ := Js.string "Widget";
    obj##.name := ();
    obj##.id := id;
    obj##.vdomForTesting := vdom_for_testing;
    obj##.init
    := Js.wrap_callback (fun () ->
      let s0, dom_node = init () in
      State_keeper.set ~id dom_node s0;
      dom_node);
    obj##.update
    := Js.wrap_callback (fun prev dom_node ->
      (* The [update] method of [obj] is only called by virtual-dom after it has checked
         that the [id]s of [prev] and [obj] are "===" equal. Thus [same_witness_exn] will
         never raise. *)
      match Type_equal.Id.same_witness_exn prev##.id id with
      | Type_equal.T ->
        let prev_state = State_keeper.get ~id dom_node in
        let state', dom_node' = update prev_state dom_node in
        State_keeper.delete dom_node;
        State_keeper.set ~id dom_node' state';
        dom_node');
    obj##.destroy
    := Js.wrap_callback (fun dom_node ->
      let prev_state = State_keeper.get ~id dom_node in
      destroy prev_state dom_node;
      State_keeper.delete dom_node);
    node_of_widget obj
  ;;
end