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
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