A Beginner’s Guide to making a Single Page app in Elm

Twenty years ago in 1997

We’re going to begin our journey into Elm navigation by making a simple web site that could have been made in 1997. Take a moment to look at the site and it’s source.

Why 1997? It was about the time that I started making websites and this was the technique that I used back then. Before jumping into building a single page application in Elm, I thought it would be beneficial to provide a source of comparison that was built with more traditional techniques.

Routing is the mapping of web pages to their URLs. With this technique, routing is determined by where the html files exist in the file system. It’s a simpler system than building a single page app and favours sites where the content doesn’t often change.

We’re going to try and build this 1997 web site as a SPA in Elm

Why build a single page application (SPA)?

With the added complexity of building a SPA, it’s reasonable to wonder when it’s appropriate to choose this technique.

Web apps, such as gmail, benefit from being built as a SPA. It makes an app feel like an app and not a series of web pages. Navigating between pages in a SPA tends to be faster because you only need to load new information when going to a new page. In addition, you can include animations for page transitions to create a fluid user experience.

Another reason to consider building a SPA is if you have an existing backend API for an Android app or an iOS app, creating a SPA that consumes data from that API might get your product faster to market than a server rendered approach.

Elm Navigation

We will need a package called Elm Navigation to build our SPA. Actually, that’s a lie: we don’t need this package at all. However, one of the nice things about web browsers, compared to other apps, is the fact that we have a location bar with an address in it. With an address, you can do things such as share a page with your friends or bookmark it.

In order to use Elm Navigation, we will need to use the Elm Navigation program as opposed to the regular Elm program.

program :
(Location -> msg)
->
{ init : Location -> (model, Cmd msg)
, update : msg -> model -> (model, Cmd msg)
, view : model -> Html msg
, subscriptions : model -> Sub msg }
-> Program Never model msg`

Notice that whenever the URL changes, we get a Location -> msg argument. Secondly, the init function takes an argument of Location.

Caution: some tutorials reference the older version of the Elm Navigation program. You can tell if this is the case if the record passed to the program contains urlUpdate in addition to view, update and subscriptions.

Elm SPA approach 1: Updating the address yourself

How might we go about updating the address? Elm Navigation comes with two functions for this:newUrl and modifyUrl. The former creates a new address in your browser history so that you can use the browser’s back button and the latter doesn’t.

Here is a demo app that uses modifyUrl to, unsurprisingly, modify the URL. And that’s all it does!

An app that recites dialogue in the browser’s location bar

Every three seconds, we get the next line of the dialogue and use Navigation.modifyUrl to update the URL. Notice that this is an Elm command as it is a side effect. I like to use the ! syntax but it is just as valid to have written line 7 as (nextModel, Navigation.modifyUrl nextLine).

Full source of the above app is available here.

The point that I want to make here is that updating the browser’s URL with Elm Navigation is not connected to changing the contents of the page. If you want to change the contents of the page, that’s something you’ll need to implement yourself.

In addition, if someone bookmarks a page after a change of address, you’ll need some backend logic to ensure that your Elm app can reproduce the state in your Elm app when someone accesses that address. For example, if your Elm app initially loads at http://my-elm-app.com/foo and while using the app, the URL updates to http://my-elm-app.com/bar, you will need something on the backend to handle http://my-elm-app.com/bar.

This approach can be cumbersome as we would need to update the address in the location bar and update the page’s content when a user clicks on a link.

As if that weren’t enough, you’d have to have some code preventing the default behaviour of clicking on a link. For example, if your app started at http://my-elm-app.com/foo and you have a link to http://my-elm-app.com/bar, the browser’s natural behaviour is to make a request to http://my-elm-app.com/bar but we don’t want that, we just want to update the URL and load a new browser.

If only there were an easier way!

Approach 2: Reacting to a change of address

Luckily, there’s another approach and that’s to use hash links and then have Elm Navigation react to the change of address in the location bar.

Here is a demo app that simply reacts to a change of address.

This app can detect changes in the URL — and that’s all it can do.

Every time the URL changes, Elm Navigation gives us a UrlChange with location as a payload. In this app, we’re just reacting to a change but it’s easy to imagine an app in which we load different pages according to the location.

Notice that we’re using a hash (#) in the location bar. Any change after the hash does not trigger a request for a new page. The hash symbol in URLs were initially used to jump to sections in a web page using anchor tags. It is still used for this purpose on sites such as Wikipedia but it also serves as a handy way to provide routes for SPAs.

This method is easier to implement but some people feel the hashes make the URLs look ugly.

Full source of the above app is available here.

Making the website from 1997 in Elm

Using Approach 2, we will make the site from 1997. In addition to Elm Navigation, we will require another package called Elm Url-Parser.

As shown in the demo app, Elm can react to a change in URL but we still need a way to convert the URL into meaningful Elm Data. That’s where Elm Url-Parser comes into the picture.

module UrlParsing exposing (..)
import UrlParser as Url exposing (..)
type Route
= Home
| Birds
| Cats
| Dogs
route : Url.Parser (Route -> a) a
route =
Url.oneOf
[ Url.map Home top
, Url.map Birds (s "birds")
, Url.map Cats (s "cats")
, Url.map Dogs (s "dogs")
]

What we’re saying here is that a valid route is going to be one of Home, Birds, Cats or Dogs. What s "birds" means is “match a path that has the string birds and only birds in the URL”. For example, if this app were to load at http://foo.com/index.html, then it would match http://foo.com/index.html#birds . Home has been mapped to top — that just means it will match the URL at the top level. So in our example, it would match the URL of http://foo.com/index.html .

Think of the function route as a set of instructions of how to map the location to nice Elm types such as Birds and Dogs.

In order to put it into action, we need to put it in the update function.

module Update exposing (update)
import Msg exposing (Msg(..))
import Model exposing (Model)
import UrlParser as Url exposing (parsePath)
import UrlParsing exposing (route)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UrlChange location ->
let
nextRoute =
Url.parseHash route location
in
{ model
| history = nextRoute :: model.history
, currentRoute = nextRoute
}
! []

We use our function route as an argument in Url.parseHash route location. We’re using parseHash because we’re using the hash style as discussed in Approach 2. If you would like to use Approach 1, there’s a function called parsePath. Both functions gives us back a Maybe Route where the union type of Route was defined as one of Home, Birds, Cats or Dogs. It’s a type of Maybe because it might not match any of our valid routes.

We’re keeping a history in our model just for interest but you don’t have to do this in your app.

Lastly, to display the correct view, we can use a view function like the one shown below to map a route to a particular view.

view : Model -> Html msg
view model =
case model.currentRoute of
Just Home ->
homeView model
Just Birds ->
animalView "birds" "have wings and a beak"
Just Cats ->
animalView "cats" "have whiskers and claws"
Just Dogs ->
animalView "dogs" "bark and like to go for walks."
Nothing ->
notFoundView

Here is our final product and you can view the full source here.

The Elm single page app version of the web site from 1997

Conclusion

We have created a simple SPA using Elm and I hope this gives you a starting point to build your own projects. The app that we built was not the ideal case for building a single app due to the static content but I wanted to keep things simple for this blog post.

Further Reading

Be sure to check out Elm URL Parser documentation as it can match more complex patterns than what we covered in this post.

Wouter In t Velt wrote a good article about choices one needs to make when building a SPA in Elm.