Virtualdom / Svg / Blob for Icon

I have an SVG Icon, which is basically a

<svg>
lots of data for paths / strokes / fills
<?svg>

To use this in VirtualDom, it wants me to break the paths down to Virtual_dom_svg.Attr.path_op list which it then sprintfs back into a string.

I’d prefer not to do this as I have many SVG Icons, and I do not want to manually convert them by hand.

The good news: these SVG Icons never change. Is there some way I feed VirtualDom a big literal string of

<svg>
...
</svg>

instead of converting the svg into Virtual_dom_svg AST?

Thanks!

The good news: these SVG Icons never change

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)))
1 Like

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 :cherries: icon via its id="cherry").

1 Like

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”

I agree that your solution is the faster one.

Another solution would be to create a Widget that renders svgs. virtual_dom/src/node.mli at master · janestreet/virtual_dom · GitHub. You have complete control of the DOM within them

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

link: virtual_dom/src/node.mli at master · janestreet/virtual_dom · GitHub

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 ()))
  ...

(my 2 cents, anyway)