For several reasons1 we’re moving away from Django to a separate front- and back-end. The back-end is written in Erlang, based on 99s’ excellent Cowboy, and provides a REST API. The front-end, which uses that REST API, is written in Elm.

While the Elm website has lots of great examples, none of them show how to build such a front-end. This is the first part of a series detailing how we’ve gone about it. These posts assume only a basic understanding of Elm (i.e. syntax and signal fundamentals). I’ll try to explain everything else. Please leave a comment if anything is unclear.

Table of Contents

The Example

We’re going to build a music database (think of a stripped down Discogs). We’ll start simple and over the series build up to a full single-page application.

This first post shows how to retrieve JSON data using a GET request, how to decode that data, and how to use it to populate a page.

The example data is the following list of artists:

[ {"id": 1, "name": "Juan Atkins"}
, {"id": 2, "name": "Larry Heard"}
, {"id": 3, "name": "Marcos Valle"}
, {"id": 4, "name": "Arthur Russell"}
, {"id": 5, "name": "Charles Mingus"}
, {"id": 6, "name": "Nils Frahm"}
, {"id": 7, "name": "Sun Kil Moon"}
, {"id": 8, "name": "Sleaford Mods"}
]

Basic Application Structure

We use the standard Elm architecture (aka Model/Update/View), where:

  • the model holds the state of the application
  • the update function details how a predefined set of actions change the model
  • the view function renders the model

Model

First we define the following type alias:

type alias Artist =
  { id: Int
  , name: String
  }

An Artist value is a record with two fields:

  1. a field named id that has type Int
  2. a field named name that has type String

Next we use that type alias to define a Model type alias:

type alias Model =
  List Artist

Thus our model is a list of artists; we initialize it using the empty list:

init : Model
init =
  []

Update

To begin we define only one action:

type Action
  = NoOp

And that action does nothing:

update : Action -> Model -> Model
update action model =
  case action of
    NoOp ->
      model

Later we’ll see why this action is useful.

View

For now we stub the view function:

view : Model -> Html
view model =
  div [] [text "Loading"]

Wiring Everything Together

To wire everything together we need three things:

  1. a main function of type Signal Html
  2. a Signal Model
  3. a mailbox we can send action updates to

We look at these in reverse order.

Mailbox

The mailbox is created using Signal.mailbox as follows:

actions : Signal.Mailbox Action
actions =
  Signal.mailbox NoOp

A Signal.Mailbox acts as a communication center, its type alias is defined as follows:

type alias Mailbox a =
  { address : Address a
  , signal : Signal a
  }

Thus a mailbox of type a (Action in our case) is a record with the following two fields:

  • an Address where we can send messages to
  • a Signal of messages sent to the mailbox

Now we see why the NoOp action is useful: Signal.mailbox requires a default value for the signal.

Signal Model

Next we will use Signal.foldp to create a so-called past-dependent signal of the model. It has the following type signature:

foldp : (a -> state -> state) -> state -> Signal a -> Signal state

Replacing the type variables a and state with the actual types we’re using gives:

foldp : (Action -> Model -> Model) -> Model -> Signal Action -> Signal Model

We must give foldp three arguments:

  1. a function that takes an action and a model, and returns a model
  2. a model
  3. a signal action

The third argument represents the incoming signal, we use the signal of the mailbox defined above (i.e. actions.signal). Each update of that signal is used to step the state of the application forward. This is done by applying the first argument (the update function) to the current state. The whole thing is bootstrapped using the second argument (the initial model init).

The resulting definition:

model : Signal Model
model = Signal.foldp update init actions.signal

The outgoing signal represents the current application state.

Main

Finally, for our main function, we use Signal.map to apply the view function to the model signal:

main : Signal Html
main =
  Signal.map view model

Retrieving and Decoding the Data

Http.get has the following type signature:

get : Json.Decode.Decoder value -> String -> Task Error value

Since its first parameter is a JSON decoder we look at the decoding first.

Decoding the Data

We define the following artist data decoder:

artist : Json.Decode.Decoder Artist
artist =
  Json.Decode.object2 Artist
    ("id" := Json.Decode.int)
    ("name" := Json.Decode.string)

This decoder uses Json.Decode.object2 to extract two fields from a JSON value (id and name) and then constructs an Artist value.

As mentioned above our API returns an array of artists, so in our request we will use Json.Decode.list artist to extract a List Artist from the JSON.

Retrieving the Data

The next step is to get the data: the second parameter of Http.get represents the URL of the request (/api/artists in this case).

We’re now ready to define our request as follows:

get : Task Http.Error (List Artist)
get =
  Http.get (Json.Decode.list artist) "/api/artists"

Contrary to what you might think this code does not execute an actual request, for that you need to hand it to a port (a so-called “runner”):

port runner : Task Http.Error (List Artist)
port runner =
  get

Now the above code does execute the request, but it does not update our model. For that we will need a new action SetArtists (List Artist), the update function needs a case for that action, and we need to send that action to our actions mailbox.

So we add SetArtists to our Action type:

type Action
  = NoOp
  | SetArtists (List Artist)

Add a case-clause to the update function:

update : Action -> Model -> Model
update action model =
  case action of
    NoOp ->
      model

    SetArtists model' ->
      model'

Remember that our model is simply a list of artists.

And finally we use Task.andThen to send that action to the mailbox:

port runner : Task Http.Error ()
port runner =
  get `andThen` (SetArtists >> Signal.send actions.address)

andThen takes a task of type Task x a and a function of type a -> Task x b, and returns a task of type Task x b.

Let’s look at the second argument in more detail. We use function composition (>>) to combine SetArtists and Signal.send actions address. The result is a function that takes a List Artist, uses that list to construct a Action, and sends that action to the actions mailbox, thus updating our model.

And yes, we’re totally ignoring errors2.

Populating the Page

Now that our model has been updated, showing the list of artists is pretty straightfoward. Using Bootstrap and given open imports of the Html and Html.Attributes modules:

view : Model -> Html
view model =
  let th' field = th [] [text field]
      tr' artist = tr [] [ td [] [text <| toString artist.id]
                         , td [] [text <| artist.name]
                         ]
  in
    div [class "container"]
    [ table [class "table table-striped table-bordered"]
      [ thead [] [tr [] (List.map th' ["ID", "name"])]
      , tbody [] (List.map tr' model)
      ]
    ]

Code

The full code is on GitHub:


  1. Detailed in a probably-never-to-be-written post.

  2. More on those in a future post.