Generating HTML pages

Ocsigen provides several ways to generate and type HTML5 pages.

  • The default technique to produce HTML5 pages using Eliom are the Eliom_content.​Html5.​F, Eliom_content.​Html5.​D and Eliom_content.​Html5.​R modules. It is the only one supported for client-server Eliom programs. This module provides a typing based on OCaml's polymorphic variants, which ensures at compile time, that the pages you will generate will respect the recommendations of the W3C (or be very close).
  • It is also possible to use a syntax extension to write your pages with the usual HTML syntax. This solution is also typed with polymorphic variants and is compatible with the previous one.
  • You can also choose to generate untyped html as text.

The types in OCaml closest to XML types are polymorphic variants. Ocsigen uses them to provide a module with very good HTML5 typing. The full documentation is available in the TyXML documentation.

Generating HTML for Eliom applications

The TyXML library vs. the DOM API

On client side there are two kinds of HTML representations: one is based on the TyXML library and the other one is the browser DOM tree accessible through Js_of_ocaml modules Dom and Dom_html. The TyXML representation is a OCaml immutable typed tree. The DOM tree is mutable structure manipulated using the browser API which permit the modification of the displayed page. In the DOM represention adding a node as a child to an other node removes it from its previous ancessor.

Since those representation does not behave at all the same way, they are not used for the same thing.

  • It is far easier and safer to describe content using TyXML, but it is not possible to add a TyXML element to the page without explicit conversion to the DOM representation.
  • The TyXML representation has the same interface on client and server side. This allows share code between server and client.
  • Dom manipulation is heavy: to build some part of a tree, one needs to create each node separately then append them to their parents.

For example, here is a div element built with TyXML and then converted to the DOM representation using the module

open Eliom_content.Html5.D
let n = div ~a:[a_id "some div id"]
  [ pcdata "some text";
    br ();
    pcdata "some other text"; ]
let b = Eliom_client.Html5.of_div n

And here the same built using the DOM API:

open Dom
open Dom_html

let d = createDiv document in
let t1 = document##createTextNode( Js.string "some text" ) in
let t2 = document##createTextNode( Js.string "some other text" ) in
let b = createB document in
  appendChild d t1;
  appendChild d b;
  appendChild d t2;
  d##id <- (Js.string "some div id");
  d

To ease the DOM manipulation on the client, the usual DOM manipulation function are also available on TyXML elements. See section the next section for HTML5 element manipulation, by value and by reference.

HTML5 element manipulation, by value and by reference

There are four modules to create typed HTML: Eliom_content.Html5.F, Eliom_content.Html5.D, Eliom_content.Html5.R and Eliom_content.Html5.C. The last one is for reactive elements and is addressed in another section.

It is possible to mix the four kinds of nodes in the same page.

Elements built with Eliom_content.Html5.F are sent by value, while elements built with Eliom_content.Html5.D are sent to the client by reference. Eliom adds an identifier as attribute of D elements to make it possible to find them back in the page from client side.

Sending elements by reference allows easy manipulation of elements included in the initial html document from event handlers, as the input element in the following example.

let main_service =
  My_appl.register_service ~path:[""] ~get_params:Eliom_parameter.unit
    (fun () () ->
       let open Eliom_content.Html5.D in
       let input = input ~a:[a_input_type `Text] () in
       let onclick_handler =
	 {{ let v =
	      Js.to_string (Eliom_client.Html5.of_input %input)##value
	    in
	    Dom_html.window##alert(Js.string ("Input value :" ^ v)) }}
       in
       let button =
         button ~a:[a_onclick onclick_handler] [pcdata "Read value"]
       in
       Lwt.return
         (html
	    (head (title (pcdata "Test")) [])
            (body [input; button]) ) )

In this example, if the input button would have been incorrectly sent by value, two different input fields would have been created: one displayed in the document and one referenced from the event handler. The latter will always contains an empty value.

There are still two situations where sending elements by value is still required:

  • one want to have multiple occurences of the same elements in the document. Indeed, elements sent by reference follow the DOM semantics where an element have only one instance in current document. For example, the following list will contains a single element:

    let li = li [pcdata "Shared item"] in ul [li; li; li;] .
  • one have a large page with a lot elements. Handling elements by references add a small overhead while loading the page, around 50ms per 1000 elements on a not so fast computer.

In any case, it is possible to mix elements sent by references and elements sent by value in the same document.

By default, a reference on an element is only valid in the current HTTP request: hence, sending an element built with Eliom_content.Html5.D in two different pages will produce two distinct nodes. If you want to define a element reference that is preserved accross the different page of an application, you must explicitely name this element with the function Eliom_content.​Html5.​Id.​create_named_elt, that takes as parameters an element identifier and a non named element. Element identifiers are created with the function Eliom_content.​Html5.​Id.​new_elt_id. See also section Global elements of an application.

The module Eliom_content.​Html5.​Manip allows using the classical DOM manipulation functions (e.g. appendChild, addEventlistener, ...) directly on the identifier of an HTML5 elements.

Reactive DOM

Eliom_content.Html5.R allows one to insert time varying values into the DOM tree. It relies on React's signal 'a React.signal. More information about react can be found on the homepage.

When dealing with dynamic content, one usally ends up with a lot of imperative DOM manipulations: replacing, appending, removing DOM elements, updating attributes, etc. Html5.R hides most of those imperative DOM operations. Every time a signal changes, the corresponding DOM tree updates itself.

Usage on client side

To insert reactive DOM elements, just use module Html5.R instead of Html5.D or Html5.F for these elements. Html5.R makes also possible to define reactive attributes.

Use function Html5.R.node : 'a elt React.signal -> 'a elt to insert a reactive node in a page.

Example
{shared{
  open Eliom_lib
  open Eliom_content
  open Html5
  open F
}}

module Testnodes_app =
  Eliom_registration.App (
    struct
      let application_name = "testnodes"
    end)

{client{

open Eliom_content.Html5

let split s =
    let len = String.length s in
    let rec aux acc = function
      | 0 -> acc
      | n -> aux (s.[n - 1] :: acc) (pred n)
    in aux [] len

let value_signal, set_value = React.S.create "initial"
(* value_signal : string React.signal *)

let value_len = React.S.map String.length value_signal
(* value_len : int React.signal *)

let content_signal : Html5_types.div_content_fun elt React.signal =
  React.S.map (fun value ->  let l = split value in
                F.div (
                  List.map (fun c ->
                      F.p [F.pcdata (Printf.sprintf "%c" c) ]
                    ) l
                )
              ) value_signal

let html_value_signal : [ `PCDATA ] R.elt list React.signal
  = React.S.map (fun s -> [pcdata s]) value_signal

let make_color len =
  let d = (len * 10) mod 255 in
  Printf.sprintf "color: rgb(%d,%d,%d)" d d d

let make_client_nodes () =
  [
    R.p html_value_signal;
    D.p ~a:[ R.a_style (React.S.map make_color value_len)]
      [R.pcdata value_signal];
    R.node content_signal
  ]


 }}


let make_input () =
  let inp = D.Raw.input ~a:[a_input_type `Text] () in
  let _ = {unit{ Lwt_js_events.(async (fun () ->
      let inp = To_dom.of_input %inp in
      keyups inp (fun _ _ ->
          let s = Js.to_string (inp##value) in
          set_value s;
          Lwt.return ()))) }}
  in
  inp

let main_service =
  Eliom_service.App.service ~path:[] ~get_params:Eliom_parameter.unit ()

let () =
  Testnodes_app.register
    ~service:main_service
    (fun () () ->
       let inp = make_input () in
       let cldiv = D.div [] in
       ignore {unit{ Manip.appendChildren %cldiv (make_client_nodes ()) }};
       Lwt.return
         (Eliom_tools.F.html
            ~title:"testnodes"
            ~css:[["css"; "testnodes.css"]]
            (body [F.h1 [pcdata "Reactive DOM"];
                   inp;
                   F.h2 [pcdata "Client side reactive nodes:"];
                   cldiv;
                   ])
         ))

Dom & Client-values

Eliom_content.Html5.C allows one to insert client-side content into server-side HTML5-trees. This makes possible, for example, to insert reactive nodes in a server-side generated page.

Example

{shared{
  open Eliom_lib
  open Eliom_content
  open Html5
  open F
}}

module Testnodes_app =
  Eliom_registration.App (
    struct
      let application_name = "testnodes"
    end)

let main_service =
  Eliom_service.App.service ~path:[] ~get_params:Eliom_parameter.unit ()

{client{
open Eliom_content.Html5
let value_signal, set_value = React.S.create "initial"
(* value_signal : string React.signal *)
}}

let client_reactive_attrib () = {{  R.a_style value_signal }}

let client_reactive_title () = {{ F.h1 [pcdata "I'm a client node"]; }}

let () =
  Testnodes_app.register
    ~service:main_service
    (fun () () ->
       Lwt.return
         (Eliom_tools.F.html
            ~title:"testnodes"
            ~css:[["css"; "testnodes.css"]]
            (body [
                    F.div ~a:[C.attr (client_reactive_attrib ())] [
                      C.node (client_reactive_title ());
                  ]
         ))

Module C is also available on client-side, to make it possible to use it in shared sections.

Global elements of an application

Sometimes you may want to modify the content of an HTML element and to keep the element and its modified content when changing page. For example a div element which contains a chat box or a music player should be preserved while browsing across the different pages of your site. For this purpose, Eliom provides a notion of global element. Such elements are instantied only once for an application and that unique instance is used in every page that references the element.

To create a global element, use function Eliom_content.Html5.Id.create_global_elt.

val create_global_elt: 'a elt -> 'a elt

In the following example, the content of global_list will be preserved when you click on the "reload page" link.

{shared{
open Eliom_content.Html5.D
}}

let global_list = create_global_elt (ul [])
let cpt = ref 0

let main_service =
  Eliom_service.App.service
    ~path:[""] ~get_params:Eliom_parameter.unit
    ()

let reload_link =
  a ~service:main_service [pcdata "reload page"] ()

let _ =
  My_appl.register ~service:main_service
    (fun () () ->
       let page_number = incr cpt; string_of_int !cpt in
       let append_item =
	 {{ let item_text = "item inserted in page #" ^ %page_number in
	    let item = Eliom_client.Html5.of_li (li [pcdata item_text]) in
	    Dom.appendChild (Eliom_client.Html5.of_ul %global_list) item }}
       in
       let append_link =
         a ~a:[a_onclick append_item] [pcdata "append item"]
       in
       Lwt.return
         (html
	    (head (title (pcdata "Test")) [])
            (body [h1 [pcdata ("Page #" ^ page_number)];
	           p [append_link];
		   p [reload_link];
		   global_list]) ) )

Another use of global element is for external javascript that should be included in every page but must be executed only once in an application. In the following code snippet, the alert "global script" is displayed only once, while the alert "non global script" is display every time you click on the "reload page" link.

open Eliom_content.Html5.D

let global_script =
  create_global_elt
    (script (cdata_script "alert(\"global script\")"))
let simple_script =
     script (cdata_script "alert(\"non global script\")")

let main_service =
  Eliom_service.App.service
     ~path:[] ~get_params:Eliom_parameter.unit ()

let reload_link =
  a ~service:main_service [pcdata "reload page"] ()

let _ = My_appl.register ~service:main_service
  (fun () () ->
    Lwt.return
      (html
         (head
	   (title (pcdata "Global script example"))
	   [ global_script;
	     simple_script ])
         (body
	   [ p [reload_link] ])))

HTML syntax extension

Ocsigen also has a syntax extension for OCaml that allows you to write pages using HTML syntax (but you are free not to use it). This is convenient for example if you want to include (parts of) HTML pages that have been created by third party. To choose actual XML-implementation you have to provide a module named Html5 (or Svg respectively):

For example, the following code:

module Html5 = Html5.M (* NB this is TyXML's Html5; could also be Eliom_content.Html5.F *)

<< <html>
     <head><title></title></head>
     <body><h1>plop</h1></body>
   </html> >>

is a caml value of type Html5_types.html Html5.M.elt.

To compile a module containing this syntax, you need the camlp4 preprocessor:

ocamlc -I /path_to/ocsigen/
 -pp "camlp4o /path_to/ocsigen/xhtmlsyntax.cma -loc loc"
 -c your_module.ml

You can insert OCaml expressions of type 'a Html5.M.elt inside html using $...$, like this:

let oc = << <em>Ocsigen</em>

in

You can insert OCaml expressions of type string inside html using $str:... $, like this:

let i = 4 in
<< <p>i is equal to $str:string_of_int i$</p> >>

If you want to use a dollar in your page, just write it twice.

You can write a list of HTML5 expressions using the syntax <:xmllist<...>>, for example:

<:html5list< <p>hello</p> <div></div> >>

Here are some other examples showing what you can do:

<< <ul class=$ulclass$ $list:other_attrs$>
     $first_il$
     $list:items$
   </ul> >>

Warning: lists antiquotations are allowed only at the end (before a closing tag). For example, the following is not valid:

<< <ul $list:other_attrs$ class=$ulclass$>
     $list:items$
     $last_il$
   </ul> >>

The syntax extension is not allowed in patterns for now.

Warning: The two syntaxes are not equivalent for typing. Using the syntax extension will do less checking. For example the following code is accepted but not valid regarding HTML5 standard (because <head> must contain a title):

<< <html>
     <head></head>
     <body><h1>plop</h1></body>
   </html> >>

We recommend you use the functions from Eliom_content.​Html5.​D, as you will (almost) always get valid HTML5. Use the syntax extension for example to enclose already created pieces of HTML, and check your pages validity with the W3C validator.

Text HTML

The last possibility is to use untyped HTML. Just build strings containing your pages. Here is an example:

let coucoutext =
  Eliom_registration.Html_text.register_service
    ~path:["coucoutext"]
    ~get_params:Eliom_parameter.unit
    (fun () () ->
      Lwt.return
        ("<html>n'importe quoi "^
         (Eliom_content.Html_text.a coucou "clic" ())^
         "</html>"))

Writing HTML as text makes applications much more difficult to maintain. We do not recommend this.

Custom data for HTML5

Eliom provides a type-safe interface for using HTML5's custom data, Eliom_content.​Html5.​Custom_data.

Creation

Custom data may be created either from string-conversation functions by Eliom_content.​Html5.​Custom_data.​create

open Eliom_content
let my_int_data =
  Html5.Custom_data.create ~name:"my_int"
    ~of_string:int_of_string
    ~to_string:string_of_int ()

or by a Json-deriving type Eliom_content.​Html5.​Custom_data.​create_json

{shared{
  type coord = { x : int; y : int; } deriving (Json)
  let coord_data =
    Html5.Custom_data.create_json ~name:"coord" Json.t<coord>
}}

Injecting

Custom data can be injected into HTML5-trees of type Eliom_content.​Html5.​elt by the function Eliom_content.​Html5.​Custom_data.​attrib:

div ~a:[Html5.Custom_data.attrib coord_data {x = 100; y = 200}] []

NB, HTML5 gives no restriction on the usage of custom data, any custom data can may be added to any HTML5 element.

Reading/writing the DOM

On the client side, custom data can be read from and written to JavaScript DOM elements of type Dom_html.​element.

Custom data can be read from a DOM-element with the function Eliom_content.​Html5.​Custom_data.​get_dom. If no respective custom data attribute can be found in the element

  • the default value from creating the custom data is returned, if any, or
  • an exception Not_found is raised, otherwise.

The custom data of a DOM-element can be set with the function Eliom_content.​Html5.​Custom_data.​set_dom.

{client{
    fun (div : Dom_html.element Js.t) ->
      let i = Html5.Custom_data.get_dom div coord_data in
      debug "{x=%d; y=%d}" i.x i.y;
      Html5.Custom_data.set_dom div coord_data { x = i.x + 1; y = i.y - 1 }
  }}

Default value

If a custom data is created with the optional argument default, calls to Eliom_content.​Html5.​Custom_data.​get_dom return that instead of throwing an exception [Not_found].

let coord_data' =
    Html5.Custom_data.create_json
       ~name:"coord" default:{x=0;y=0;} Json.t<my_data>