In this new version of the Vdom library, you can now define custom event handlers, moving away from the previously hard-coded system. Before this update, event handling was confined to a predefined set of event types, with limited data extraction capabilities. With the introduction of decoders, the approach becomes more flexible and extensible. This shift allows for more intricate operations and expands the potential use cases. Previously unsupported complex operations, such as retrieving files that users drag and drop, can now be defined with ease.
The Vdom library, previously known as Ocaml-vdom, is an open-source project launched by LexiFi and designed for functional UI application development. Proven reliable through internal use at LexiFi since 2016, Vdom offers a solid solution for web application development with ongoing maintenance.
The OCaml type used for the abstract representation is the following GADT (Generalized Algebraic Data Type):
module Decoder = struct type _ t = | String : string t | Int : int t | Float : float t | Bool : bool t | Object : js_object t | List : 'a t -> 'a list t | Field : string * 'msg t -> 'msg t | Method : string * arg_value list * 'msg t -> 'msg t | Bind : ('a -> 'msg t) * 'a t -> 'msg t | Const : 'msg -> 'msg t | Fail : string -> 'msg t | Try : 'a t -> 'a option t | Factor : ('a -> 'msg t) -> ('a -> ('msg, string) Result.t) t end
Each value of type
'a Decoder.t represents a decoder that produces an OCaml value of type
These constructors serve as building blocks for creating more complex decoders.
The first six constructors are the base building blocks. They attempt to convert input objects to an OCaml value of a specific type and raise an error if the input is incompatible.
Decoder.string, which is just a wrapper around
Decoder.String, is a
Things become more interesting when we employ
Decoder.field "label" d is a decoder that accesses the field
.label and then applies the decoder
d. For example, to access the value of a text field
input in the DOM, equivalent to accessing
let module D = Vdom.Decoder in D.field "value" D.string
Nested fields can be accessed by nesting the constructor:
field "target" (field "value" string) corresponds to accessing
x.target.value. For convenience, we also support the shorthand
field "target.value" string.
The next constructor,
Decoder.method_, operates similarly, allowing you to call a method with a list of arguments and then apply a decoder to the result. While decoders are primarily designed for data retrieval rather than object manipulation, calling methods is needed in many practical scenarios. This is especially true when accessing pseudo-properties that are exposed as methods. For instance, within the DOM, one needs to invoke the
getBoundingRect() method in order to obtain the bounding rectangle of an element.
The remaining five constructors are decoder combinators, introducing a higher level of decoding power. The most notable are the monadic operators
bind combines decoders sequentially, passing the result of one decoder to another.
Decoder.try_ constructor is designed to handle decoder failures by returning
None when the nested decoder fails. The last two operators cater to more intricate use cases:
Decoder.fail is intended to consistently produce a failure, while
Decoder.factor offers a mechanism to delay the execution of a decoder.
A convenient way to use the
bind operator is with the
let* syntax, defined as
let ( let* ) d f = bind f d. Consider the following decoder:
let open Vdom.Decoder in let* value = field "value" string in let* inner_html = field "innerHTML" string in const (value, inner_html)
This decoder successively applies three decoders to the input:
field "value" string,
field "innerHTML" string, and
const (value, inner_html). The first two extract the value and inner HTML of a DOM element, and the last one returns a pair of both extracted strings.
In the Vdom library, decoders are primarily useful in crafting custom event handlers. While the Vdom library previously allowed developers to create handlers for various browser events, these handlers provided a predefined set of “relevant” data from the event. However, this approach had limitations, as it could not accommodate an ever-expanding list of potentially useful fields for every new type of event.
With decoders, users gain access to any field within the event object, including results of method calls. Moreover, only the necessary fields are extracted, reducing unnecessary overhead.
Creating an event handler with a decoder is accomplished using
Vdom.on. It requires a string specifying the event type and a
Event object. If it succeeds and does not return
None, the output is dispatched as a VDOM message.
As an example, here is how we can define an event handler that retrieves the list of files that a user drags and drops into a web application, and sends a VDOM message in response:
let files_decoder = let open Vdom.Decoder in let* files = field "dataTransfer.files" (list object_) in const (Some (Drop files)) in Vdom.on ~prevent_default:() "drop" files_decoder
For all other potential use cases, Vdom also offers a function to manually execute a decoder,
Ojs.t) as input, and returns a
Result.t value with either the result of the decoder or a string describing the part of the decoder that failed.