5.7

Subscriptions

The last piece of the Elm Architecture puzzle is subscriptions. They allow us to listen to external events such as incoming WebSocket messages, clock tick events, mouse/keyboard events, and geolocation changes.

Like commands and messages, Elm also treats subscriptions as data. When we want to listen to an event, we need to create a subscription with all the relevant information for example, which message to send when the event is triggered and then hand that subscription off to the Elm Runtime. When the event occurs, the runtime will notify our app by sending the message we included in the subscription. Let’s see how this works in practice through a simple example app that increments a counter every time we press a key.

Model

As usual we’ll start with the model. Create a new file called EventListener.elm in the beginning-elm/elm-examples directory and add the code below to it.

module EventListener exposing (..)


type alias Model =
    Int

Once again, our model is just an alias for the Int type. All we need to do is keep track of the number of key press events. Next, we need to create an initial model. Add the following code to the bottom of EventListener.elm.

init : ( Model, Cmd Msg )
init =
    ( 0, Cmd.none )

View

The code for presenting our model to the user is also quite simple. Add the following code to the bottom of EventListener.elm.

view : Model -> Html Msg
view model =
    div []
        [ text (toString model) ]

All we’re doing here is display the model. Import the Html module in EventListener.elm.

module EventListener exposing (..)

import Html exposing (..)
.
.

Message

So far in this chapter, we have been building apps by first defining a model and then writing the bare minimum code necessary for displaying that model to the users. In this section, we’ll change our approach slightly and skip the initial rendering of the model. We’ll wait until all the details have been fleshed out to actually run the app.

We’re interested in listening to an event that gets generated whenever a key is pressed. When that event is created, we want the Elm Runtime to notify our app by sending the KeyPressed message. Let’s add its definition to the bottom of EventListener.elm.

type Msg
    = KeyPressed Keyboard.KeyCode

The KeyPressed message will contain a key code as its payload. Each key in a keyboard is represented by a unique integer. For example, A has the key code value of 65. Similarly, B has 66. The Keyboard module defines a type to represent a key code.

type alias KeyCode =
    Int

Let’s import the Keyboard module in EventListener.elm.

module EventListener exposing (..)

import Html exposing (..)
import Keyboard

We also need to install the package that contains the Keyboard module. Run the following command from the beginning-elm directory in terminal.

elm-package install elm-lang/keyboard

Answer y when asked to update elm-package.json and approve the installation plan.

Update

Next, we’ll handle the KeyPressed message in update. Add the following code to the bottom of EventListener.elm.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        KeyPressed keyCode ->
            ( model + 1, Cmd.none )

When the KeyPressed message arrives, we simply increment the model by 1. Since we’re ignoring the keyCode parameter, we should replace it with _.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        KeyPressed _ ->
            ( model + 1, Cmd.none )

If we weren’t planning on using the keyCode argument, why did we add it as a payload to the KeyPressed message in the first place? Couldn’t we have just defined the message with no payload at all like this:

type Msg
    = KeyPressed

The answer to that question lies in the definition of our subscription.

Subscription

How do we tell the Elm Runtime that we’re interested in listening to a keypress event? We do that by creating a subscription. Add the following code to the bottom of EventListener.elm.

subscriptions : Model -> Sub Msg
subscriptions model =
    Keyboard.presses KeyPressed

The Keyboard module provides a function called presses which is responsible for subscribing to all keypress events. Here’s how its type signature looks:

presses : (KeyCode -> msg) -> Sub msg

The first argument to presses is a function that takes a key code and returns a message. This is why we needed to add Keyboard.KeyCode as a payload in KeyPressed’s definition earlier.

type Msg
    = KeyPressed Keyboard.KeyCode

presses returns a subscription which we need to pass to the Html.program function in main. We’ll define main later.

Like commands, we don’t tend to create subscriptions directly by using some constructor function. Instead, we rely on functions like presses. Here’s another example: let’s say we want to create a subscription for listening to any incoming messages on a WebSocket. We can use the WebSocket.listen function to create that subscription. We just look for an appropriate function in a module and use it to create a subscription.

Notice how the subscriptions function takes a model as its only argument, but doesn’t use that argument in the function body at all. Why did we include an unused argument in the definition? That’s because the Elm Runtime expects the function responsible for creating a subscription to accept a model regardless of whether the model is used or not. Since we aren’t using that argument, we should replace it with _.

subscriptions : Model -> Sub Msg
subscriptions _ =
    Keyboard.presses KeyPressed

Our example app is quite simple and doesn’t use the model to create a subscription. But other apps could use the value contained in our model to create more sophisticated subscriptions.

Wiring Everything Together

We’re now ready to wire everything together. Add the main function to the bottom of EventListener.elm.

main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

By assigning the name of the function that creates a subscription to the subscriptions property in main, we’re asking the Elm Runtime to start listening to the keypress events as soon as the app is initialized.

We’re now ready to test. Run elm-reactor from the beginning-elm directory in terminal if it’s not running already, and go to this URL in your browser: http://localhost:8000/elm-examples/EventListener.elm. You should see a view that looks like this:

Press any alphanumeric key and you should see the counter go up. Elm doesn’t require us to use subscriptions as the name for the function that creates a subscription. We used that name to make our code more readable. All Elm is looking for is a function that accepts a model and returns a subscription. In fact, we don’t even need to create a named function. We could simply assign an anonymous function to the subscriptions property directly like this:

main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Keyboard.presses KeyPressed
        }

That said, it’s cleaner to extract the code for creating a subscription out to a separate function, especially when we want to subscribe to multiple events.

Subscribing to Multiple Events

Let’s extend our app by also listening to a mouse click event. When that event arrives we’ll decrement the counter. The first thing we need to do is add a new message to the Msg type in EventListener.elm.

type Msg
    = KeyPressed Keyboard.KeyCode
    | MouseClicked Mouse.Position

The MouseClicked message will contain the position of the mouse as payload. Position is defined as a record in the Mouse module like this:

type alias Position =
    { x : Int
    , y : Int
    }

Let’s import the Mouse module in EventListener.elm.

module EventListener exposing (..)
.
.
import Keyboard
import Mouse

We also need to install the package that contains the Mouse module. Run the following command from the beginning-elm directory in terminal.

elm-package install elm-lang/mouse

Answer y when asked to update elm-package.json and approve the installation plan. Next, we’ll handle the MouseClicked message by adding a new branch to the update function.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        KeyPressed keyCode ->
            ...

        MouseClicked position ->
            ( model - 1, Cmd.none )

When the MouseClicked message arrives, we simply decrement the model by 1. Similarly to the KeyPressed message, we’re ignoring the position argument. It’s better to replace any unused parameters with _.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        KeyPressed keyCode ->
            ...

        MouseClicked _ ->
            ( model - 1, Cmd.none )

The only thing remaining is to create a subscription. Modify the subscriptions function like this:

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Keyboard.presses KeyPressed
        , Mouse.clicks MouseClicked
        ]

We can use the Sub.batch function defined in the Platform.Sub module to batch multiple subscriptions. Here’s how its type signature looks:

batch : List (Sub msg) -> Sub msg

It’s interesting to note that the return type of the subscriptions function didn’t change at all even though we’re now returning multiple subscriptions. Refresh the page at http://localhost:8000/elm-examples/EventListener.elm and you should be able to decrement the count by clicking anywhere on the page.

Summary

In this section, we learned how to use subscriptions to listen to various events. Subscriptions also cause side effects. That’s why we have to let the Elm Runtime manage them. Here is how the Elm Architecture looks with the introduction of subscriptions:

The interaction between the Elm runtime and our code is slightly different now that we’re using subscriptions. The sequence diagram below shows that interaction.

Here is the entire code from EventListener.elm:

module EventListener exposing (..)

import Html exposing (..)
import Keyboard
import Mouse


type alias Model =
    Int


init : ( Model, Cmd Msg )
init =
    ( 0, Cmd.none )


view : Model -> Html Msg
view model =
    div []
        [ text (toString model) ]


type Msg
    = KeyPressed Keyboard.KeyCode
    | MouseClicked Mouse.Position


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        KeyPressed _ ->
            ( model + 1, Cmd.none )

        MouseClicked _ ->
            ( model - 1, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.batch
        [ Keyboard.presses KeyPressed
        , Mouse.clicks MouseClicked
        ]


main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }
Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close