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.