incr_dom_interactive

A monad for composing chains of interactive UI elements
Library incr_dom_interactive
type 'a t

'a Interactive.t is a monad. Within this monad, your program can receive input from the user using DOM elements such as checkboxes, text fields, or buttons, and display output to the user using text, tables, images, or anything else that can be represented as a DOM node.

The meaning of the 'a parameter is that 'a Interactive.t allows the user to provide your program a value of type 'a.

For example:

  • A text box is a string Interactive.t.
  • A checkbox is a bool Interactive.t.
  • A button is a [Pressed | Not_pressed] Interactive.t.
  • Static text is a unit Interactive.t.

Since Interactive.t is a monad, you can inspect the user's input and decide afterwards what the rest of the Interactive.t should be.

For example, this defines a form which only allows the user to submit if they have entered at least 10 characters:

let open Interactive.Let_syntax in
let open Interactive.Primitives in
let submit_button = button ~text:"Submit" () in
let%bind_open user_input = text () in
if String.length user_input < 10
then
  let%map_open () = message "Please enter at least 10 characters." in
  None
else
  match%map submit_button with
  | Not_pressed -> None
  | Pressed -> Some user_input

If you have used Incr_dom, then you are familiar with the pattern of creating Virtual_dom nodes with callbacks that convert user input into Incr_dom actions and then into Virtual_dom events using the inject function. For instance:

Node.input
  [ Attr.on_input (fun _ev text -> inject (Action.Submit text)) ]
  []

Interactive works in the same way. (In fact, this is how it is implemented.) To render an 'a Interactive.t, you must provide functions for converting values of 'a into actions and actions into Virtual_dom events. These functions are used in the callbacks of the Virtual_dom nodes returned by the render function. Then, each time the underlying value of the 'a Interactive.t changes as a result of a user action (entering text in a text field, checking a checkbox, selecting from a drop-down menu, etc.), this results in an event created from the updated 'a value.

For example, you might render the form defined above in the view function of your Incr_dom app as follows:

let view model ~inject =
  ...
  let nodes: Node.t Incr.t =
    Interactive.render form
      ~on_input:(fun x -> Action.Submit x)
      ~inject
  in
  ...

We already handled invalid user inputs above, so we don't have to handle them here.

Note: Be careful about creating a new Interactive.t within a bind.

Consider this code:

let%bind_open is_checked = checkbox () in
if is_checked
then text ~init:"Foo" ()
else text ~init:"Bar" ()

Whenever the checkbox's value is changed, this recreates the text field. So if the user modifies the value of the checkbox, the text field's value is lost. Instead, prefer the following:

let checked_text = text ~init:"Foo" () in
let unchecked_text = text ~init:"Bar" () in
let%bind_open is_checked = checkbox () in
if is_checked
then checked_text
else unchecked_text

This code is better because if the user modifies "Foo", then checks the box and unchecks it again, their input will be saved.

include Core.Monad.S_without_syntax with type 'a t := 'a t
val (>>=) : 'a t -> ( 'a -> 'b t ) -> 'b t

t >>= f returns a computation that sequences the computations represented by two monad elements. The resulting computation first does t to yield a value v, and then runs the computation returned by f v.

val (>>|) : 'a t -> ( 'a -> 'b ) -> 'b t

t >>| f is t >>= (fun a -> return (f a)).

module Monad_infix : sig ... end
val bind : 'a t -> f:( 'a -> 'b t ) -> 'b t

bind t ~f = t >>= f

val return : 'a -> 'a t

return v returns the (trivial) computation that returns v.

val map : 'a t -> f:( 'a -> 'b ) -> 'b t

map t ~f is t >>| f.

val join : 'a t t -> 'a t

join t is t >>= (fun t' -> t').

val ignore_m : 'a t -> unit t

ignore_m t is map t ~f:(fun _ -> ()). ignore_m used to be called ignore, but we decided that was a bad name, because it shadowed the widely used Caml.ignore. Some monads still do let ignore = ignore_m for historical reasons.

val all : 'a t list -> 'a list t
val all_unit : unit t list -> unit t

Like all, but ensures that every monadic value in the list produces a unit value, all of which are discarded rather than being collected into a list.

module Primitives : sig ... end
val render : 'a t -> on_input:( 'a -> 'action ) -> inject:( 'action -> unit Incr_dom.Vdom.Effect.t ) -> Incr_dom.Vdom.Node.t Incr_dom.Incr.t

You have to schedule an action whenever the state changes in order for the view to update correctly.

If you don't want to take any action, you should define an action which has no effect on your model.

val map_nodes : 'a t -> f:( Incr_dom.Vdom.Node.t list -> Incr_dom.Vdom.Node.t list ) -> 'a t

map_nodes can be used to change the presentation of the Interactive.t. For example, the following takes an Interactive.t and produces a Node that has a red background when rendered:

Interactive.map_nodes t ~f:(fun nodes ->
  Node.div [Attr.style ["background", "red"]] nodes)
val map_nodes_value_dependent : 'a t -> f:( 'a -> Incr_dom.Vdom.Node.t list -> Incr_dom.Vdom.Node.t list ) -> 'a t

map_nodes_value_dependent is like map_nodes, but the function can also depend on the current value of the Interactive.t.

For example, in a 'a Or_error.t Interactive.t, you could use this to add a node which displays the error.

val wrap_in_div : ?attrs:Incr_dom.Vdom.Attr.t list -> 'a t -> 'a t
val of_incr : 'a Incr_dom.Incr.t -> 'a t
val current_value : 'a t -> 'a

current_value calls Incr.stabilize.

See also render, which is the typical way of handling changes to the value.

module Let_syntax : sig ... end