Pages & routing

Source code: GitHub

This next guide will show you how pages, routing, and the elm-spa add command work together to automatically handle URLs in your elm-spa application.

The setup

Just like with the last guide, we can use elm-spa new and elm-spa server to get a brand new elm-spa project up and running:

elm-spa new

This generates the "Hello, world!" homepage from before:

elm-spa server

A browser displaying "Hello world"

Adding a static page

elm-spa add /static static

This command adds a page at http://localhost:1234/static with the static template. This is similar to Home_.elm, but it has access to Shared.Model and Request in case we need data from either of those.

Here is the complete Static.elm file:

module Pages.Static exposing (page)

import Page exposing (Page)
import Request exposing (Request)
import Shared
import View exposing (View)


page : Shared.Model -> Request -> Page
page shared req =
    Page.static
        { view = view
        }


view : View msg
view =
    View.placeholder "Static"

The View.placeholder function just stubs out the view function with an empty page that only renders "Static" in the browser.

Visit http://localhost:1234/static to see it in action!

Making a layout

Before we continue, I want to make a layout with a navbar so that we can easily navigate between pages without manually editing the URL.

I'll create a file at src/UI.elm that looks like this:

module UI exposing (layout)

import Html exposing (Html)
import Html.Attributes as Attr


layout : List (Html msg) -> List (Html msg)
layout children =
    let
        viewLink : String -> String -> Html msg
        viewLink label url =
            Html.a [ Attr.href url ] [ Html.text label ]
    in
    [ Html.div [ Attr.class "container" ]
        [ Html.header [ Attr.class "navbar" ]
            [ viewLink "Home" "/"
            , viewLink "Static" "/static"
            ]
        , Html.main_ [] children
        ]
    ]

Using the layout in a page

Because it works from one List (Html msg) to another, we can add UI.layout in front of the body list on both pages:

-- src/Pages/Home_.elm

view : View msg
view =
    { title = "Homepage"
    , body = UI.layout [ Html.text "Homepage" ]
    }
-- src/Pages/Static.elm

view : View msg
view =
    { title = "Static"
    , body = UI.layout [ Html.text "Static" ]
    }

Use routes, not strings

In src/UI.elm, we had a function for rendering our navbar links that looked like this:

viewLink : String -> String -> Html msg
viewLink label url =
    Html.a [ Attr.href url ] [ Html.text label ]

This function works great– but it's possible to provide a URL that our application doesn't have!

[ viewLink "Home" "/"
, viewLink "Static" "/satic"
]

Here, I mistyped the URL /satic, but the compiler didn't warn me about it! Let's use the Route values generated by elm-spa to improve this experience:

import Gen.Route as Route exposing (Route)

viewLink : String -> Route -> Html msg
viewLink label route =
    Html.a [ Attr.href (Route.toHref route) ] [ Html.text label ]

By using the Gen.Route module from .elm-spa/generated, we can pass in a Route instead of a String:

[ viewLink "Home" Route.Home_ 
, viewLink "Static" Route.Static
]

This will prevent typos, but more importantly it allows the Elm compiler to remind us to update the navbar in case we remove Home_.elm or Static.elm in the future.

Deleting either of those pages changes the generated Gen.Route module, so the compiler can let us know that our UI.layout function has a broken link– before our users do!

Adding CSS

In UI.layout, we used Attr.class to provide our HTML with some CSS classes:

Html.div [ Attr.class "container" ]
    [ Html.header [ Attr.class "navbar" ]
        [ viewLink "Home" Route.Home_
        , viewLink "Static" Route.Static
        ]
    ]

The container and navbar classes are used in our code, but not defined in a CSS file. Let's fix that by creating a new CSS file at public/style.css:

.container {
  max-width: 960px;
  margin: 1rem auto;
}

.navbar {
  display: flex;
  align-items: center;
}

.navbar a {
  margin-right: 16px;
}

After creating style.css, we can import the file in our public/index.html entrypoint:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- import our new CSS file -->
  <link rel="stylesheet" href="/style.css">

</head>
<body>
  <script src="/dist/elm.js"></script>
  <script> Elm.Main.init() </script>
</body>
</html>

Using the <link> tag as shown above (with the leading slash!) imports our CSS file. All files in the public folder are available at the root of our web application. That means a file stored at public/images/dog.png would be at http://localhost:1234/images/dog, without including public in the URL at all.

Adding more page types

elm-spa add /sandbox sandbox
elm-spa add /element element
elm-spa add /advanced advanced

These commands add in the other three page types described in the pages guide.

For each page, the View.placeholder function stubs out the view functions so you can visit them in the browser.

For example, http://localhost:1234/element should display "Element" on the screen.

Adding some dynamic routes

To add in dynamic routes, we can use elm-spa add again:

elm-spa add /dynamic/:name static

With this command, we just created a page at src/Pages/Dynamic/Name_.elm. When a user visits a URL like /dynamic/ryan or dynamic/123, we'll be taken to this page.

Let's tweak the default view function to render the dynamic name parameter from the URL:

-- src/Pages/Dynamic/Name_.elm

view : Params -> View msg
view params =
    { title = "Dynamic: " ++ params.name
    , body =
        UI.layout
            [ UI.h1 "Dynamic Page"
            , Html.h2 [] [ Html.text params.name ]
            ]
    }

We can provide in the req.params to the view function by telling our page function to pass it along:

page : Shared.Model -> Request.With Params -> Page
page _ req =
    Page.static    -- 👇 we pass in params here
        { view = view req.params 
        }          

Updating the navbar

Once we wire up these pages to use UI.layout, we can add links to the navbar:

-- src/UI.elm
import Gen.Route as Route

layout : List (Html msg) -> List (Html msg)
layout children =
    let
        viewLink : String -> Route -> Html msg
        viewLink label route =
            Html.a [ Attr.href (Route.toHref route) ] [ Html.text label ]
    in
    [ Html.div [ Attr.class "container" ]
        [ Html.header [ Attr.class "navbar" ]
            [ Html.strong [ Attr.class "brand" ] [ viewLink "Home" Route.Home_ ]
            , viewLink "Static" Route.Static
            , viewLink "Sandbox" Route.Sandbox
            , viewLink "Element" Route.Element
            , viewLink "Advanced" Route.Advanced
            , Html.div [ Attr.class "splitter" ] []
            , viewLink "Dynamic: Apple" (Route.Dynamic__Name_ { name = "apple" })
            , viewLink "Dynamic: Banana" (Route.Dynamic__Name_ { name = "banana" })
            ]
        , Html.main_ [] children
        ]
    ]

That's it!

Feel free to play around with the elm-spa add command to mix-and-match different pages.

As always, the source code for this example is available on GitHub


Next up: Storage