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.
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
.
View
The code for presenting our model to the user is also quite simple. Add the following code to the bottom of EventListener.elm
.
All we’re doing here is display the model. Import the Html
module in EventListener.elm
.
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
.
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
.
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
.
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:
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
.
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 _
.
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
.
And import the Browser
module.
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:
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.
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
.
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:
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.
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.
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.
We can implement foldl
for strings too using a similar pattern with the help of uncons
.
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.
The only thing remaining is to use keyDecoder
in the subscriptions
function.
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
.
Next we’ll handle MouseClick
by adding a new branch to the update
function.
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.
onClick
is also defined in the Browser.Events
module. Let’s expose it in EventListener.elm
.
When we have more than one subscription, we must batch them with Sub.batch
. Here’s how its type signature looks:
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.
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: