Pages
Overview
Pages are the basic building blocks of your Elm Land application. When a user visits a URL, Elm Land will use the names of the files in your src/Pages folder to decide which page to render.
Later on in this section, you'll learn different kinds of routes and priority.
Already familiar with Elm?
In a standard Elm project, all URL requests go to one Main.elm file. In Elm Land, you can think of each page as its own main function.
The big difference is that all pages are connected to each other, can share data with via Shared.Model, and access type-safe URL information using the Route type.
No need to write your URL parsers by hand!
Adding pages
When you run the elm-land add page command, a new page is created. Each new page will look something like this:
elm-land add page /settingsThat command generates src/Pages/Settings.elm:
module Pages.Settings exposing (Model, Msg, page)
import Page exposing (Page)
-- ...
page : Shared.Model -> Route () -> Page Model Msg
page shared route =
Page.new
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}"What about view, sandbox, and element?"
Earlier in the guide, you may have seen commands like add page:view, add page:sandbox, or add page:element.
Those three commands are designed to help you learn the basics "The Elm Architecture".
Once you are comfortable with Model, Msg, Effect, and Sub, we recommend only using Page.new in your app.
Understanding pages
The Page.new function takes in four smaller functions. Together, they tell Elm Land how your page should look and behave. Here's an overview of each function:
init
This function is called anytime your page loads.
init : () -> ( Model, Effect Msg )
init _ =
...update
This function is called whenever a user or the browser sends a message.
update : Msg -> Model -> ( Model, Effect Msg )
update msg model =
...view
This function converts the current model to the HTML you want to show the user.
view : Model -> View Msg
view model =
...subscriptions
This function listens for ongoing events like "window resized" or "javascript sent a message" and forwards that as a Msg for the update function to handle.
subscriptions : Model -> Sub Msg
subscriptions model =
...Working with "shared" or "route"
You may have noticed that every page is a function that receive two arguments, shared and route:
page : Shared.Model -> Route () -> Page Model Msg
page shared route =
Page.new
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}But what are these arguments for?
shared– Stores any data you want to share across all your pages.- In the Shared section, you'll learn how to customize what data should be available.
route– Stores URL information, including things likeroute.paramsorroute.query.- In the Route section, you'll learn more about the other values on the
routefield.
- In the Route section, you'll learn more about the other values on the
Both of these values are available to any function within page. That means init, update, view and subscriptions all can get access to shared and route.
In the code example below, note how we pass the shared value as the first argument of the view function:
module Pages.Settings exposing (Model, Msg, page)
import Page exposing (Page)
-- ...
page : Shared.Model -> Route () -> Page Model Msg
page shared route =
Page.new
{ init = init
, update = update
, view = view shared
, subscriptions = subscriptions
}After we pass in the shared argument on line 12, we can update our view function to get access to shared in our view code:
-- BEFORE
view : Model -> View Msg
view model = ...
-- AFTER
view : Shared.Model -> Model -> View Msg
view shared model = ...The same concept applies to init, update, and subscriptions.
For example, you might want your init function to use a URL parameter to decide what API endpoint to call. In this case, we can pass route into our init function using the same process as before:
module Pages.Settings exposing (Model, Msg, page)
import Page exposing (Page)
-- ...
page : Shared.Model -> Route () -> Page Model Msg
page shared route =
Page.new
{ init = init route
, update = update
, view = view
, subscriptions = subscriptions
}After we pass in the route argument on line 10, we can update our init function to get access to route in our view code:
-- BEFORE
init : () -> ( Model, Effect Msg )
init _ = ...
-- AFTER
init : Route () -> () -> ( Model, Effect Msg )
init route _ = ...Removing pages
Elm Land uses the elm-land add page command to create pages, so a few users have expected a similar elm-land remove page command. There is no special command for removing a page file, instead you can delete the file in your file explorer, or run this command:
rm src/Pages/Settings.elmElm Land will automatically delete the generated code associated with the old page. The Elm compiler will let you know if any other parts of your app depended directly on that page.
We recommend using Route.href rather than Html.Attributes.href when linking to other pages with <a> tags. This allows Elm Land to detect broken links and tell you about them.
Route naming convention
When working with pages, it's important to understand how Elm Land determines which page files to load. If you have worked with a JavaScript application framework before, these rules should look familiar.
Here are the categories of routes you'll find in every Elm Land project, ordered from most to least specific:
| Route | URL example | Description |
|---|---|---|
| Homepage | / | Handles requests to the top-level URL (/). |
| Static routes | /people | Directly maps one URL to a page. |
| Dynamic routes | /people/:id | Maps many URLs with a similar structure to a page. |
| Catch-all routes | /people/* | Like dynamic routes, but can support any depth. |
| Not found page | /* | Handles any URL that can't find a matching page. |
Homepage
This file is created automatically for you with the elm-land init command. It uses a special trailing underscore to help distinguish itself from the static routes documented below.
Here's a visual to help understand the subtle difference:
| Page filename | URL |
|---|---|
src/Pages/Home_.elm | / |
src/Pages/Home.elm | /home |
Note: In other projects, you might see this file called "index" or "root" or "top-level".
Static routes
Let's start by talking about "static routes". These routes directly map one URL to a page file.
You can use capitalization in your filename to add a dash (-) between words.
| Page filename | URL |
|---|---|
src/Pages/Hello.elm | /hello |
src/Pages/AboutUs.elm | /about-us |
src/Pages/Settings/Account.elm | /settings/account |
src/Pages/Settings/General.elm | /settings/general |
src/Pages/Something/Really/Nested.elm | /something/really/nested |
Dynamic routes
Some page filenames have a trailing underscore, (like Id_.elm or User_.elm). These are called "dynamic pages", because this page can handle multiple URLs matching the same pattern. Here are some examples:
| Page filename | URL | Example URLs |
|---|---|---|
src/Pages/Blog/Id_.elm | /blog/:id | /blog/1, /blog/2, /blog/xyz, ... |
src/Pages/Users/Username_.elm | /users/:username | /users/ryan, /users/2, /users/bob, ... |
src/Pages/Settings/Tab_.elm | /settings/:tab | /settings/account, /settings/general, /settings/api, ... |
The name of the file (Id_, User_ or Tab_) will determine the names of the parameters available on the route.params value passed into your page function:
-- /blog/123
route.params.id == "123"
-- /users/ryan
route.params.user == "ryan"
-- /settings/account
route.params.tab == "account"For example, if we renamed Settings/Tab_.elm to Settings/Foo_.elm, we'd access the dynamic route parameter with route.params.foo instead!
"Wait, I've seen these before!"
If this concept is already familiar to you, great! "Dynamic routes" aren't an Elm Land idea, they come from popular frameworks like Next.js and Nuxt.js:
- Next.js uses the naming convention:
blog/[id].js - Nuxt.js uses the naming convention:
blog/_id.vue
Because Elm files can't start with special characters, Elm Land uses a trailing _ to denote the difference between Blog/Id.elm and Blog/Id_.elm:
Blog/Id.elmis a static page that only handles/blog/idBlog/Id_.elmis a dynamic page that can handle/blog/id,/blog/xyz,/blog/3000, etc
Catch-all routes
Sometimes you'll need to define a page that handles an unknown depth. Using the special reserved keyword ALL_.elm, you can define a "catch-all" route that does just that.
Here are a few examples to help you visualize how it works:
| Page filename | URL |
|---|---|
src/Pages/ALL_.elm | /* |
src/Pages/Blog/ALL_.elm | /blog/* |
src/Pages/Settings/Tab_/ALL_.elm | /settings/:tab/* |
src/Pages/:User/:Repo/Tree/ALL_.elm | /:user/:repo/tree/* |
The all_ parameter
For dynamic parameters, we need access to a single variable, like params.id or params.username. Because catch-all routes are nested, you'll want a List String back when dealing with them.
Every catch-all route has access to the params.all_ variable:
-- Filename: src/Pages/ALL_.elm
-- URL: /each/part/of/the/path
route.params ==
{ all_ = [ "each", "part", "of", "the", "path" ]
}Note: To avoid confusion with a dynamic route All_.elm, Elm Land adds a trailing underscore after all_ on the params.
Simple catch-all example
If you're making a blog, you might want a page that handles all requests within the /blog/* URL. Here are some examples to help you visualize the value of route.params for different URLs:
| Page filename | URL |
|---|---|
src/Pages/Blog/ALL_.elm | /blog/* |
-- /blog/hello-world
route.params ==
{ all_ = [ "hello-world" ]
}
-- /blog/elm/part-1
route.params ==
{ all_ = [ "elm", "part-1" ]
}
-- /blog/elm/part-2
route.params ==
{ all_ = [ "elm", "part-2" ]
}Advanced catch-all example
A practical example of this is GitHub's file explorer page. These URLs have different depth, depending on the content of a user's repo.
With Elm Land, you can mix and match dynamic parameters with your catch-all files to get the exact URL route parameters you need. Here's another visual example:
| Page filename | URL |
|---|---|
src/Pages/:User/:Repo/Blob/:Branch/Tree/ALL_.elm | /:user/:repo/tree/:branch/* |
-- /elm-land/elm-land/tree/main/README.md
route.params ==
{ repo = "elm-land"
, user = "elm-land"
, branch = "main"
, all_ = [ "README.md" ]
}
-- /ryannhg/elm-spa/tree/master/README.md
route.params ==
{ repo = "ryannhg"
, user = "elm-spa"
, branch = "master"
, all_ = [ "README.md" ]
}
-- /elm-land/elm-land/tree/main/projects/cli/package.json
route.params ==
{ repo = "elm-land"
, user = "elm-land"
, branch = "main"
, all_ = [ "projects", "cli", "package.json" ]
}Not found page
By default, a 404 page is generated by Elm Land. This will automatically handle any URL request that doesn't map to one of your page files.
Imagine these are the pages in your project:
src/
└── Pages/
├── Home_.elm
├── Settings
│ ├── Account.elm
│ └── Notifications.elm
├── People.elm
└── People/
└── Id_.elmIf these were the pages in your app, here's how each URL would map to a page file:
| URL | Elm Land Page |
|---|---|
/ | src/Pages/Home_.elm |
/settings | ( Page not found! ) |
/settings/account | src/Pages/Settings/Account.elm |
/settings/notifications | src/Pages/Settings/Notifications.elm |
/people | src/Pages/People.elm |
/people/ryan | src/Pages/People/Ryan.elm |
/people/duncan | src/Pages/People/Duncan.elm |
/people/something/nested | ( Page not found! ) |
/banana | ( Page not found! ) |
In the Custom 404 Pages section, you'll learn how to customize your 404 page. When you do that, a new file called NotFound_.elm will appear in your src/Pages folder.
Just like we saw with the homepage file, the trailing underscore helps prevent confusion with any projects containing a static route at /not-found:
| Page filename | URL |
|---|---|
src/Pages/NotFound.elm | /not-found |
src/Pages/NotFound_.elm | /* |
Auth-protected pages
Because Elm Land is designed for building web applications, it also comes with a built-in way to mark a page as "auth-protected". An "auth-protected" page is one that shouldn't be rendered for users that aren't signed in.
You can easily upgrade any page to become "auth-protected", by adding the Auth.User as the first argument:
-- BEFORE
page : Shared.Model -> Route () -> Page Model Msg
-- AFTER
page : Auth.User -> Shared.Model -> Route () -> Page Model MsgBy adding Auth.User as the first argument of your page function, you're letting Elm Land know that this page should only show when a user is signed in.
In the Auth section, we'll learn more about the User type, how to define redirect rules, and more.
Elm Land