Why are you not just making a static file and referencing symbols in a <use> element ala SVG symbol a Good Choice for Icons | CSS-Tricks - CSS-Tricks? SVGs being text and not a binary format makes them easy to track in version control when a new icon is needed.
If you have (?) a way to create a virtual dom node from a dom node then you should simply create a dom node for the svg via a DOMParser and inject it that way. E.g. that’s the way you would go about it in brr, maybe you can adapt something similar:
let make_icon : Jstr.t -> Brr.El.t = fun s - >
(* Protect web workers *)
if Jv.is_none (Brr.Document.to_jv Brr.G.document)
then Brr.El.of_jv Jv.undefined else
let dom_parser = Jv.get Jv.global "DOMParser" in
let p = Jv.new' dom_parser [||] in
let c = Jv.call p "parseFromString" Jv.[|of_jstr s; of_string "text/html"|] in
List.hd (Brr.El.children (Brr.Document.body (Brr.Document.of_jv c)))
I ended up going with Vdom.Attr.create : string -> string -> ... . The first argument is “d”, the 2nd argument is the actual path. This turns out to be the underlying function Vdom.Attr.our_favorite_attr uses under the hood.
Using this method, I can not put up the entire svg as a string, but I can upload each path as a string, instead of a Virtual_dom_svg.Attr.path_op list, which is good enough for me.
@toastal@dbuenzli : Did not get to test out your solutions, but appreciate the suggestions. Thanks!
What is the use case for needing it in the virtual DOM?
If the icons never changes, the VDOM should not be loading it or even thinking about diffing it. You can reference parts of a SVG by a fragment identifier which means you should be able to ship the a static assets/icons.svg file (ideally compressed with Scour, cached on the client, and shipped with a prefetch header) filled with lots of <symbol>s and then reference any icon by reference with <use> (i.e. <use href="assets/icons.svg#cherry"> to get a icon via its id="cherry").
Just to clarify. Bonsai demands a Vdom.Node.t Bonsai.Computation.t, so my choice is NOT “use virtual dom” vs “do not use virtual dom”. My choice is “vdom node that includes data” vs “vdom node that references data”
This looks really cool, like something future me will need.
There is one part i do not understand
val widget
: ?vdom_for_testing:t Lazy.t
-> ?destroy:('s -> (#Dom_html.element as 'e) Js.t -> unit)
-> ?update:('s -> 'e Js.t -> 's * 'e Js.t)
-> id:('s * 'e Js.t) Type_equal.Id.t
-> init:(unit -> 's * 'e Js.t)
-> unit
-> t
So to create a widget, we need to have an update function. This update function has signature:
-> ?update:('s -> 'e Js.t -> 's * 'e Js.t)
So we have
input: 's = old “data state”
input: 'e Js.t = old JS dom tree
output: 's * 'e JS.t = new “data state” new JS dom tree
but aren’t we missing an input arguent, i.e. an “action / diff” of some form? How we supposed to compute the new 's from the old 's without some type of what CHANGE happened ?
Hmm. Looking at the ml file widget_of_module allows you to do exactly that. widget seems to be special-cased to input being unit.
I’m still a little new to ocaml, but if I had to guess…widget_of_module is the real widget, and the only reason to create a widget function api that doesn’t require you to create a whole module is that special case of input being unit.
This is what I see on github. I don’t see how this works:
val widget
: ?vdom_for_testing:t Lazy.t
-> ?destroy:('s -> (#Dom_html.element as 'e) Js.t -> unit)
-> ?update:('s -> 'e Js.t -> 's * 'e Js.t)
-> id:('s * 'e Js.t) Type_equal.Id.t
-> init:(unit -> 's * 'e Js.t)
-> unit
-> t
(** [widget_of_module] is very similar to [widget], but it pulls all of the
callbacks out into a first-class module. Read the comment for [widget] to learn more.
It is very important that you call [widget_of_module] exactly once for any
"widget class" that you want to construct. Otherwise, the nodes created by
it won't be comparable against one another and the widget-diffing will just
run [destroy, init, destroy, init] over and over. *)
val widget_of_module
: (module Widget.S with type Input.t = 'input)
-> ('input -> t) Staged.t
The widget of_module code has an update function that looks at prev_input and input:
And an (somewhat abstract) example here - the other example I saw in bonsai chose not to do anything with prev_input / input:
So the remaining question is the utility of a widget function that has Unit input and optional update and delete (i.e. the widget function is the equivalent of calling of_module with input being ().) My take is that of_module is useful for writing reusable components, and widget is useful for just conveniently returning vdom that can render whatever you want. If we take svgs as an example, the expected use with of_module seems to be to take an input type t that represents which icon to render (and perhaps also parameters like size/color), and have update properly change the icon and size if prev_input and input differ. This would be a reusable svg component implemented with vdom widgets that I could then use from different functions (or in this case different bonsai computations.)
With the widget function above that has unit input widget I imagine one could be lazier - whatever function/computation you have that returns vdom and also needs to render an svg can just call widget directly and pass in an init function that renders the specific svg you want it to render. Your code doesn’t actually need genericness of a reusable widget component, it just needs to return vdom. So something like
let svg = {|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12" height="12"><path d="M2.22 2.22a.749.749 0 0 1 1.06 0L6 4.939 8.72 2.22a.749.749 0 1 1 1.06 1.06L7.061 6 9.78 8.72a.749.749 0 1 1-1.06 1.06L6 7.061 3.28 9.78a.749.749 0 1 1-1.06-1.06L4.939 6 2.22 3.28a.749.749 0 0 1 0-1.06Z"></path></svg>
|} in
let create_svg_dom () =
(* I don't know if this dom creation code compiles/typechecks, I just copy/pasted the relevant lines from the above brr example to give a flavor of what this would be *)
let dom_parser = Jv.get Jv.global "DOMParser" in
let p = Jv.new' dom_parser [||] in
let c = Jv.call p "parseFromString" Jv.[|of_jstr svg; of_string "text/html"|] in
List.hd (Brr.El.children (Brr.Document.body (Brr.Document.of_jv c)))
in
widget
~init:(fun () -> (), (create_svg_dom ()))
...