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:
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!
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.
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
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!
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!
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:
You now have the information and resources to start working with Js of OCaml on your own!
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.
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: