Building a Single Page Application With Elm v17



This blog post briefly describes building of SPA (Single Page Application) with Elm. Kindly note that I used Elm 17 for writing code. Please, see another post for writing SPA with Elm v18.

Application structure

We are building a very simple application. Here is how the App.elm file looks like:

module App exposing (..)

import Navigation
import State
import View

main : Program Never
main = Navigation.program State.urlParser
  {
    init = State.init
  , view = View.rootView
  , update = State.update
  , urlUpdate = State.urlUpdate
  , subscriptions = State.subscriptions
  }

We use Navigation package for implemeting navigation through our SPA. State and View are our another modules where we implement update function and application views respectively.

Model and messages

Model and messages are defined in the Types module. Here is how application messages look like:

type Msg
    = NavigateTo Page
    | NavigateBack

NavigateTo - navigates to corresponding web page
NavigateBack - just navigates one page back

And here is how the model is defined:

type Page
    = Home
    | About
    | Contact
    | NotFound

type alias Model = { page : Page }

Our model defines four kinds of pages: Home, About, Contact, and NotFound. The latter one is used for displaying the “Page Not Found” landing page.

Update

It is a mandatory function for an Elm application.

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

View

All possible views of the application are described in the View module. What we do here is just showing corresponding view according to the active page.

rootView : Model -> Html Types.Msg
rootView model =
  case model.page of
    Home ->
      span []
           [ text "Home page -> "
           , a [ href "/about"
               , Utils.onClick' <| NavigateTo About
               ]
               [ text "About us" ]
           ]
    About ->
      span []
           [ text "About us -> "
           , a [ href "/contact"
               , Utils.onClick' <| NavigateTo Contact
               ]
               [ text "Contact form" ]
           ]
    Contact ->
      span []
           [ text "Contact form -> "
           , a [ href "/home"
               , Utils.onClick' <| NavigateTo Home
               ]
               [ text "Home" ]
           ]
    NotFound ->
      span []
           [ text "Error 404, page not found... "
           , a [ href "/"
               , Utils.onClick' <| NavigateTo Home
               ]
               [ text "Home" ]
           ]

As you can see, every page has its own simple view.

Navigation

Application navigation is implemented basing on Elm.Navigation module.

Navigation.Program which we used in the App.elm module for creating our application, wants to receive parser as its argument. We pass urlParser function that creates corresponding parser for initializing the application.

urlParser : Navigation.Parser Page
urlParser = Navigation.makeParser pathParser

The urlParser function actually passes pathParser function to the Navigation.makeParser. The actual logic of how the location should be parsed is implemeted in the PathParser function.

pathParser : Navigation.Location -> Page
pathParser location =
    let
        pathname = location.pathname
        path =
            if String.startsWith "/" pathname then
                String.dropLeft 1 pathname
            else pathname
    in
        case parse identity pageParser path of
            Err err -> NotFound
            Ok route -> route

pageParser : UrlParser.Parser (Page -> a) a
pageParser =
    oneOf
        [ format Home (oneOf [ s "home", s "" ])
        , format About (s "about")
        , format Contact (s "contact")
        ]

Updating URL

Function urlUpdate is executed when the new location was parsed and the page object is known.

urlUpdate : Page -> Model -> ( Model, Cmd Msg )
urlUpdate page' model = ( { model | page = page' }, Cmd.none )

No hash URLs

Most of the Elm-based application demos use hashes in URL (href objects) for handling navigation. I don’t like this approach since it looks a bit unusual to me. So, here is how I implemented nice URLs without using hash symbol.

Pay attention to the View module. You can see the following line there:

a [ href "/about", Utils.onClick' <| NavigateTo About ] []

Here, for building HTML anchor, I used custom onClick function that is implemented in the Utils module:

onClick' : msg -> Attribute msg
onClick' msg = onWithOptions
                "click"
                { preventDefault = True, stopPropagation = True }
                ( Json.Decode.succeed msg )

The custom onClick function prevents default behavior of the anchor, and for navigating on the link it uses Elm messaging. Using this approach, all application’s URLs look as normal but behave as Elm navigation, web browser also will show «normal» links in the status bar instead of hashes.

Web server configuration

You probably need to configure your web server properly for making this SPA work. This is how NGINX configuration might look like:

server {
    listen 8080;
    root /home/mywebsite;
    index index.html index.htm;
    server_name localhost;
    location / {
        try_files $uri $uri/ /index.html;
    }
}

This way NGINX will not say «Error 404» when you’re accessing the application’s virtual pages.

Source codes

Full source codes of the demo application can be found on my Github page.