···11+open Js_top_worker_message.Message
22+33+(* --- Send function, injected by worker at startup --- *)
44+55+let sender : (string -> unit) ref = ref (fun _ -> ())
66+77+let set_sender f = sender := f
88+99+let send_msg msg =
1010+ let json = json_of_worker_msg msg in
1111+ !sender json
1212+1313+(* --- Handler registry --- *)
1414+1515+type widget_state = {
1616+ handlers : (string * (string option -> unit)) list;
1717+}
1818+1919+let widgets : (string, widget_state) Hashtbl.t = Hashtbl.create 16
2020+2121+(* --- Public API --- *)
2222+2323+let display ~id ~handlers view =
2424+ Hashtbl.replace widgets id { handlers };
2525+ send_msg (WidgetUpdate { widget_id = id; view })
2626+2727+let update ~id view =
2828+ send_msg (WidgetUpdate { widget_id = id; view })
2929+3030+let clear ~id =
3131+ Hashtbl.remove widgets id;
3232+ send_msg (WidgetClear { widget_id = id })
3333+3434+let handle_event ~widget_id ~handler_id ~value =
3535+ match Hashtbl.find_opt widgets widget_id with
3636+ | None -> ()
3737+ | Some state ->
3838+ match List.assoc_opt handler_id state.handlers with
3939+ | None -> ()
4040+ | Some handler -> handler value
+55
widget/widget.mli
···11+(** Interactive widget support for the OCaml toplevel.
22+33+ Widgets are rendered in the client as HTML elements built from
44+ {!Js_top_worker_message.Widget_view.node} trees. Event handlers in the view
55+ are symbolic string identifiers — when the user interacts with a widget,
66+ the client sends the handler ID and input value back to the worker,
77+ where the registered callback is invoked.
88+99+ Typical usage with Note FRP:
1010+ {[
1111+ let e, send = Note.E.create ()
1212+ let s = Note.S.hold 50 e
1313+1414+ let () =
1515+ Widget.display ~id:"my-slider"
1616+ ~handlers:["x", (fun v ->
1717+ send (int_of_string (Option.get v)))]
1818+ (Js_top_worker_message.Widget_view.Element { tag = "input";
1919+ attrs = [Property ("type", "range")];
2020+ children = [] })
2121+2222+ (* Wire up automatic updates via Note: *)
2323+ let _logr = Note.S.log
2424+ (Note.S.map (fun v -> ... build view ...) s)
2525+ (Widget.update ~id:"my-slider")
2626+ ]} *)
2727+2828+val display :
2929+ id:string ->
3030+ handlers:(string * (string option -> unit)) list ->
3131+ Js_top_worker_message.Widget_view.node ->
3232+ unit
3333+(** [display ~id ~handlers view] registers a widget with the given [id],
3434+ installs [handlers] for routing incoming events, and sends the
3535+ initial [view] to the client. If a widget with this [id] already
3636+ exists, it is replaced. *)
3737+3838+val update : id:string -> Js_top_worker_message.Widget_view.node -> unit
3939+(** [update ~id view] sends an updated view for an existing widget.
4040+ The handler map is not changed. *)
4141+4242+val clear : id:string -> unit
4343+(** [clear ~id] removes the widget and its handlers. Sends a
4444+ WidgetClear message to the client. *)
4545+4646+val handle_event :
4747+ widget_id:string -> handler_id:string -> value:string option -> unit
4848+(** [handle_event ~widget_id ~handler_id ~value] routes an incoming
4949+ event to the registered handler. Called by the worker message loop
5050+ when a WidgetEvent is received. *)
5151+5252+val set_sender : (string -> unit) -> unit
5353+(** [set_sender f] installs the function used to send JSON strings to
5454+ the client. Called once by the worker at startup. The function [f]
5555+ should call [Worker.post_message]. *)