The library Fmlib_browser
is best installed via opam issuing the command
opam install fmlib_browser
In this section we write first a very simple application with a counter value and two buttons to decrease and increase the counter value. Here is the ocaml source file which implements the application.
(* file: counter.ml *)
open Fmlib_browser
(* The type of the state is [int]. Therefore no type declaration is
necessary for the state. *)
let type msg =
| Decrement
| Increment
let view (counter: int): msg Html.t =
let open Html in
let open Attribute in
div []
[ button
[color "blue"; on_click Decrement]
[text "-"]
; span
[background_color "silver"; font_size "20px"]
[text (string_of_int counter)]
; button
[color "blue"; on_click Increment]
[text "+"]
]
let update (counter: int): msg -> int = function
| Decrement ->
counter - 1
| Increment ->
counter + 1
let _ =
sandbox (* very simple applications are sandbox applications *)
0 (* initial state *)
view (* view function *)
update (* update function *)
The application in the file webapp.ml
has to be compiled to javascript. This is done with the help of js_of_ocaml
. The following dune file can be used:
(executable (name counter) (modes js) ; Activate compilation to javascript (libraries fmlib_browser) ; Use the library ) (rule (targets counter.js) ; Generate the file 'counter.js' (deps counter.bc.js) (mode (promote (until-clean))) (action (copy counter.bc.js counter.js)) )
The application is compiled via one of the commands
dune build ./counter.js dune build --profile release ./counter.js
The first one generates the file counter.js
with a lot of diagnostic information. The second one generates a much smaller file counter.js
with no diagnostic information.
Furthermore we need a html file.
<!-- file: index.html --> <!DOCTYPE html> <html> <head> <script type="text/javascript" src="counter.js"> </script> </head> <body> </body> </html>
The sandbox application installs itself as an event listener for the onload
event and creates and updates the dom directly below the body
element of the html document. Everything in the html file below the body element will be overwritten.
Now you have the files webapp.js
and index.html
in your source directory. By loading index.html
into the browser (either from a webserver or from disk), the browser loads the application webapp.js
and fires the onload
event which starts the application.
In order to demonstrate commands and subscriptions with a simple example we write an application displaying a digital clock. We want the webpage to look like
10:15:37
to represent the time of 10 o'clock and 15 minutes and 37 seconds. The current time has to be updated each second.
The state of the application contains the posix time and the current time zone.
(* file: clock.ml *)
open Fmlib_browser
type state = {
time: Time.t;
zone: Time.Zone.t;
}
The application has to be informed about the current time and time zone and has to receive a notification each second to update the time. The following message type contains the needed information.
type msg =
| Time of Time.t (* message for the current time *)
| Zone of Time.Zone.t (* message for the current time zone *)
We use a minimal view function to render the current time in a browser window.
let view (state: state): msg Html.t * string =
let open Html in
h2
[]
[text (
Printf.sprintf "%02d:%02d:%02d"
(Time.hour state.zone state.time)
(Time.minute state.zone state.time)
(Time.second state.zone state.time))
]
,
"Digital clock"
For a full blown application the view function returns the virtual dom and the title of the page.
The update function is quite straightforward.
let update (state: state): msg -> state * msg Command.t =
function
| Time time ->
{state with time}, Command.none
| Zone zone ->
{state with zone}, Command.none
We need a subscription function to get each second a notification of the new time.
let subscription (_: state): msg Subscription.t =
Subscription.every 1000 (fun time -> Time time)
We need a message Time time
each second independently from the system state. The time for repeating timers is expressed in milliseconds.
In order to start the application we need an initial state and an initial command.
initial_state: state =
{time = Time.zero; zone = Time.Zone.utc}
initial_command: msg Command =
Command.batch [
Command.perform Task.(map (fun time -> Time time) now)
;
Command.perform Task.(map (fun zone -> Zone zone) time_zone)
]
The task now
returns the current posix time and the task time_zone
returns the current time zone. However the application needs messages which can be fed into the update function. Therefore the results have to be mapped to messages. Unfortunately ocaml does not allow constructors to be used like functions, therefore two small anonymous function are needed to do the mapping.
Initially we set the time to zero and the time zone to utc. This is evidently not correct. Therefore we immediately start commands which get the current time and the current time zone.
The application is started by
let _ =
basic_application
initial_state
initial_command
view
subscription
update
The corresponding dune and html files are the same as for the counter example.