Elm - The Elm Architecture

Elm program

Elm program is set up using Browser module from elm/browser package. There are several functions to do that based on the use case.

  • sandbox - Program that interacts with the user input but cannot communicate with the outside world.
  • element - HTML element controlled by Elm that can talk to the outside world (e.g. HTTP requests). Can be embedded into a JavaScript project.
  • document - Controls the whole HTML document, view controls <title> and <body> elements.
  • application - Creates single page application, Elm controls not only the whole document but also Url changes.

JSON

It is very common to use JSON format when communicating with different APIs. In JavaScript, JSON is usually turned into a JavaScript object and used within the application. However, this is not the case in Elm since we have a strong type system. Before we can use JSON data, we need to convert it into a type defined in Elm. There is the elm/json package for that.

Decoders

Elm use decoders for that. It is a declarative way how to define what should be in the JSON and how to convert it into Elm types. Functions for that are defined in Json.Decode module.

For example, we have this JSON representing a TODO:

{
    "id": 24,
    "label": "Finish the home",
    "completed": false
}

To get the label field, we can define a decoder like this:

import Json.Decode as Decode

labelDecoder : Decode.Decoder String
labelDecoder =
    Decode.field "label" Decode.string

There are functions to decode other primitives, like bool or int. However, we usually need more than just one field. We can combine decoders using map functions from Json.Decode module, e.g. map3.

import Json.Decode as Decode

map3 :
    (a -> b -> c -> value)
    -> Decode.Decoder a
    -> Decode.Decoder b
    -> Decode.Decoder c
    -> Decode.Decoder value

We can then define our own type for TODO and a decoder.

import Json.Decode as Decode

type alias Todo =
    { id : Int
    , label : String
    , completed : Bool
    }

todoDecoder : Decode.Decoder Todo
todoDecoder =
    Decode.map3 Todo
        (Decode.field "id" Decode.int)
        (Decode.field "name" Decode.string)
        (Decode.field "completed" Decode.bool)

There is a package NoRedInk/elm-json-decode-pipeline for more convenient JSON decoders. It is especially useful for large and more complex objects. We could rewrite the previous example using pipeline:

import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline as Pipeline

type alias Todo =
    { id : Int
    , label : String
    , completed : Bool
    }

todoDecoder : Decode.Decoder Todo
todoDecoder =
    Decode.succeed Todo
        |> Pipeline.required "id" Decode.int
        |> Pipeline.required "name" Decode.string
        |> Pipeline.required "completed" Decode.bool

It is not that big change in this case, however, we only have map8 function in Json.Decode so this library comes handy if we need more. Moreover, it has other functions to define for example optional or hardcoded values.

Encoders

When we want to send something to an API we need to do the opposite – turn the Elm value into JSON value. We use functions from Json.Encode package for that. There is a type called Value which represents a JavaScript value and functions to convert Elm primitives, lists and objects into Value type.

Here’s an example using the TODO from decoders example.

import Json.Encode as Encode

type alias Todo =
    { id : Int
    , label : String
    , completed : Bool
    }

encodeTodo : Todo -> Encode.Value
encodeTodo todo =
    Encode.object
        [ ( "id", Encode.int todo.id )
        , ( "label", Encode.string todo.label )
        , ( "completed", Encode.bool todo.completed )
        ]

The object is representend as a list of key value tuples.

Http

There is Http module in elm/http package for making HTTP requests in Elm. The functions creating requests create a command for Elm runtime which defines what request should be made, what is the expected response and what message should be send to update function when the request is done.

Here is an example for getting TODO using the decoder defined in previous section.

import Http

type Msg =
    GotTodo (Result Http.Error Todo)

getTodo : Cmd Msg
getTodo =
    Http.get
        { url = "http://example.com/todo"
        , expect = Http.expectJson GotTodo todoDecoder
        }

The function getTodo creates a command with HTTP request that expect JSON to be returned and uses todoDecoder to get Todo type from the returned JSON. Once the request is finished, we get GotTodo message containing the Result with either Http.Error if the request failed or Todo if the request was successful.

There are other functions we can use for expected response like expectString to get the string as is or expectWhatever when we don’t really care about the response as long as it’s ok.

When we want to do a POST request we also need to define the body. Here’s an example of posting TODO to the server, using encoder function from previous section.

import Http

type Msg =
    TodoSaved (Result Http.Error ())

postTodo : Todo -> Cmd Msg
postTodo todo =
    Http.post
        { url = "http://example.com/todo"
        , body = Http.jsonBody <| encodeTodo todo
        , expect = Http.expectWhatever TodoSaved
        }

Of course, we can send different types of body, not just JSON, e.g., stringBody for plain string or emptyBody when we don’t want to send anything.

When we want to do a different type of request than GET and POST or we want to set headers, we need to use Http.request function (Http.post and Http.get are actually just a shorthand for calling Http.request).

request :
    { method : String
    , headers : List Http.Header
    , url : String
    , body : Http.Body
    , expect : Http.Expect msg
    , timeout : Maybe Float
    , tracker : Maybe String
    }
    -> Cmd msg

Subscriptions

Subscriptions are used to tell Elm that we want to be informed if something happend (e.g., web socket message or clock tick).

Here’s an example of subscriptions defining that a message Tick with current time should be send to update function every 1000 milliseconds.

import Time


type alias Model =
    ()

type Msg =
    Tick Time.Posix


subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every 1000 Tick

Materials

Further Reading

Forms

Form elements are created the same way as other HTML elements using functions from Html module and attributes from Html.Attributes module from elm/html package.

We can use onInput from Html.Events module to detect input events and create a message for our update function.

The loop is the following:

  • user changes the value in an input field
  • a new message is created
  • the update function is called with the message and it updates the model with the new value
  • input field is re-rendered with the new value

Here is a simple example with a single input field:

import Browser
import Html exposing (Html)
import Html.Attributes as Attributes
import Html.Events as Events

main : Program () Model Msg
main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

type Msg =
    NameChanged String

type alias Model =
    { name : String }

init : Model
init =
    { name = "" }

update : Msg -> Model -> Model
update msg model =
    case msg of
        NameChanged newName ->
            { model | name = newName }

view : Model -> Html Msg
view model =
    Html.input
        [ Attributes.placeholder "Your name"
        , Attributes.value model.name
        , Events.onInput NameChanged ]
        []

When we need more complex forms in our application, there are packages to handle forms like etaque/elm-form.

Random

There is a Random module in elm/random package for generating pseudo-random values in Elm. It defines a type called Generator which can be think of as a recipe for generating random values.

Here is a definition of genrator for random numbers between 1 and 4.

import Random exposing (Generator)

randomGrade : Generator Int
randomGrade =
    Random.int 1 4

If we want to use it, we have two options. The first is using generate function to create a command. Then we got the generated value back with a defined message to our update function. Here’s an example:

import Random exposing (Generator)


type Msg = NewGrade Int

generateGrade : Cmd
generateGrade =
    Random.generate NewGrade randomGrade

The other option is to use step functions. It requires the generator and also a Seed and returns a tuple with generated value and a new Seed. The initial seed can be hardcoded (but then the generated values are same each time we run the application), send to Elm via Flags (we’ll cover those in the next lesson) or using generate function first to get the seed and then use it to generate other random values.

import Random exposing (Seed)


generateGrade : Seed -> (Int, Seed)
generateGrade seed =
    Random.step randomGrade seed

Licenses and Attributions


Speak Your Mind

-->