What's new in Eliom 2.1

Dom manipulation and event handlers

Rationale

The HTML representation used by Eliom on the server-side is a functional tree (a directed acyclic graph). It is created using functions from the module Eliom_pervasives.​HTML5.​M (based on the TyXML library). On the client side, the browser representation of HTML is an imperative tree, the DOM. The semantics of the two kinds of trees differ in multiple ways:

While sending to the client a TyXML tree built by an Eliom service, we have to choose what to do with shared nodes: should we duplicate them or should we keep only one instance of each ?

A second question to solve is, when sending the same TyXML tree twice to a client in two successive requests, should we have one or two representations of the tree on the client ? The answer to this question is important when the content of the tree has been modified by the client.

Eliom provides the programmers the ability to choose the answer to those questions. First, classical TyXML nodes will always be duplicated when sent to the client. Secondly, we have introduced a new kind of TyXML nodes for which Eliom ensures that there will be a unique instance in a client. We call those nodes: unique nodes or nodes sent by references or nodes with Dom semantics. Within unique nodes we distinguish two categories:

The latter categories has been introduced in the release 2.1 of Eliom and we use them as default representation for nodes in the manual and the tutorial. In every case, it is possible to mix different kind of nodes in the same tree.

Referencing nodes from event handlers

To create simple unique nodes, we introduced in Eliom-2.1 a new module Eliom_pervasives.​HTML.​DOM with the same interface as Eliom_pervasives.​HTML.​M, but whose functions returns simple unique node instead of classical functional nodes.

To create global unique nodes, you may use the function HTML.​create_global_node that takes a classical node as parameter.

When you want to access a node sent in the body of a page in an event handler, the node must be unique. For instance, in the following code the input is a simple unique node.

module MyAppl = Eliom_output.Eliom_appl(struct
  let application_name = "unique_nodes"
end)

let field = HTML5.DOM.input ~a:[HTML5.a_input_type `Text] ()

let show_field = {{
  let value = Js.to_string ((Eliom_client.Html5.of_input %input)##value) in
  Dom_html.window##alert (Js.string)
}}
let show_button =
  HTML5.M.button ~a:[HTML5.a_onclick show_field] [HTML5.M.pcdata "Show"]

let main_page =
  let open HTML5.M in
  (html
    (head (title (pcdata "Main page")) [])
    (body [field; show_button]))

let main =
  MyAppl.register_service
    ~path:[]
    ~get_params:Eliom_parameters.unit
    (fun () () -> Lwt.return main_page)

If the input node in the example was a classical node, the conversion function Eliom_client.​Html5.​of_input would have generated a new DOM input node and its value property would have been empty. Hence, it is a classical mistake to forget to create a node as unique and then use it in an imperative way. However the new simple unique node introduced in Eliom 2.1 are cheap and we advise you to use the simple unique nodes by default.

Another way to create global unique node is to explicitly create a new abstract node identifier with the function Eliom_pervasives.​HTML5.​new_elt_id and then to name a node with this identifier by applying the function Eliom_pervasives.​HTML5.​create_named_elt.

Nodes modifications

Eliom 2.1 introduces a new module Eliom_dom that allows modification of unique nodes on the client-side. For instance, consider the following piece of code in Eliom-2.0:

let content = HTML5.M.unique (HTML5.M.div [])
let button =
  HTML5.M.button
    ~a:[ HTML5.M.a_onclick {{
           let data = HTML5.M.p (HTML5.M.pcdata "Data...") in
           Dom_html.appendChild
             (Eliom_client.Html5.of_element %content)
             (Eliom_client.Html5.of_element data)
         }} ]
    [pcdata "Click me..."]

With Eliom 2.1, it could be shortens in:

let content = HTML5.DOM.div []
let button =
  HTML5.M.button
    ~a:[ HTML5.a_onclick {{
           let data = HTML5.M.p (HTML5.M.pcdata "Data...") in
           Eliom_dom.appendChild %content data
         }} ]
    [pcdata "Click me..."]

Full example

(* This is a small example showing the changes that were made in Eliom
   2.1.

   This example demonstrate how to easily do delayed loading of parts
   of the page. For that we define a function that sets the content of
   a node with the answer of a service.

   We define two versions of that function, one for request nodes
   (created using the functions from the module HTML5), and one on
   global nodes (in fact on ids of global nodes)

   The differences are that the one on request node, since the a new
   node is created each time the page change, is content must be
   reloaded each time, but on global nodes, the content is kept. *)

open Eliom_parameters
open HTML5
open Eliom_output.Html5

(* Helpers *)
let sprintf f = Printf.ksprintf pcdata f
let static s = make_uri ~service:(Eliom_services.static_dir ()) s

(* Abbreviation for class name of the "bootstrap" css *)
module Bootstrap = struct
  let navbar = a_class ["navbar"]
  let navbar_fixed_top = a_class ["navbar-fixed-top"]
  let nav = a_class ["nav"]
  let container = a_class ["container"]
  let navbar_inner = a_class ["navbar-inner"]
  let brand = a_class ["brand"]
  let hero_unit = a_class ["hero-unit"]
end

(* Creating the application *)
module Demo =
  Eliom_output.Eliom_appl(struct let application_name = "demo" end)

(* Creating services *)

(* Services for main pages *)
let demo_delayed_load =
  Eliom_services.service ["demo_delayed_load"] unit ()
let demo_delayed_load_2 =
  Eliom_services.service ["demo_delayed_load_2"] unit ()
let demo_delayed_load_3 =
  Eliom_services.service ["demo_delayed_load_3"] unit ()

(* Service for the delayed content. *)
let delayed_loading_service =
  Eliom_services.service ["delayed_load"] (int "n") ()

(* Service representing a static image
   (displayed while waiting for delayed content). *)
let loading_image = img (static ["img";"loader.gif"]) "loading" ()

(** The function [delayed_load service get_param post_param elt] does
    a request to [service] when the page is loaded (with parameters
    [get_param] and [post_param]) and set the content of [elt] with
    the result of the request. *)
let delayed_load service get_param post_param elt =

  Eliom_services.onload {{
    ignore (
      lwt content =
        Eliom_client.call_caml_service %service %get_param %post_param
      in

      (* The functions from the new Eliom_dom module allows to modify
         directly the nodes without casting them to Js_of_ocaml's Dom
         types (a.k.a. [Dom_html.element Js.t]). *)
      Eliom_dom.replaceAllChild %elt content;

      Lwt.return ())
  }};
  elt

(** The function [delayed_onload service get_param post_param id] is
    a variant of [delayed_load] to be used with global nodes. Instead
    of adding a onload event handler to a document, it returns an
    onload event handler to be set as attribute on the node referenced
    by [id]. *)
let delayed_onload service get_param post_param id =
  a_onload {{
    ignore (
      lwt content =
        Eliom_client.call_caml_service %service %get_param %post_param
      in
      Eliom_dom.Named.replaceAllChild %id content;
      Lwt.return ())
  }}

(* Handler for the delayed content service. If n = 0 we return a
   simple page. Otherwise, we return a page with more delayed stuff. *)
let () = Eliom_output.Caml.register delayed_loading_service
  (fun n () ->
    let elt =
      if n <= 0
      then ul [ li [ pcdata "no more level" ]]
      else delayed_load delayed_loading_service (n-1) ()
        (ul [ li [ loading_image; sprintf "loading level %i" n ]]) in

    (* wait to simulate some long computation *)
    lwt () = Lwt_unix.sleep 1. in

    Lwt.return [ li [ sprintf "finished loading level %i" n;
                      elt ]])

(* Helper function to build the main page. We put a lot of things
   in the page to have a nice render, but the only important thing
   is the [content] node. *)
let make_page page_title content =
  Lwt.return
    (html

       (head (title (pcdata page_title))
          [css_link (static ["css";"style.css"]) ()])

       (body [
         div ~a:[Bootstrap.navbar; Bootstrap.navbar_fixed_top] [
           div ~a:[Bootstrap.navbar_inner] [
             div ~a:[Bootstrap.container] [
               a ~a:[Bootstrap.brand]
                 ~service:Eliom_services.void_coservice'
                 ~fragment:"" [pcdata page_title] ();
               ul ~a:[Bootstrap.nav] [
                 li [a demo_delayed_load [pcdata "request elements"] ()];
                 li [a demo_delayed_load_2 [pcdata "global elements"] ()];
                 li [a demo_delayed_load_3 [pcdata "global elements 2"] ()]]]]];

         div ~a:[Bootstrap.container] [
           div ~a:[Bootstrap.hero_unit] [
             h1 [ pcdata "Delayed loading demo" ]];
           content]]))

(* This is the request node on which we will add the loading
   stuff. Each time the page is displayed a new version of that node
   is sent and replace the old one. That means clicking on the link to
   the [demo_delayed_load] service will relaunch the delayed
   loading. *)
let request_node = ul [ li [ loading_image ] ]

let () =
  Demo.register demo_delayed_load
    (fun () () ->
      make_page "Page with request node"
        (delayed_load delayed_loading_service 3 () request_node))

(* This is a single global node shared by two pages. Moving from one
   page to the other won't relaunch the loading (even if this node is
   not displayed in some page, for instance when going to the
   [demo_delayed_load] service) *)
let global_loading_node =
  let content_id = new_elt_id () in
  create_named_elt ~id:content_id
    (ul ~a:[delayed_onload delayed_loading_service 3 () content_id] [
      li [loading_image] ])

let () =
  Demo.register demo_delayed_load_2
    (fun () () ->
      make_page "Page with global node"
        (div [global_loading_node]))

let () =
  Demo.register demo_delayed_load_3
    (fun () () ->
      make_page "Page with global node 2"
        (div [
          h2 [pcdata "Almost the same page, sharing the same global node" ];
          global_loading_node]))

Others novelties

See also the full Changelog