Skip to main content

Building a single page application with Elm

·20 mins

Hi, its been a very long while since I last blogged, I promised this ages ago, its a partial sample chapter for a book I was planning on Elm but the publisher started to muck me about so it never happened.

I also have the table of contents for the rest of the book but I guess your not interested in a book Im not writing so I have omitted that. Anyway heres the sample chapter in its raw form.

Building a single page application #

In this chapter we will specify and build a single page application consisting of bootstrap based navbar navigation with a selection of views that are shown based on the navigation option selected within the applications model.

Basic Structure #

The basic structure of the application we will be constructing comprises a home, about, contact, gallery and item view as follows:

Home View #

This will contain a basic navigation bar at the top with the contents reflecting the current navigation selection:

Navigation bar: [About|Gallery|Contact]
<Main content view, this is based on the current navigation selection>
<fixed footer>

About View #

The About view is simply a page displaying details, like a description and image.


The gallery view is a list of categories showing an image and description for each. Clicking on the image result in navigating to the Item View, theres also a back button to navigate back to Home.

 <fixed description text>
 Category1 image
 Category2 image
 Category3 image
 <back button>

Contact View #

The contact view contain some text and links to various social media, and a link to navigate back to Home.

<social media link1>
<social media link2>
<back button>

Item View #

The item view contains a image and descriptive text as well as a means to navigate back to the Gallery.

<back button>

The basic structure of this single page application is relatively simple and also follows The elm architecture as you would expect.

This single page application will be split into files loosely based on How i structure elm apps by Kris Jenkins.

├─ App.elm
├─ State.elm
├─ Types.elm
├─ View.elm

The main application startup will be hosted in App.elm. Application state and models will be contained within State.elm. Types.elm will contain the various types that we will be using in the application. Finally the initial view visible to the user will be contained in View.elm.
In addition each view in the application can be given its own module and file nested in the file structure as follows:

├─ About
│  └─ View.elm
├─ Category
│  └─ View.elm
├─ Contact
│  └─ View.elm
├─ Detail
│  └─ View.elm
├─ Gallery
│  └─ View.elm
└─ Home
   └─ View.elm

Although this single page application is relatively simple and could be built by any one of the many static site engines like Ghost, Hugo or Jekyll it builds on the earlier chapters slowly adding complexity so you can see where things would lead to on a bigger site more complex site where you have additional requirements like web sockets etc.

Core structure #

First of all lets look at how we can construct the main entry point of the application, let’s create the following skeleton for App.elm:

module Main exposing (..)

type alias Model =
    { page : string }

init =
    { page = "Home" }

view model =
  div [] []

update msg model =
    (model, Cmd.none)

subscription model =

main : Program Never
main =
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions

All the main elements of the application are imported and a Html.program is started with the appropriate init, view, update, and subscriptions.
Lets start to flesh things out further by further defining the model and other related types.

Model, Messages and types #

In this section we will be describing the model which represents the applications state as runtime, the messages used within the application and also any types that we might need to represent in the application too.

Model #

At the moment the model is really simple, its just a record with a single field page which is a string. Thats not a very robust way of defining the model so lets see about changing that now.

The current page of the application is fairly well defined its either going to be one of the following pages:

  • Home - the default home page
  • About - a page which displays information about the site
  • Contact- a page which displays contact details after first exposing a captcha request
  • Gallery - a page which shows the user different categories that are available to view
  • Gallery Category - a page which shows a list of items available in a a category
  • Item Detail - a page showing details on a single item

We can model the concept of a page really well with a union type:

type Page
    = Home
    | About
    | Contact
    | Gallery
    | CategoryDetail String
    | ItemDetail String

Home, About, Contact and Gallery are depicted by empty types with no specific shape but CategoryDetail and ItemDetail have the shape or type of a string. Come to think of it CategoryDetail is very loosely typed being represented by a simple string, we can tighten that up too as the set of categories is also well defined. Lets just define just three categories for now:

type CategoryDetail
    = Seasides
    | IllustratedQuotes
    | Architecture```

The same cannot be said about ItemDetail as thats just going to be a key to the items name, it could equally be a number but lets keep that as a simple string for now. Lets update the Page type to take those new types into account:

type Page
    = Home
    | About
    | Contact
    | Gallery
    | CategoryDetail CategoryDetail
    | ItemDetail String

We can now also update the Model so that the field page is represented by the Page type:

type alias Model =
    { page : Page }

Messages #

The primary messages that will be used will be either no navigate back a page or to navigate to a specific page. There will also be a message to indicate we want to use a captcha to view an email address to avoid an email address being exposed to crawlers. Lets define these three messages again using union types:

type Msg
    = NavigateTo Page
    | NavigateBack
    | Captcha
  • NavigateTo - navigates to the page detailed.
  • NavigateBack - navigates back one page.
  • Captcha - displays a captcha request which then shows an email address on success.

More types #

In the application we will also need types to represent the information about the categories detail and Item detail, we can use records to do this with simple string fields to represent textual information and images. Category will have a categoryType, img and description:

type alias Category =
    {categoryType : CategoryType, img : String, description : String}

Item will have an id, title, img, description and category:

type alias Item =
    {id : String, title : String, img : String, description : String, category : CategoryType}

All of the field types are simple types apart from category which is also a CategoryType which we defined above.

View #

The View is quite simple based on a standard Bootstrap 4 Navbar navigation. We can define it by adapting some standard bootstrap html:

<nav class="navbar navbar-light bg-faded">
  <a class="navbar-brand" href="#">Navbar</a>
  <ul class="nav navbar-nav">
    <li class="nav-item active">
      <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
    <li class="nav-item">
      <a class="nav-link" href="#">Link</a>
    <li class="nav-item">
      <a class="nav-link" href="#">Link</a>
    <li class="nav-item dropdown">
      <a class="nav-link dropdown-toggle" href="" id="supportedContentDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
      <div class="dropdown-menu" aria-labelledby="supportedContentDropdown">
        <a class="dropdown-item" href="#">Action</a>
        <a class="dropdown-item" href="#">Another action</a>
        <a class="dropdown-item" href="#">Something else here</a>

We can define the following rootView function:

rootView model =
    div [ class "container" ]
        [ nav [ class "navbar navbar-light", attribute "role" "navigation" ]
            [ a
                [ class "pull-xs-left"
                , href <| toHash Home
                , onClick_ <| NavigateTo Home
                [ img [ id "logo", class "img-fluid", src "/img/logogreen.png", srcset [ "/img/logogreen.png", "/img/logogreen@2x.png" ] ] [] ]
            , button
                [ attribute "aria-controls" "exCollapsingNavbar2"
                , attribute "aria-expanded" "false"
                , attribute "aria-label" "Toggle navigation"
                , class "navbar-toggler hidden-sm-up flex-center"
                , attribute "data-target" "#exCollapsingNavbar2"
                , attribute "data-toggle" "collapse"
                , type' "button"
                [ text "☰" ]
            , div [ class "collapse navbar-toggleable-xs", id "exCollapsingNavbar2" ]
                [ ul [ class "nav navbar-nav pull-sm-right text-xs-center" ]
                    [ renderMenuItem model Home "Home"
                    , renderMenuItem model About "About"
                    , renderMenuItem model Gallery "Gallery"
                    , renderMenuItem model Contact "Contact"
        , div [ class "content container-fluid" ] [ viewPage model ]

Theres are several functions here which we have not seen before: toHref, onClick_, srcset, renderMenuItem and viewPage, lets go though them now.

Converting pages to hrefs #

When we want to navigate we can either use a href node or send a command to navigate via the Elm architecture. If we want to navigate using href then we need some way to convert our representation of a page into something that can be represented in the url in the browser navigation:

toHref : Page -> String
toHash page =
    1case page of
        Home ->

        About ->

        Contact ->

        Gallery ->

        CategoryDetail categoryType ->
            3"/category/" ++ toString categoryType

        ItemDetail name ->
            "/item/" ++ name

The case statement is used to start pattern matching on the page type, we will have a separate statement to handle each case in the page union type. For a simple page like About, we return a string that represents a simple path. For a more complex page like CategoryDetail we have to combine elements from the union type to build a path. Here you can see that the categoryType is concatenated not the string “category/”. The function toString is used to convert categoryType into a string as it is also a union type.

onClick event handling #

The astute reader may have noticed the onClick_ with a trailing underscore which is different to the normal definition of onClick. The onClick event is also working hand in hand with the href to allow us to navigate with our own event handler. We define a slightly different onClick as follows:

onClick_ : a -> Attribute a
onClick_ msg =
    onWithOptions "click" { stopPropagation = True, preventDefault = True} (succeed msg)

Whats happening here is we are using onWithOptions to define an alternative onClick thats stops the propagation of the event and also overrides the default browser behaviour. If we did not do this then clicking on the link would result in the browser trying to open the href which would result in a page not found 404 error as the page does not actually exist, rather it is generated by the Elm architecture during the navigation and updating of the application.

Multiple images sources based on pixel density #

srcset is an html 5 img attribute that can be used to apply different images depending on the native resolution of the browser, [][6] describes it as follows:

A list of one or more strings separated by commas indicating a set of possible image sources for the user agent to use. Each string is composed of: a URL to an image, optionally, whitespace followed by one of: a width descriptor, or a positive integer directly followed by ‘w’. The width descriptor is divided by the source size given in the sizes attribute to calculate the effective pixel density. a pixel density descriptor, which is a positive floating point number directly followed by ‘x’.

We can define srcset is defined as follows:

srcset : List String -> Attribute a
srcset items =
        maps =
            1items |> List.indexedMap (\i item -> item ++ " " ++ toString (i + 1) ++ "x")
        property "srcset" (2maps |> String.join "," >> Json.Encode.string)

An indexed map is applied to each of the strings in items, we use the index to define the pixel density descriptor. The mapped strings are joined back together with String.join and encoded as a Json string. So in the view where we see:

srcset ["/img/logogreen.png","/img/logogreen@2x.png" ]

we will get the following attribute:

<img srcset="/img/logogreen.png 1x,/img/logogreen@2x.png 2x>

Defining navigation and changing the appearance #

renderMenuItem is changing a navigations item’s style based on the current page and also defines the navigation to a specific page:

renderMenuItem : Model -> Page -> String -> Html.Html Msg
renderMenuItem model navigationPage txt =
        liClass =  //1
            (if == navigationPage
             then "nav-item active"
             else "nav-item")

        textElement = //2
            if == menuItem
            then [ text txt
                 , span [ class "sr-only" ]
                     [ text "(current)" ]
            else [text txt]
    li [class liClass]
        [ a [ class "nav-link specialEliteFont"
            , href (toHref navigationPage), onClick_ <| NavigateTo navigationPage //3
  1. Here we define liClass to be either nav-item active if the current page is equal to menuItem

  2. if the current page is equal to menuItem We define a textElement which will have it an extra span with the class sr-only (Screen Reader only) defined and the text “(Current)”. If it is not the current page then we just use the text. This is so that screen readers will have an indication of what navigation option is active as an accessibility aid.

  3. Notice the onClick_ event we defined along with the NavigateTo message we defined the the Messages section.

Rendering the sub views #

The sub view is shown underneath the navigation menu:

| Navigation |
|            |
|  sub view  |

We render the subview with viewPage, the view is updated depending on which page is current:

viewPage model =
    case of
        Home -> getHomePage ()
        About -> getAboutPage ()
        Gallery -> getGalleryAsCards ()
        Contact -> getContactPage () 
        CategoryDetail category -> getCategoryPageCards category
        ItemDetail item -> getItemPage item

You can see there is a separate view for each page which in return a list of nodes for that particular view.

For each of these sub views we can create a separate module and import the function into View.elm.

Any of the parameterless pages could be defined very simply, heres an example of what Home.View could look like:

module Home.View exposing (..)
import Html exposing (br, div, Html, img, p, text)
import Html.Attributes exposing (class, src)

getAboutPage : () -> Html Msg
getAboutPage () =
    div [ class "container-fluid" ] [ text “About" ]

We create that file in the Home folder and name the file View.Elm. Remember from Chapter 3 than Elm enforces naming of modules and file names to coincide with the name of the file and module name. So we have to ensure we have a file named View.elm in a folder named Home. The module name also has to be Home.View, don’t worry the elm compiler will call you out if you get anything wrong.
We can import the module and function into View.elm by adding an import statement to the top of the file:

import Home.View exposing (getHomePage)

Heres a slightly more advanced example using one of the pages CategoryDetail with parameters Category.View:

module Category.View exposing (getCategoryPageCards)

import Html exposing (a, br, div, Html, img, p, text)
import Html.Attributes exposing (alt, class, href, name, src)

getCategoryPageCards category =
        items =
            List.filter (\c -> c.category == category) Data.items
        colClass =
            case List.length items of
                1 -> "col-xs-12"
                2 -> "col-xs-12 col-sm-6"
                _ -> "col-xs-12 col-sm-6 col-md-4"

        itemMapper item =
            div [ class colClass ]
                [ div [ class "card"]
                    [ a [ noContextMenu
                        , href (toHref <| ItemDetail
                        , onClick_ (NavigateTo <| ItemDetail
                        [ img [ noContextMenu, class "card-img-top img-fluid", src item.img ] [] ]
                    , div [ class "card-block" ]
                        [ p [ class "card-text" ] [ text item.title ] ]
        div [ class "container-fluid" ]
            [ div [ class "row" ]
                (items |> itemMapper)
            , div [] [ backButton ]

The nodes returned from getCategoryPageCards are returned as part of viewPage. Data.items are filtered by the current category and mapped into divs with onClick_ navigation to the ItemDetail page.

Update #

The purpose of the update function is to update our model in relation to external events, in this instance its going to be mainly navigation oriented so the update is rather simple.

The update function is defined as follows:

update : Msg -> Model -> ( Model, Cmd b )
update msg model =
    case msg of
        1NavigateTo page ->
            ( model, (Navigation.newUrl <| pageToString page) )

        2NavigateBack ->
            model => (Navigation.back 1)

        3Captcha ->
            ( model, captcha() )

We pattern match on the msg parameter and use the commands described in the Messages section.
For the NavigateTo command we update the model to the new page. For navigating back we use a command from the Navigation package to navigate back exactly one page. For the Captcha command we run another function to do that work for us, the captcha function.
The captcha function is defined as Port defined like this:

port captcha : () -> Cmd msg

We then need to add a little JavaScript subscription to the port which opens a new url with the captcha verification :

app.ports.captcha.subscribe(function () {'', '', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=500,height=300');
    return false;

Whats happening here is that when the captcha message is received the JavaScript subscriber is notified and a a window is opened allowing the email address to be retrieved if the captcha is successful.

Navigation or routing as it is sometimes called in single page applications is the managing of the browser address bar without creating new requests to servers etc. This is done internally via Html 5 push state.

Navigation in Elm is handled by the Elm Navigation package. This package provides an alternative program Navigation.program which means the one we defined in Core structure now needs to be altered. The program function in Elm Navigation has been extended with an additional two extra arguments. The main program entry point needs to be modified to look like this:

main : Program Never
main =
    Navigation.program (Navigation.makeParser pathParser)
        { init = nit
        , view = view
        , update = update
        , urlUpdate = urlUpdate
        , subscriptions = subscriptions

The first additional argument is a Parser, there is a utility function called makeParser in the Navigation package that allows us to define a function to turn a browser Location into whatever data we want to:

makeParser : (Location -> a) -> Parser a

In the Navigation.program above you can see this used along with the pathParser function below to parse a Location into a Page:

Navigation.program` (Navigation.makeParser pathParser)

## Parsing

Parsing is handled with a parser combinator library defined in the url-parser package. There are other parser combinator libraries which add more functionality to url-parser but it has everything you need for most situations. We covered combinators in chapter 7 Interoperability and this is very much the same concept of combining small functions to create more complex functions and behaviour. We will only be using a small selection of combinators to parse the results:

  • oneOf is a combinator that will try to match one of the parsers in a list.
  • s is a string combinator matching a particular string like “home”, “shop” etc.
  • </> is a combinator that matches a / character in the location like item/myitem.
  • UrlParser.string matches any string.
  • format Is a combinator that allows you to customise or map another Parser, here it is used to Parsed output into the union types that represent them.
pathParser : Navigation.Location -> Result String Page
pathParser location =
    parse identity pageParser (String.dropLeft 1 location.pathname)

The pathParser functions first parameter is a function to map the successful parsing to another type here we are using the identity function to leave the result as is. The String.dropLeft 1 function is removing the leading character from location.pathname which is the leading forward slash.

pageParser : UrlParser.Parser (Page -> a) a
pageParser =
        [1format Home (oneOf [ s "home", s "" ])
        ,2 format About (s "about")
        , format Shop (s "shop")
        , format Gallery (s "gallery")
        , format Contact (s "contact")
        ,3 format (stringToCategoryType >> CategoryDetail) (s "category" </> UrlParser.string)
        , format ItemDetail (s "item" </> UrlParser.string)

The format function is part of the navigation package and is simply a function to map the parsing result in if successful, in this instance we are using the Page union type constructors to perform that map. The combinators oneOf is used to choose between several in the preceding list, in this instance s is used to match the strings home and an empty string.
Again format is used to construct a map to the About Page. The s combinator is again use to match the string “about” This time format is used with an extra function stringToCategoryType. As shown below, . The combinators used here are the s combinator to match the string “category”, the </> the forward slash combinator and finally UrlParser.string which matches any string. e,g, category/Seasides

stringToCategoryType : String -> CategoryType
stringToCategoryType category =
    case category of
        "Seasides" ->

        "IllustratedQuotes" ->

        _ ->

To match the category in the url back to a CategoryType we match the corresponding string representation back into a CategoryType. Finally now that we know how parsing work we can look at the urlUpdate to see how it works:

urlUpdate : Result a Page -> Model -> ( Model, Cmd c )
urlUpdate result model =
    case result of
        Err _ ->
            ( model, Navigation.modifyUrl (pageToString )

        Ok page ->
            { model | page = page } => updateAnalytics (pageToString page)

We pattern match on the result witch is a Result type and if its the Ok case then we update the models page to the one passed in. If the result is an error (Err) then we modify the url with Navigation.modifyUrl just pointing it back to the previous page. pageToString simply turns the Page type back into a string. I’m going to strategically ignore updateAnalytics for now as this will be covered in the next section.

Google Analytics integration #

Google analytics can be easily added to any web application be simply creating an account and including the following JavaScript in your html and replacing UA-12345678-1 with your own id:

    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    ga('create', 'UA-12345678-1', 'auto');
    ga('require', 'linkid');
    ga('send', 'pageview');

The problem with this solution is that theres only one real page in the application, it would be really nice if the navigation in this application could show correctly. We need to provide a way to update Google analytics whenever the page navigation changes. Luckily Google provides a way to do this via the ga('set', 'page', page) and ga('send', ‘pageview') JavaScript functions. We can do this by defining another port. Remember the updateAnalytics function from urlUpdate above?

{ model | page = page } => updateAnalytics (pageToString page)

Well thats the port we are going to define now, it looks like this:

port updateAnalytics: String -> Cmd msg

Now all we need to do is wire up a little more JavaScript so that when the updateAnalytics function is called the JavaScript subscriber is notified and Google analytics is updated correctly.

The JavaScript looks like this:

app.ports.updateAnalytics.subscribe(function (page) {
    ga('set', 'page', page);
    ga('send', 'pageview');

Summary #

We have covered quite a range of different aspects in this chapter:

  • Setting up a project with a structure that supports a lot more expansion rather than having thousands of lines crammed into a single file.
  • We used a model and type based approach to model the navigation and pages with the application
  • We learned how Navigation works in single page applications
  • Used Html 5 img srcset attribute to apply different images based on pixel density
  • Added custom events to override default browser behavior on navigation to a url
  • Learned how to use parser combinators to parse url fragments
  • Used ports to communicate with JavaScript
  • Solved a problem with the correct navigation been shown in single page apps analytics

Final word #

I hope this blog post has been a useful read to someone, as I said I wrote it as partial sample chapter for a publisher but they mucked me about so I though it was better off being on my blog rather than gathering virtual dust.

Thanks for reading

Until next time!