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
}