elm christmas

Routing in Elm

←Previous postNext post →

When creating a single-page application you will want to handle URL changes and routing. It might be difficult to know where to start and how to structure this well, so we will walk through what you need.

A 4 min read written by
Ingar Almklov

Browser.application

When you are making an SPA in Elm you want Browser.application. According to the documentation it has this signature:

application :
    { init : flags -> Url -> Key -> ( model, Cmd msg )
    , view : model -> Document msg
    , update : msg -> model -> ( model, Cmd msg )
    , subscriptions : model -> Sub msg
    , onUrlRequest : UrlRequest -> msg
    , onUrlChange : Url -> msg
    }
    -> Program flags model msg

If you have done some Elm stuff before you probably recognize init, view, update and subscriptions, as they are essential parts of The Elm Architecture. What's new here is onUrlRequest and onUrlChange, and that init gets an Url and something called a Key in addition to flags.

Lets get the Key thingy out of the way. In short, it's something you need when you want to use navigation functions like pushUrl, replaceUrl, back, and forward. You should store this value in your model.

The Url that init gets is the current URL at the time of the application loading.

onUrlRequest and onUrlChange

As the underlying integration with the browser is handled by the Elm runtime, the only thing we have to implement is what to do when

  1. The user clicks a link (onUrlRequest)
  2. The URL has changed (onUrlChange)

The usefulness of onUrlRequest is that you get a chance to for example presist some data before the user leaves the page. See the UrlRequest documentation for more information.

As the name onUrlChange implies, it's there to give you a message on URL changes. This message contains the new URL. Working with the pretty primitive URL record is doable but not really convenient. Luckily, there are some great modules we can use.

Handling URL changes

Let's say we have an application with an index page and two sub-pages:

Since there is nothing stopping the user from entering https://example.com/some/unknown/url in the URL bar in their browser, we also need to handle that. In our example we will just show the index page for all unknown routes, but in your own application you might want some nice 404 page.

What we want to end up with is something like this:

type Page
    = Index
    | Cats
    | User Int


viewPage : Page -> Html msg
viewPage page =
    case page of
        Index ->
            viewIndex

        Cats ->
            viewCats

        User userId ->
            viewUser userId

For this we need some function that takes a URL and turns it into a Page value:

urlToPage : Url -> Page

We will make use of the Url-Parser module from elm/url. We have to make a "parser", which is a description for how to parse some part of a URL. Let's step through the code.

-- import some very important stuff. IMPORTant, get it? Ha ha.
import Url exposing (Url)
import Url.Parser as Url exposing (Parser, (</>))

urlToPage : Url -> Page
urlToPage url =
    -- We start with our URL
    url
        -- Send it through our URL parser (located below)
        |> Url.parse urlParser
        -- And if it didn't match any known pages, return Index
        |> Maybe.withDefault Index

-- The type signature here is a bit gnarly, but you can read it as "a parser for a Page"
urlParser : Parser (Page -> a) a
urlParser =
    -- We try to match one of the following URLs
    Url.oneOf
        -- Url.top matches root (i.e. there is nothing after 'https://example.com')
        [ Url.map Index Url.top
        -- Url.s matches URLs some string, in our case '/cats'
        , Url.map Cats (Url.s "cats")
        -- Again, Url.s matches a string. </> matches a separator ('/') in the URL, and Url.int matches any integer and "returns" it, so that the user page value gets the user ID
        , Url.map User (Url.s "user" </> Url.int)
        ]

There are many ways to compose parsers (Url.s is as much a parser as our urlParser is) and I recommend checking out Url-Parser's documentation.

We've parsed the route, now what?

All that is left is to use urlToPage in init and in the UrlChange branch of update to set the page value in the model.

If you want the complete source for this example, have a look at https://ellie-app.com/49wDhn9hncya1. Do note that navigation doesn't actually work when run in Ellie, so you will have to copy the code and run it yourself if you want to play around with it.

PS: this is of course not the only way of structuring your code for dealing with URL changes, but it is a relatively simple approach that I've found useful. Check out the links below for more resources and examples on how to do this kind of stuff.

←Previous postNext post →