I’m interested to learn more about the Ocsigen stack.
For this reason, I’d also like to learn and understand React / Erratique, in a JSOO context.
Here’s my dune file:
(executable
(name simple_counter)
(libraries
js_of_ocaml-ppx
lwt
lwt_ppx
js_of_ocaml-lwt
tyxml
js_of_ocaml-tyxml
react
reactiveData)
(preprocess
(pps js_of_ocaml-ppx lwt_ppx))
(modes js))
I’m trying to implement a simple incrementing counter. The following code compiles, however I’m not triggering a new signal on button click.
(*
* MODEL
*)
type model = int
type rs = model React.signal
type rf = ?step:React.step -> model -> unit
type rp = rs * rf
let init : model = 0
(*
* UPDATE
*)
type action = Click | NOOP [@@warning "-37"]
let update action ((r, f) : rp) =
let current_model = React.S.value r in
let new_model =
match action with
| NOOP -> current_model
| Click -> current_model + 1
in
f new_model
;;
(*
* VIEW
*)
let view_btn ((r, _f) : rp) =
let model = React.S.value r in
Js_of_ocaml_tyxml.Tyxml_js.Html5.(button [ txt (string_of_int model) ])
;;
let view_btn_dom (rp : rp) =
Js_of_ocaml_tyxml.Tyxml_js.To_dom.of_button (view_btn rp)
;;
let view ((r, f) : rp) =
Js_of_ocaml_lwt.Lwt_js_events.(
async (fun () ->
clicks
(view_btn_dom (r, f))
(fun _evt _ -> Lwt.return @@ update Click (r, f))))
; Js_of_ocaml_tyxml.Tyxml_js.To_dom.of_button @@ view_btn (r, f)
;;
let main (_ : Js_of_ocaml.Dom_html.event Js_of_ocaml.Js.t) =
let document = Js_of_ocaml.Dom_html.document in
Js_of_ocaml.Dom.appendChild document##.body (view @@ React.S.create init)
; Lwt.return ()
;;
let _ =
let%lwt evt = Js_of_ocaml_lwt.Lwt_js_events.onload () in
Lwt.return @@ main evt
;;
Any pointers would be much appreciated
Baby step n°2: update a clock within a web UI, a simpler example.
Inspired by the documented example which outputs the current time to a terminal.
I think I’m getting the hang of this. Posting here for reference, I have yet to get back to my original difficulty.
(executable
(name main)
(libraries js_of_ocaml react unix)
(preprocess
(pps js_of_ocaml-ppx))
(modes js))
Type annotations are for clarity:
open Js_of_ocaml
let (seconds_stream : float React.event), (start_clock : unit -> unit) =
let evt, send = React.E.create () in
let do_send () =
send (Unix.gettimeofday ())
; print_endline "time changed"
in
let do_schedule () =
Dom_html.window##setInterval (Js.wrap_callback do_send) 1000.0
|> ignore (*interval ID*)
in
(evt, do_schedule)
;;
let create_button () =
let button = Dom_html.(createButton document) in
let div = Dom_html.(createDiv document) in
Dom.appendChild div button
; Dom.appendChild Dom_html.document##.body div
; button
;;
let button = create_button ()
let update_btn (t : float) : unit =
let h, m, s =
let tm = Unix.localtime t in
(tm.Unix.tm_hour, tm.Unix.tm_min, tm.Unix.tm_sec)
in
let tm_s = Printf.sprintf "%02d:%02d:%02d" h m s in
button##.innerText := Js.string tm_s
;;
let start_update_btn () =
let (_ : unit React.event) = React.E.map update_btn seconds_stream in
()
;;
let () =
start_update_btn ()
; start_clock ()
;;
I’m interested in finding simple examples like this. I haven’t found much so far.
There’s a todomvc with note here. This gesture recognizer uses react you can find the source at the end of the blog post.
Thanks for the heads up! I’ll be sure to dig into those examples, once I fully grokked the basics.
So I managed to get my baby step #1 going:
open Js_of_ocaml
let (counter, change_counter) = React.S.create 0
let create_ui () =
let body = Dom_html.document##.body in
let div = Dom_html.(createDiv document) in
let h1 = Dom_html.(createH1 document) in
let input = Dom_html.(createInput document) in
let btn = Dom_html.(createButton document) in
()
; h1##.innerText := Js.string "Counter example"
; btn##.innerText := Js.string "count"
; input##.disabled := Js.bool true
; Dom.appendChild div h1
; Dom.appendChild div input
; Dom.appendChild div btn
; Dom.appendChild body div
; (input, btn)
;;
let (input, btn) = create_ui ()
let start_listen_click () =
btn##.onclick :=
Dom_html.handler (fun _ ->
let curr = React.S.value counter in
change_counter (curr + 1)
; Js.bool false)
;;
let update_input n = input##.value := Js.string (string_of_int n)
let start_update_input () = React.S.map update_input counter |> ignore
let () =
start_update_input ()
; start_listen_click ()
;;
Based on those definitions:
An event is a value with discrete occurrences over time.
A signal is a value that varies continuously over time. In contrast to events which occur at specific point in time, a signal has a value at every point in time.
I wanted to use an event to describe the mouse clicks, but to make things work I had to resort to using a signal instead.
To me, it sounds like the mouse position should be a signal, but a mouse click should be an event.
I’ll keep digging…
If you want to inspire yourself there’s quite a bit of code that interfaces with DOM events in Useri / Erratique or Brr_note_kit (brr.Brr_note_kit)
Thanks
Brr is on my todo list.
It looks like you initially intended to release useri
but then decided not to?
$ opam install js_of_ocaml useri
[ERROR] No package named useri found.
My main motivation for looking into React results from having seen the possibility to share a signal between the client and server, with type safety
So now, I want to dig in and understand the details. The fact that I can see that React may be applied to other domains than web makes it even more so interesting.
Here’s a baby step #3, in a more declarative style this time, using ReactiveData which uses React under the hood.
Js_of_ocaml_tyxml
provides the ability to define “reactive nodes” via its module Tyxml_js.R.Html
.
Adapted from one of the examples available at: OCaml toplevel
open Js_of_ocaml
open Js_of_ocaml_lwt
open Js_of_ocaml_tyxml
module RList = ReactiveData.RList
(*
* REACTIVE DATA
*)
let (r_lst, r_handle) = RList.create []
let cons s = RList.cons s r_handle
(* snoc is the inverse of cons *)
let snoc s = RList.snoc s r_handle
let time_signal =
let (s, set) = React.S.create (Sys.time ()) in
let rec loop () : unit Lwt.t =
let t = Sys.time () in
()
; set t
; if t < 10.0 then snoc "Will add every 1s (until uptime=10s)"
; Lwt.bind (Lwt_js.sleep 1.) loop
in
Lwt.async loop
; s
;;
(*
* REACTIVE VIEW
*)
let r_li = RList.map (fun x -> Tyxml_js.Html.(li [ txt x ])) r_lst
let r_ul = Tyxml_js.R.Html.ul r_li
let r_view =
Tyxml_js.(
Html.(
div
[ h1
[ txt "Uptime is "
; R.Html.txt
(React.S.map (fun s -> Printf.sprintf "%.0fs" s) time_signal)
]
; r_ul
]))
;;
(*
* BOOTSTRAP
*)
let append_to_body elt =
Dom.appendChild Dom_html.document##.body (Tyxml_js.To_dom.of_element elt)
;;
(*
This program outputs something like this:
Uptime is 99s
- Item 3
- Item 2
- Item 1
- Will add every 1s (until uptime=10s)
- Item 4
- Will add every 1s (until uptime=10s)
- Will add every 1s (until uptime=10s)
- Will add every 1s (until uptime=10s)
- Will add every 1s (until uptime=10s)
- Will add every 1s (until uptime=10s)
- Will add every 1s (until uptime=10s)
- Will add every 1s (until uptime=10s)
- Will add every 1s (until uptime=10s)
- Will add every 1s (until uptime=10s)
*)
let () =
()
; cons "Item 1"
; cons "Item 2"
; cons "Item 3"
; snoc "Item 4"
; append_to_body r_view
;;
Okay it’s finally clicking in for me.
As a last baby step, here’s a declarative style version of what I was trying to do at the opening of my post. My original attempt looks so cute in comparaison
Pretty fun!
open Js_of_ocaml
open Js_of_ocaml_tyxml
(*
* SETUP
*)
let (counter, set_counter) = React.S.create 0
(*
* REACTIVE VIEW
*)
let r_input =
(* The input's background turns increasingly red, as the button is clicked. *)
let bg_towards_red c =
let bg n =
let n =
if n < 0 then
0
else
n
in
Printf.sprintf "background-color: rgb(255, %d, %d)" n n
in
bg (255 - (c * 10))
in
Tyxml_js.R.Html.(
input
~a:
[ a_style @@ React.S.map bg_towards_red counter
; a_disabled ()
; a_value @@ React.S.map string_of_int counter
])
()
;;
let button' =
Tyxml_js.Html.(
button
~a:
[ a_onclick (fun _ ->
let curr = React.S.value counter in
set_counter (curr + 1)
; false)
]
[ txt "counter" ])
;;
let r_view =
Tyxml_js.(
Html.(div [ h1 [ txt "Counter example" ]; div [ r_input; button' ] ]))
;;
(*
* BOOTSTRAP
*)
let append_to_body elt =
Dom.appendChild Dom_html.document##.body (Tyxml_js.To_dom.of_element elt)
;;
let () = append_to_body r_view