Bonsai, window size, Value.t, Computation.t

Question:

Let Pt2Int.t = { x: int; y : int }. Suppoose we want to implement a reactive
variable for the global window size.

As far as I can tell, this requires the use of Bonsai.Edge:

Is this the correct way to implement Pt2Int.t Bonsai.Computation.t

module Action = struct
  type t = Set_size of { size : Pt2Int.t } [@@deriving sexp, equal]
end

module Model = struct
  type t = { size : Pt2Int.t } [@@deriving sexp, equal]

  let apply (_model : t) (action : Action.t) : t =
    match action with
    | Set_size s -> { size = s.size }

  let get_window_size () : Pt2Int.t = Pt2Int.{ x = Dom_html.window##.innerWidth; y = Dom_html.window##.innerHeight }
end

module type Global_Window_Size = sig
  type t [@@deriving sexp, equal]
end

type t = { model : Model.t; inject : Action.t list -> unit Effect.t }

So far, this is all pretty boring. We implement the Model.t which stores the size,
and the only valid Action is setting the size.

let register_onresize inject =
  Firebug.console##log "registering Global_Window_Size: changed / resize";
  Dom_html.window##.onresize :=
    Dom_html.handler (fun _ ->
        Firebug.console##log "Global_Window_Size: changed / resize";
        let _ =
          Vdom.Effect.Expert.handle_non_dom_event_exn
          @@ inject [ Action.(Set_size { size = Model.get_window_size () }) ]
        in
        Js.bool false)

This is the function we call to setup the onresize handler.
The onresize handler injects the Action.Set_size .

let window_inner_size : t Bonsai.Computation.t =
  let%sub model, inject =
    Bonsai.state_machine0 [%here]
      (module Model)
      (module struct
        type t = Action.t list [@@deriving sexp, equal]
      end)
      ~default_model:Model.{ size = Model.get_window_size () }
      ~apply_action:(fun ~inject:_ ~schedule_event:_ model actions -> List.fold actions ~init:model ~f:Model.apply)
  in
  let on_activate : unit Effect.t Value.t  = 
    let%map inject = inject in
    register_onresize inject ;
    Effect.return @@ Firebug.console##log "on activate Global_Window_Size: changed / resize" in
  let on_deactivate : unit Effect.t Value.t = 
    let%map _ = inject in
    Effect.return @@ Firebug.console##log "on deactivate Global_Window_Size: changed / resize" in
  let%sub _ = Bonsai.Edge.lifecycle ~on_deactivate ~on_activate  ()  in
  let%arr model = model and inject = inject in
  { model; inject }

This is where things get realy iffy for me. In an execution of the program, I get:

on deactivate Global_Window_Size: changed / resize
registering Global_Window_Size: changed / resize
on activate Global_Window_Size: changed / resize

Why is “on deactivate” running at all? Why does it run before “on activate” ?
This I struggle with.

Another problem I have with this is: how do ensure that “on activate” only runs once ?

I am afraid that if initialize this multiple times, only the last one gets the updates.

Question: What is the correct way to implement a Pt2Int.t Value.t for keeping track
of the window size?

Thanks!

1 Like

Hi! Really good question! There are a couple of questions in your post, so I’ll attempt to answer them one by one!

Question: What is the correct way to implement a Pt2Int.t Value.t for keeping track
of the window size?

There is a function that does something very similar to what you want, but not exactly what you want within the Size_tracker module in bonsai.web_ui_element_size_hooks [1]. It provides a function
that runs anytime the size of an Vdom.Node.t changes letting you keep track of arbitrary divs like:

module Dimensions = struct
  type t =
    { width : float
    ; height : float
    }
end

let component : (Vdom.Node.t * Dimensions.t option) Computation.t =
  let open Bonsai.Let_syntax in
  let%sub dimensions, set_dimensions = Bonsai.state None in
  let%arr set_dimensions = set_dimensions
  and dimensions = dimensions in
  let node =
    Vdom.Node.div
      ~attrs:
        [ Bonsai_web_ui_element_size_hooks.Size_tracker.on_change
            (fun ~width ~height ->
            set_dimensions (Some { Dimensions.width; height }))
        ]
      [ Vdom.Node.text "Capybaras are cool" ]
  in
  node, dimensions
;;

A better example of this hook being use lives here: (Meta: Oh whoops, I am a new user and can only put two links per post, so I am referring to examples/element_size_util/main.ml:90 in janestreet/bonsai on github)

You could attach this to your top-level Vdom.Node.t at the top level of your app, although this will track that node’s size, and not the window’s size which is different, but might be fine for your use case depending on the purpose that you want to track the size.

If tracking the windows is essential for you use case, you can implement a “global hook”. This would involve hopefully a copy-paste of (Meta: Same two link restriction :frowning: I am referring to the ml file of [1] “element_size_tracker.ml”) with a tiny change of the observe function to instead of listening for resize events on the node that the Attr.t is attached, to it’d listen to the Dom_html.window node instead of its node parameter.

I am afraid that if initialize this multiple times, only the last one gets the updates.

I think that because the implementation uses ResizeObserver API instead of setting the on_resize field, your concern of many elements accessing the value is that both elements would both read the size, and be fine, although I would need to experiment to double-check.

[1] https://github.com/janestreet/bonsai/blob/master/web_ui/element_size_hooks/src/size_tracker.mli

Why is “on deactivate” running at all? Why does it run before “on activate” ?
This I struggle with.

This question is a bit harder to answer. The short answer is that you need to do:

Effect.of_sync_fun (fun () -> Firebug.console##log "on activate Global_Window_Size: changed / resize") ()
(* or... *)
Effect.of_sync_fun print_endline "capybaras are cool"
(* or a short-hand function for sexp printing: *)
Effect.print_s [%message "capybaras are cool" (true : bool)]

instead of:

Effect.return @@ Firebug.console##log "on activate Global_Window_Size: changed / resize"

A longer way of answering this is that side effects in bonsai are weird and can run in surprising times, but that’s a bit of a non-answer.:

In the following code:

  Bonsai.Edge.lifecycle
    ~on_activate:
      (let%map inject = inject in
       Effect.return (print_endline "capybaras are cool"))
    ()

print_endline at “graph stabilization time” or any time that the let%map computation is computed, which, in this case is any time that inject changes, and can be hard to control. Using Effect.of_sync_fun will not call print_endline until the computed unit Effect.t is actually scheduled and run whenever on_activate is scheduled and run.

Also happy to answer any other follow up questions you may have!

2 Likes