Contents

Getting Started With Incr_dom - Part 2

Contents

Note: Incr dom is superceded by a new, much easier to use library called Bonsai

This installment will add the basic frame of the single page application. We’ll create a menu and be able to navigate between the various pages. In later chapters we’ll fill in the contents of each of the pages. The result of this part is shown below (interactive):

Prerequisites

In order to give our application a bit of polish we’ll be using a CSS-only framework to go with our application. Bulma.io is a CSS framework that comes with many different pre-designed components for use in any application. Being CSS only makes it easy to integrate into Incr_dom, since Incr_dom wants full control over state handling. There are escape hatches, but it can get messy. For our purposes download the release from here. Find the bulma-0.7.5/css/bulma.min.css file in the zip and copy it to the root of the Incr_dom project.

Now update the index.html file to include a reference to this CSS file after the <meta> tag:

<link rel='stylesheet' type='text/css' href='bulma.min.css'>

It will also be helpful to have a Makefile to shorten the build cycle a bit. Here’s an example one you can use:

all:
    dune build bin/main.bc.js

show:
    firefox index.html

clean:
    dune clean

Now you can type make at the terminal to build instead of dune build bin/main.bc.js.

Adding A Main Menu

We’ll add a main menu that the user can use to get to any other sub-page. We’ll add the following items:

  • New Game
  • High Scores
  • Credits

Open lib/tetris.ml and add a new variable called main_menu above the view function:

let main_menu =
    let open Vdom in
    let open Node in
    let open Attr in
    let field content =
      div [class_ "field"] [
        div [class_ "control"] [
          content
        ]
      ]
    in
  
    let menu_button content =
      button [
        classes ["button"; "is-info"; "is-centered"];
        style Css_gen.(
            display `Block
            @> margin ~bottom:(`Rem 0.5) ()
            @> width (`Percent Percent.(of_percentage 100.))
          )
      ] [text content]
    in
  
    div [classes ["container"; "has-text-centered"]] [
      div [class_ "content";
           style Css_gen.(
               width (`Percent Percent.(of_percentage 25.))
               @> display `Inline_block
               @> padding ~top:(`Rem 2.) ()
             )
          ] [
        div [classes ["has-text-centered"]] [
          h1 [] [text "Tetris"];
        ];
        field (menu_button "New Game");
        field (menu_button "High Scores");
        field (menu_button "Credits");
      ]
    ]
  ;;

Then update the view function to use this variable instead of returning a Hello World! div:

let view model ~inject =
  let open Incr.Let_syntax in
  let%map model = model in
  main_menu
;;

Compling and rebuilding should look something like this:

/images/tetris_menu.png

Unfortunately it’s a fair amount of code to do what seems like a simple layout. An outer container div is marked inline-block so text-align: centered can be used on it (the Bulma.io has-text-centered CSS class). This centers the menu. Then the Tetris title is wrapped again to center it inside the menu div. Each button is wrapped in a Bulma.io field to get some nice padding and vertical layout. is-info is used on each button to give it the blue color (default is white). Nothing exciting yet, but now we’re ready to add some sub-pages.

Adding A Sub-Page

There will be a couple steps we need to do to get sub-pages working properly. We’ll need to keep track of which page we’re on in the model. We’ll need a way to change pages, and we’ll need a way to route to the right sub-page when a link is loaded. And of course the view function needs to be updated to show the correct page. To tackle the first task lets create a new sum type that represents the different sub-pages and add it to the model. Update the model to look like:

type page =
    | MainMenu
    | NewGame
    | HighScores
    | Credits
    [@@deriving sexp, compare]

type t = {
  current_page : page;
}
[@@deriving sexp, compare]

let init = {
  current_page = MainMenu
}

let compare = ...

Now we can keep track of the different pages in the model, but we’ll need a way to change between pages and update the model. This is where the Action model and inject function comes into play. Update the action type to be:

type t = ChangePage of Model.page
[@@deriving sexp]

and update the apply_action function to be:

let apply_action (model : Model.t) (action : Action.t) _ ~schedule_action =
  match action with
  | ChangePage new_page ->
    Model.{ current_page = new_page }
;;

Now let’s update the buttons on the main menu to navigate to the proper page. The revamped main_menu code looks like:

let main_menu inject =
  (* ... *)
  let field content = (* ... *)
  let menu_button content (page : Model.page) =
    button [
      (* ... *)
      on_click (fun _ -> inject Action.(ChangePage page));
    ] [text content]
  in

  div [classes ["container"; "has-text-centered"]] [
      (* ... *)
      field (menu_button "New Game" Model.NewGame);
      field (menu_button "High Scores" Model.HighScores);
      field (menu_button "Credits" Model.Credits);
    ]
  ]
;;

The main change is an on_click attribute has been added to the buttons which calls the inject function with the appropriate Action.t to change the page. The inject function returns a Vdom.Event.t which is intercepted by the Incr_dom framework and queues a call to the apply_action function for updating the model. Incr_dom is written in such a way that it will update the model as much as possible before rendering. So you don’t need to worry about actions causing unnecessary renders.

To actually see something when a button is clicked, a view for each sub-page needs to be created. We’ll keep it simple and display a header for each sub-page and a back button. We’ll wrap this up in a helper function so each sub-page provides its content, but each page will have the same header style and behavior. Create the helper function subpage somewhere above main_menu:

let subpage page_name content inject =
  let open Vdom in
  let open Node in
  let open Attr in
  let outer_padding =
    let pad = `Rem 0.5 in
    Css_gen.(padding ~top:pad ~bottom:pad ~left:pad ~right:pad ())
  in
  div [class_ "container"] [
    div [class_ "content";
         style outer_padding]
        [
          div
            [style Css_gen.(display `Inline_block)]
            [h2 [] [text page_name]];

          button
            [classes ["button"; "is-info"];
             style Css_gen.(float `Right);
             on_click (fun _ -> inject Action.(ChangePage MainMenu))]
            [text "Back"];

          content;
        ]
  ]
;;

Pretty simple helper that will wrap up any content on the page with a header of the page name and a back button that goes back to the main menu. Lets create a variable for each of our subpages:

let new_game_page =
  subpage "New Game" Vdom.Node.(div [] [])
;;

let high_scores_page =
  subpage "High Scores" Vdom.Node.(div [] [])
;;

let credits_page =
  subpage "Credits" Vdom.Node.(div [] [])
;;

And finally let’s update the view function to display based on the Model’s current_page field:

let view model ~inject =
  let open Incr.Let_syntax in
  let%map current_page =
    model >>| (fun m -> m.Model.current_page)
  in
  match current_page with
  | MainMenu -> main_menu inject
  | NewGame -> new_game_page inject
  | HighScores -> high_scores_page inject
  | Credits -> credits_page inject
;;

Compiling and reloading should give working links! Here we can see that we projected out only the current_page variable. It doesn’t really matter now, since the model only has one field, but shows the syntax for projecting out sub-parts of the Model.

Conclusion

This part added a menu with some sub-pages and introduced the Bulma.io CSS framework. The next part in the series will start adding functionality to the different sub-pages. Code for this part can be found here.