Try   HackMD

An Introduction to js_of_ocaml

If you want to write web apps using OCaml, then there's a good chance you'll want to use js_of_ocaml. js_of_ocaml - which will be abbreviated as "jsoo" from here on out - is a compiler that turns OCaml sourcecode into Javascript. From the source README:

  • It is easy to install and use as it works with an existing installation of OCaml, with no need to recompile any library.
  • It comes with bindings for a large part of the browser APIs.
  • According to our benchmarks, the generated programs runs typically faster than with the OCaml bytecode interpreter.
  • We believe this compiler will prove much easier to maintain than a retargeted OCaml compiler, as the bytecode provides a very stable API.

This is a tutorial for getting started with jsoo by building a small interactive animation in the browser:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

We're going to need some way to build our project, and this tutorial was written with the popular OCaml build tool dune in mind. Though, instructions for compiling with another build tool ocamlc can be found in the jsoo manual. If you're new to dune you can read more about it here, along with the instructions for compiling to JavaScript.

Our final product is an interactive animation that renders in a browser window like so:

All the code in this post can be found here.

Note: Please give me feedback! If I've missed something important, phrased something poorly, or said something incorrect, comment and let me know!

Setup

To start, we're going to install some things from the jsoo library:

opam install js_of_ocaml js_of_ocaml-ppx js_of_ocaml-lwt

Our file structure is simple here:

JSOO_INTRO/ 
    |------ _build
    |------ main.ml
    |------ index.html
    |------ dune

The _build file will be generated once we build our program.

We can leave our main.ml file blank for now. But we're going to use our index.html file to run the script we generate with jsoo:

<html> <head> <title>Jsoo_intro</title> <script type="text/javascript" src="_build/default/main.bc.js"></script> </head> <body> </body> </html>

This code from line 4: src="_build/default/main.bc.js" is to tell our html file where to get our javascript from. In the case of my build path, main.bc.js lands here. Make sure to substitute your own path if you're not using dune.

Our dune file looks like the following:

(executable (name main) (modes js) (preprocess (pps js_of_ocaml-ppx)) (libraries js_of_ocaml js_of_ocaml-lwt js_of_ocaml-lwt.graphics))

To make sure everything is working run dune build ./main.bc.js

Note: A .bc.js file functions identically to a .js file. The jsoo build path first turns OCaml source code into byte code (.bc), then the jsoo compiler turns that bytecode into javascript (js). Hence, the final product, (.bc.js) signals that the javascript was compiled from bytecode.

The DOM

Before we can use jsoo, we need to know a bit about the DOM. The DOM is an interface between web documents and programming languages. It represents documents as a tree structure, and is how we're going to interact with our html page. To learn more about the DOM, check out this tutorial.

To interact with objects in the DOM using OCaml, we use the following syntax:
Accessing methods on objects is done via ## e.g. object##method
Accessing properties of objects is done via ##. e.g. object##.property
Setting the properties of objects is done via := e.g. object##.property := newValue

Example 1. Basic Display

We can start by writing a program which displays "Hello World" in our browser.

In main.ml we start with some boilerplate by aliasing useful modules. Next, because string representations in OCaml and JS are different, we make a helper function js_str which converts OCaml strings into JS strings. Finally, we create a reference for our html document.

module Html = Js_of_ocaml.Dom_html module Dom = Js_of_ocaml.Dom module Js = Js_of_ocaml.Js module G = Graphics_js let js_str = Js.string let doc = Html.document

We then make a function which creates a 'canvas'. An html canvas, much like a painter's canvas, is what we paint on:

let canvas_width = 300. let canvas_height = 150. let create_canvas () = let r = Html.createCanvas doc in r##.width := int_of_float canvas_width; r##.height := int_of_float canvas_height; r

Next we make a function for drawing our graphics:

let draw_things context = context##strokeRect 0. 0. canvas_width canvas_height; context##.font := js "50px serif"; context##fillText (js "Hello World") 20. 90.

Now we make our onload function which pieces things together.
Note that in line 5, we need to get the 2D context of our canvas in order to draw on it:

let onload _ = let canvas = create_canvas () in G.open_canvas canvas; Dom.appendChild doc##.body canvas; let c = canvas##getContext Html._2d_ in draw_things c; Js._true

Finally, we initialize our program:

let _ = Html.window##.onload := Html.handler onload

Compile our file again with dune build ./main.bc.js and view see our changes:
Ta Da!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Example 2. Dynamic Updates

Now, let's make our program more dynamic by building an incrementing timer using lwt.

There are a few changes we need to make. First we include a counter argument for our function. Then we write our counter to the canvas. Finally, we include an Lwt binding at the end of our draw_things function. This allows our function to 'sleep' for a second before 'waking up' and calling itself, incrementing the timer.

let rec draw_things c counter = let open Lwt.Syntax in c##.font := js "50px serif"; c##fillText (js "Hello World") 20. 90.; c##strokeRect 0. 0. canvas_width canvas_height; c##.font := js "20px serif"; c##fillText (js ("This page has been open for:")) 20. 110.; c##fillText (js ((string_of_int counter) ^ " seconds")) 20. 130.; let* () = (Js_of_ocaml_lwt.Lwt_js.sleep 1.0 in c##clearRect 0. 0. canvas_width canvas_height; draw_things c (counter + 1)

Our onload function also needs one small change. We pipe draw_things to an ignore like so on line 6:

let onload _ = let canvas = create_canvas () in G.open_canvas canvas; Dom.appendChild doc##.body canvas; let c = canvas##getContext Html._2d_ in draw_things c 0 |> ignore; Js._false

Recompile with dune build, and we have a dynamically updating web page!

Example 3. Adding Interactivity

Finally, we'll add a button to our page which resets the counter when we push it.

We start by defining a generic button:

let button name callback = let res = doc##createDocumentFragment in let input = Html.createInput ~_type:(js "button") doc in input##.value := js name; input##.onclick := Html.handler callback; Dom.appendChild res input; res

Note how on line 3, our input has type "button". This means our rendered input will be a button like so:

We could change this to a checkbox, text, url, or many other input types. More information on inputs can be found here.

In order to utilize this button, we'll need to change our onload function like so:

let onload _ = let main = Js.Opt.get (doc##getElementById (js "main")) (fun () -> assert false) in let canvas = create_canvas () in let c = canvas##getContext Html._2d_ in let promise = ref @@ draw_things c 0 in G.open_canvas canvas; Dom.appendChild doc##.body canvas;

Note that on line 6 we create start our animation, and capture the resulting promise in a reference variable.

We can now create our reset button:

Js_of_ocaml.Dom.appendChild main (button "Reset" (fun _ -> let div = Html.createDiv doc in Js_of_ocaml.Dom.appendChild main div; Lwt.cancel !promise; c##clearRect 0. 0. canvas_width canvas_height; promise := draw_things c 0; Js._false)); Js._false

On line 6 our button cancels the promise we captured in our reference variable, this stops the incrementing timer and allows us to clear our canvas. We then start a new animation, and capture the resulting promise in our original reference variable, that way we can reuse it whenever we press the reset button.

Recompile and we get the following:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

You now have the information and resources to start working with Js of OCaml on your own!

Further Reading and Practice

Here's a link to the Jsoo documentation.

More generally, if you want to know what's in a library or module, look in the Ocaml Documentation Hub.

If you like problem sets, try following this tutorial on the canvas api and replicating the examples using Ocaml.

If you want to work with something larger, try understanding these jsoo examples and modifying them.

As an example of a full project here's 2048 built with OCaml.

Jane Street builds web apps with a library called Incr_Dom. You can watch a presentation on the library here.

Appendix: Viewing Your Work

When working with javascript and html documents files, you're going to want some way to see the changes you've made in the browser. For simple programs you can locate your html file and open it in a web browser, then refresh the browser when you want to see changes made. Getting an automatic refresh is doable, but the setup is different depending on what editor you're using:

Visual Studio Code:

Emacs:

Vim & Others: