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.
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
The code for presenting our model to the user is also quite simple. Add the following code to the bottom of
All we’re doing here is display the model. Import the
Html module in
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
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.
Next we’ll handle the
KeyPressed message in
update. Add the following code to the bottom of
KeyPressed message arrives, we simply increment the model by
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
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
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
Json.Decode modules in
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
And import the
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
CharacterKey represents all character keys, for example
+, etc. And
ControlKey represents special keys such as
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
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:
Decode.field "key" Decode.string expression in
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 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
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
We can implement
foldl for strings too using a similar pattern with the help of
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
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
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
Next we’ll handle
MouseClick by adding a new branch to 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 is also defined in the
Browser.Events module. Let’s expose it in
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.
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: