8.3

Subscriptions

In this section, we’ll learn how a subscription works. Subscription is the last piece of the Elm Architecture puzzle. It’ll also come handy for receiving data from JavaScript in the next section.

Subscriptions allow us to listen to external events such as incoming WebSocket messages, clock tick events, mouse/keyboard events, geolocation changes, and an output generated by a JavaScript library.

When we want to listen to an event all we have to do is create a subscription that specifies the type of event and which message to send to the update function when that event is triggered. We then hand that subscription over to the Elm runtime and wait for the event to occur. The runtime figures out how to listen to that event. All we need to do is handle the message it sends to our app. Let’s see how this works in practice through a simple example app that increments a counter every time a key is pressed.

Model

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

module EventListener exposing (Model)


type alias Model =
    Int

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 (String.fromInt model) ]

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

module EventListener exposing (Model)

import Html exposing (..)
.
.

Message

Whenever a key is pressed an event is generated. We want the Elm runtime to notify us about that event by sending the KeyPressed message. Let’s add its definition to the bottom of EventListener.elm.

type Msg
    = KeyPressed

We aren’t interested in knowing which key was pressed. That’s why KeyPressed doesn’t have a payload. Later we’ll learn how to listen to a specific key press event.

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 ->
            ( model + 1, Cmd.none )

When the KeyPressed message arrives, we simply increment the model by 1.

Subscription

Now we need to tell the Elm runtime to listen to a key press event. We can do that by creating a subscription. Add the following code to the bottom of EventListener.elm.

subscriptions : Model -> Sub Msg
subscriptions model =
    onKeyPress (Decode.succeed KeyPressed)

The Browser.Events module provides a function called onKeyPress which is responsible for subscribing to all key press events. Here’s what its type signature looks like:

onKeyPress : Decoder msg -> Sub msg

It takes a decoder as an input and returns a subscription. In the Decoding JSON - Part 1 & Part 2 sections, we used decoders to transform JSON values to Elm. onKeyPress uses the same decoders to translate underlying key codes to Elm values.

How do we let onKeyPress know that we don’t care about a specific key value? We can do that by using Decode.succeed which ignores its input and always returns the given Elm value. In the subscriptions function above, we asked it to always return the KeyPressed message.

Note: If you’re interested in seeing more examples of Decode.succeed, you may want to review the Decoding a JSON Object section from chapter 6.

Like commands, we don’t tend to create subscriptions by using some constructor function. Instead, we just look for an appropriate function like onKeyPress in a module and use it to create a subscription. Here’s another example: let’s say we want to get the current time periodically. We can create a subscription for that by using the Time.every function.

We need to import the Browser.Events and Json.Decode modules in EventListener.elm.

module EventListener exposing (Model)

import Browser.Events exposing (onKeyPress)
import Json.Decode as Decode
.
.

Subscription Takes a Model

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 Elm runtime expects the function responsible for creating a subscription to accept a model regardless of whether that model is used or not. Since we aren’t using that parameter, we should replace it with _.

subscriptions : Model -> Sub Msg
subscriptions _ =
    onKeyPress (Decode.succeed KeyPressed)

Our example app is quite simple and doesn’t use the model to create a subscription. But other apps may use it to build complex subscriptions.

Wiring Everything Up

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

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

And import the Browser module.

module EventListener exposing (Model)

import Browser
.
.

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

We’re now ready to test. Run elm reactor from the beginning-elm directory in terminal and go to this URL in your browser: http://localhost:8000/src/EventListener.elm. You should see a page that just displays 0.

Press any alphanumeric key and the counter will 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 field directly like this:

main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> onKeyPress (Decode.succeed KeyPressed)
        }

That being 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 a Specific Key Event

Let’s say we want to increment the counter only when the i key is pressed. Similarly, we want to decrement it only when the d key is pressed. To do that, we need to pay attention to the underlying value of a key. Let’s replace KeyPressed with the following messages in the Msg type.

type Msg
    = CharacterKey Char
    | ControlKey String

CharacterKey represents all character keys, for example i, d, 1, +, etc. And ControlKey represents special keys such as Control, Left Arrow, and Right Arrow. Next we’ll write a decoder that’s capable of making this distinction. Add the following code below the subscriptions function in EventListener.elm.

keyDecoder : Decode.Decoder Msg
keyDecoder =
    Decode.map toKey (Decode.field "key" Decode.string)


toKey : String -> Msg
toKey keyValue =
    case String.uncons keyValue of
        Just ( char, "" ) ->
            CharacterKey char

        _ ->
            ControlKey keyValue

In the Decoding a JSON Object section, we learned how to decode an individual JSON field using the field decoder. When a key is pressed, the browser sends onKeyPress a JSON that looks something like this:

{
    "key": "keyValue"
}

The Decode.field "key" Decode.string expression in keyDecoder pulls keyValue out of JSON. After that Decode.map uses the toKey function to determine whether the user pressed a character or a control key.

String.uncons

The String.uncons function splits a non-empty string into its head and tail. Here’s what its type signature looks like:

Let’s fire up elm repl from the beginning-elm directory and experiment with uncons to understand it better.

> String.uncons "abc"
Just ('a',"bc") : Maybe ( Char, String )

> String.uncons "a"
Just ('a',"") : Maybe ( Char, String )

> String.uncons ""
Nothing : Maybe ( Char, String )

As you can see, uncons must return a value of type Maybe because the given string can be empty. We saw a similar pattern with the List.head function.

> List.head [ "a", "b", "c" ]
Just "a" : Maybe String

> List.head [ "a" ]
Just "a" : Maybe String

> List.head []
Nothing : Maybe a

By splitting a string into its head and tail, uncons has given us the ability to pattern match on strings exactly as we would on lists. In the Pattern Matching Lists section, we saw how the List module uses pattern matching to elegantly implement the foldl function.

foldl : (a -> b -> b) -> b -> List a -> b
foldl func acc list =
    case list of
        [] ->
            acc

        x :: xs ->
            foldl func (func x acc) xs

We can implement foldl for strings too using a similar pattern with the help of uncons.

foldl : (Char -> b -> b) -> b -> String -> b
foldl func acc string =
    case String.uncons string of
        Nothing ->
            acc

        Just ( head, tail ) ->
            foldl func (func head acc) tail

Exercise 8.3.1

The inner workings of List.foldl has already been covered extensively in the Pattern Matching Lists section. See if you can use that as a reference to figure out how the above implementation for String.foldl works.

CharacterKey

Let’s handle the CharacterKey message in update by replacing its current implementing with the following.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        CharacterKey 'i' ->
            ( model + 1, Cmd.none )

        CharacterKey 'd' ->
            ( model - 1, Cmd.none )

        _ ->
            ( model, Cmd.none )

The only thing remaining is to use keyDecoder in the subscriptions function.

subscriptions : Model -> Sub Msg
subscriptions _ =
    onKeyPress keyDecoder

We’re now ready to test. Refresh the page at http://localhost:8000/src/EventListener.elm. When you press the i key, the counter should go up and when you press the d key, the counter should go down.

Subscribing to Multiple Events

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

type Msg
    = CharacterKey Char
    | ControlKey String
    | MouseClick

Next we’ll handle MouseClick by adding a new branch to the update function.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        CharacterKey 'd' ->
            ...

        MouseClick ->
            ( model + 5, Cmd.none )

        _ ->
            ...

When the MouseClick message arrives, we simply increment the model by 5. It’s important to add the MouseClick -> branch above the catch-all branch. Otherwise, it’ll be unreachable. Now let’s create a subscription by using the onClick function.

subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.batch
        [ onKeyPress keyDecoder
        , onClick (Decode.succeed MouseClick)
        ]

onClick is also defined in the Browser.Events module. Let’s expose it in EventListener.elm.

import Browser.Events exposing (onClick, onKeyPress)

When we have more than one subscription, we must batch them with Sub.batch. Here’s how its type signature looks:

Sub.batch : List (Sub msg) -> Sub msg

It’s interesting to note that the return type of our subscriptions function didn’t change at all even though we’re now returning multiple subscriptions. We also saw this pattern with Cmd.batch in the Navigating to List Posts Page section earlier.

Cmd.batch : List (Cmd msg) -> Cmd msg

Refresh the page at http://localhost:8000/src/EventListener.elm and the counter should be incremented by 5 when you click 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 following sequence diagram shows the interaction between the Elm runtime and our code.

Here’s the entire code from EventListener.elm for your reference:

module EventListener exposing (Model)

import Browser
import Browser.Events exposing (onClick, onKeyPress)
import Html exposing (..)
import Json.Decode as Decode


type alias Model =
    Int


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


view : Model -> Html Msg
view model =
    div []
        [ text (String.fromInt model) ]


type Msg
    = CharacterKey Char
    | ControlKey String
    | MouseClick


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        CharacterKey 'i' ->
            ( model + 1, Cmd.none )

        CharacterKey 'd' ->
            ( model - 1, Cmd.none )

        MouseClick ->
            ( model + 5, Cmd.none )

        _ ->
            ( model, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.batch
        [ onKeyPress keyDecoder
        , onClick (Decode.succeed MouseClick)
        ]


keyDecoder : Decode.Decoder Msg
keyDecoder =
    Decode.map toKey (Decode.field "key" Decode.string)


toKey : String -> Msg
toKey keyValue =
    case String.uncons keyValue of
        Just ( char, "" ) ->
            CharacterKey char

        _ ->
            ControlKey keyValue


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }
Back to top
Close