Single Page Web Apps in Elm

Client-side routing without the magic

billperegoy
im-becoming-functional
7 min readNov 6, 2016

--

Why Single Page Apps?

Once we start creating rich, responsive sites using JavaScript, it’s natural to want to keep as much of the functionality in the browser. Minimizing server interactions makes a site navigation crisper and prevents unneeded page loads. On the other hand, navigation on the client side can have a downside. Without URLs that make sense and can be bookmarked, we can easily end up with a site that is fast but not really useful in the real world.

This is where single page web apps come into play. With a single page web app, transitions from page to page are accompanied by changes to the URL. In addition, manually entering a URL will get you to the expected page state. Finally all navigation techniques like forward and back buttons as well as bookmarks, work as expected. So single page web apps give us fast and flexible web pages without the need to contact the server for each page transition.

How Elm is Different

I’ve experimented with single page apps in a number of front-end technologies including Ember, Angular and React. While all of these technologies are effective, I’ve always found them a little bit complex and difficult to use.

In most cases, I’m looking for something relatively simple and the Elm Navigation package provides the functionality I need in a straightforward way. At the core, I want to be able to detect when the URL has changed and extract the elements of the URL in a way that allows me to render the correct page based upon that URL value. We will now see just how the Elm Navigation package gives us that in the simplest form possible.

An Elm Review

If you’ve used Elm in the past, you’ll remember that one of the key secrets to making Elm applications predictable and easy to maintain is that the view is a pure function. It takes in only the application state and produces a DOM model to be rendered. The only way to change the resulting page is to change the model.

That’s all fine, but now we have another need. We want the page we are displaying to change based upon the URL. So how can we do this? The Elm Navigation package gives us the ability to have a change in the URL result in a change to the model. This preserves the purity of the Elm view.

URL Change --> Model Change --> New Rendered View

An Example Application

To give us something to demonstrate, let’s spec out a simple application to use as an example. We will describe the URLs and resulting functionality for each of the pages of this application.

www.site.com                       Home page
www.site.com/#/home Home page
www.site.com/#/about About page
www.site.com/#/users List all users
www.site.com/#/users/:id Show info for one user
www.site.com/#/users/:id/hobbies Show a list of hobbies for 1 user

As you can see, we have described a set of RESTful URLs that allow us to access a couple of static pages as well as dynamically generated pages with different types of user information. We will now walk through making this happen in Elm.

The Main Program

In order to make this possible, we need to make a change to the main Elm program. In the past, we’ve used the Html.program function as the main entry point. To enable navigation we will need to replace this entry point with Navigation.program. Here is an example of how to set this up.

import Navigationmain : Program Never
main =
Navigation.program urlParser
{ init = init
, view = view
, update = update
, urlUpdate = urlUpdate
, subscriptions = subscriptions
}

You’ll notice that this looks just like the Html.program except for the additional items, urlUpdate and urlParser. This is how we connect the asynchronous changes in the URL into the rest of the application. The result is a hook to allow URL changes to be reflected in the model. These model changes can then be reflected into the view allowing the navigation to affect the displayed page.

Connecting to the Model

In order to allow the URL to have an effect on the displayed page, we first need to store the route information in the model. How we do this is wide open as the Navigation package does not specify one golden way. For the example here, I’ve decided to store the components of the URL as elements of an array (see examples below).

www.site.com                       []
www.site.com/#/home ["home"]
www.site.com/#/about ["about"]
www.site.com/#/users ["users"]
www.site.com/#/users/12 ["users", "12"]
www.site.com/#/users/13/hobbies ["users", "13", "hobbies"]

We can then add a representation of the current route to the model and init function as shown below.

type alias RoutePath =
List String
type alias Model =
{ currentRoute : RoutePath
, users : List User
}
init : Route -> ( Model, Cmd Msg )
init route =
urlUpdate route
{ currentRoute = [ "home" ]
, users = initialUsers
}

Note that we have defined a type named RoutePath, added a currentRoute element to the model and given it an initial value in the init function.

From the Model to the View

Given that we have the current route information in the model, we can use this information to choose what “page” to display in the view. Here is a sample view function that demonstrates this.

view : Model -> Html Msg
view model =
case model.currentRoute of
[] ->
homePage
[ "home" ] ->
homePage
[ "about" ] ->
aboutPage
[ "users" ] ->
usersPage model
[ "users", userId ] ->
userPage model userId
[ "users", userId, "hobbies" ] ->
hobbiesPage model userId
_ ->
notFoundPage

So we just use the information stored in the currentRoute field of our model to determine what to render in the view. Here I have tried to represent each “page” as a separate function that renders HTML. Note that pattern matching within the Elm case statement makes it easy to map URL patterns into the specific function we need to render that page. Here are a couple of examples of these “page rendering” functions.

In the case of a static page, we simply render HTML.

homePage : Html Msg
homePage =
h1 [] [ text "Home" ]

In the case of one of the dynamically generated pages, we use other information in the model to generate the HTML for this page. In the case below, we turn the /users route into a list of the users along with links to the detailed /users/:id page for each of these users.

usersPage : Model -> Html Msg
usersPage model =
div []
[ h1 [] [ text "Users" ]
, ul []
(List.map
(\user ->
li []
[ a
[ href ("/#/users/" ++ toString user.id) ]
[ text user.name ]
]
)
model.users
)
]

So at this point, we have code that can generate any page, but we still don’t know how to have the URL change to update the currentRoute information in the model.

On To Navigation

Looking back at the use of Navigation.program, you’ll notice that this top level program expects two functions. Defining each of these two functions puts all of the pieces together and completes the loop of getting the modified URL information into the model.

  • urlParser
    I’ll admit that there is a bit of magic here. This function extracts the hash portion of the URL and converts that into the Route type we have defined (with help from the fromUrl function which simply drops the ‘#’ portion of the URL and turns it into a list).
urlParser : Navigation.Parser Route
urlParser =
Navigation.makeParser (fromUrl << .hash)
fromUrl : String -> Route
fromUrl url =
let
routeElements =
url
|> String.split "/"
|> drop 1
in
Ok routeElements
  • urlUpdate
    This function takes the Route that was created by urlParser and defines how the model changes with a new URL. You’ll note that it takes a route and a model and returns a new model. In the case of a valid route, it simply stores the new route in the currentRoute field of the model record. In the case of a bad route, we keep the model as is and used the modifyUrl side effect to change the URL to the currently store route.
urlUpdate : Route -> Model -> ( Model, Cmd Msg )
urlUpdate route model =
case route of
Ok routeElements ->
{ model | currentRoute = routeElements }
! []
Err _ ->
model
! [ Navigation.modifyUrl (toUrl model.currentRoute) ]
toUrl : RoutePath -> String
toUrl currentRoute =
"#/" ++ (String.join "/" currentRoute)

Deepening Your Understanding

I’ll admit that I don’t have a completely top to bottom understanding of the urlParser. A lot of the details of that operation are in some native javascript code that lives inside the Navigation package. You can deepen your understanding by looking at the source code at https://github.com/elm-lang/navigation.

Additionally, I have purposely made a very simple, generic urlParser. I just turn the URL into a list and use pattern matching on that list to make decisions. It’s possible to make this function more sophisticated. Evan discusses some possibilities in this repository: https://github.com/evancz/url-parser.

For now, I’m happy with the simple pattern matching I’m using so I haven’t investigated this below the surface level.

A Working Example

You can find a working example of all of this in action in my github repository.

https://github.com/billperegoy/elm-spa

You can clone this repository and you’ll have all of the code you need to experiment with a working version of the single page web app that we’ve discussed in this blog post.

--

--

billperegoy
im-becoming-functional

Polyglot programmer exploring the possibilities of functional programming.