How to Handle Navigation in a SPA in Bucklescript-TEA

Bucklescript-TEA has a way to track and handle navigation or the way you can think about it is URL changes.

It tracks it in a record called Web.Location.location:

type location =
  { href     : string
  ; protocol : string
  ; host     : string
  ; hostname : string
  ; port     : string
  ; pathname : string
  ; search   : string
  ; hash     : string
  ; username : string
  ; password : string
  ; origin   : string
  }

We don’t need to setup this manually, it is given to us to use automatically, which we’ll see later.

First we create a message to handle the event for a URL change and pass us back a new location:

type msg
  = UrlChange of Web.Location.location
  [@@bs.deriving accessors]

The [@@bs.derving accessors] gives us this function automatically:

val urlChange : location -> msg

let urlChange location =  (* Note the lowercase `u` *)
  UrlChange location

If this is confusing, take a look at this post: Click here

Assuming you already know about the App.standardProgram (if not, read this first: How to use standardProgram in Bucklescript-TEA.

Then what we need to do, is use a different program to start that can handle URL changes.

We can use Navigation.navigationProgram:

val navigationProgram : 
  (location -> msg) 
  -> 
  { init
  ; update
  ; view
  ; sub
  ; shutdown
  } 
  -> 
  'msg programInterface (* Default return value *)
let main =
  Navigation.navigationProgram urlChange
    { init
    ; update
    ; view
    ; subscriptions = (fun _ -> Sub.none)
    ; shutdown = (fun _ -> Cmd.none)
    }

You can just ignore the shutdown function and pass it a function that returns Cmd.none.

You can see we are passing that urlChange function in so it knows to invoke that function when a new URL location changes.

The init function gets changed slightly by also accepting the initial location: (This is where we get handed our first location setup for us already)

type model =
  { history : Web.Location.location list
  ; ...
  }

let init () location =
  { history = [ location ] (* Add initial location to an empty list *)
  ; ...
  }, Cmd.none

So we store it here in a list of locations so that we can track the history of URL changes.

Then, what happens is that whenever the URL changes, it fires off that message (you can name it whatever you want) and it gets passed into your update function.

let update model msg =
  match msg with
  | UrlChange location ->
    { model with
      history = location :: model.history
    }, Cmd.none

Here we just store the new location is the history list.

Now you can take the location record and parse it, and direct how your page will navigate.


Here’s how that may work. We’ll assume were using hash-based navigation:

First, we’ll add our current page to the model:

type model =
  { history : Web.Location.location list
  ; page    : string
  ; ...
  }

Then we’ll set our home page it in our init function:

let init () location =
  { history = [ location ]
  ; page    = "#home"
  ; ...
  }, Cmd.none

Then in our update function, we just update new page to the hash of the URL:

let update model msg =
  match msg with
  | UrlChange location ->
    { model with
      history = location :: model.history
    ; page = location.Web.Location.hash
    }, Cmd.none

And in your view, you just pattern match on the page hash to switch to new views:

let view_main model =
  match model.page with
  | "#home" -> view_home model
  | "#other" -> view_other model
  | _ -> view_not_found model

let view model =
  div []
    [ aside [] [ text "Sidebar" ]
    ; main [] (view_main model)
    ]

And then you have links somewhere to change pages

a [ href ("#" ^ page_name) ] [ text page_name ]

You can modify any of this to suit your needs.

That’s it! Hope that helps.

Comments