Navigation and SPA are a popular topic these days in Elm-land, with lots of good stuff, like:

Selectors as an intermediary between model (pure data) and viewModel (derived version, fit for rendering): Medium Post by Charlie Kosten here.

Elm Models as Types, a way structuring your model in Union Types rather than straight records, to get better readibility and improved making impossible states impossible. Medium Post by Charlie Koster here.

Taco, a library to extend (view) functions to take in more than just the model, to facilitate sharing application-wide data across various modules of your SPA. Github repo by Ossi Hanhinen here.

These ideas and patterns sort of intersect with the setup I use. But on some points there are differences, so I like to share these ideas here as well.

A simple navigation structure may look something like this:

Simple app flow for the example: show details and go back

There’s a couple of ways you can implement navigation in Elm:

The URL determines model state*. Model state determines URL. Keep model model state and URL in sync at all times*.

* If you want the URL to determine the model state, allowing users to type the URL in the browser bar, then the easiest way to do this is with a hash (like main#/movies ) in the URL. Using a hash will make the browser stay on the same page, and not reload the page by making a server request. You can make it work with plain URLs (like main/movies ), but then you need to

a) set up your server to always return the same page and

b) implement additional stuff to preserve your model state (reloading the page will reset your app’s model).

In this post the 3rd kind is implemented.

Also, I really wanted my app to behave in a very specific (and more user-friendly) way:

only show a route (page) if all data for that page is available

Friendly list navigation : only show details page if details are there..

For this, I set up my navigation in a different way. The Route type is still used, but only in Msg . It is not stored in the model. Instead, a new type Page is stored in the model.

The new type Page = Route with Data, essentially making impossible routes impossible.

Sync page and browser bar — the logic

To make all this work, the app needs the following flow for dealing with a new url:

Flow to keep browser bar and current page in sync

The check whether the route changed is to prevent endless circular updates.

If the data is available, the model is updated. If the data needs to be fetched from a server, you can set a loading state in the model. In that case, the users stays at the current page while the data is loading. So you need to set the url back to the url for the current page.

Later, when the data does come in, the flow is something like this:

Flow for when the data comes in

If the data received belongs to the fetch request made, then the model is updated. At this time, you also need to set the url to the new route.

In code —Routes and helpers

To use this in my code, I define some additional types and helpers:

In code — the update function

Below is the UrlChange branch of the update function:

case Route.parse newLocation of

Nothing ->

( { model | message = "invalid URL: " ++ newLocation.hash }

, Route.modifyUrl model.currentPage

) Just validRoute ->

if Route.isEqual validRoute model.currentPage then

( model , Cmd.none )

else

case validRoute of

Home ->

( { model | currentPage = HomePage}

, Cmd.none

)



Movies ->

( { model

| currentPage = MoviesPage (Dict.toList model.movies)

}

, Cmd.none

)



MovieDetail id ->

( { model

| serverRequest = Just id

, message =

"Loading data for movie : " ++ toString id

}

, Cmd.batch

[ fetchMovieDetail id

, Route.modifyUrl model.currentPage

]

)

Better view functions

The greatest benefit of saving the data inside the Page in the model, is that view functions will become much easier to build.

view : Model -> Html Msg

view model =

case model.currentPage of

HomePage ->

homeView model



MoviesPage movies ->

moviesView model movies



MovieDetailPage movieId movie ->

moviesDetailView model movieId movie