Imagine you need to do something when the enter key is pressed but don’t care
when any other keys are pressed. What if you don’t always want to react to the
keyup
event? How would you do it in an Elm app?
I can think of two ways to tackle the problem:
- Write an
onKeyup
handler and send every event’s keycode to theupdate
function for evaluation. - Write an
onEnter
handler and check the keycode in the handler function first. Only send on to theupdate
function if the enter key was pressed.
The onKeyup
handler seems like it would be easier to write but having to
check the keycode in our update
function each time feels clunky.
The onEnter
handler sounds like a tidier solution. But how do you build that
conditional logic into the handler function?
How DOM event handler functions work in Elm
In Elm, the Html.Events
package provides the things you need for handling
events, including some built in handlers like Html.Events.onClick
. It also
provides Html.Events.on
, the function used to build Html.Events.onClick
,
and which any developer can use to build their own custom DOM event handlers.
The way Html.Events.on
works is that you give it the string name of the event
to react to and a JSON Decoder to decode the event object with. Whatever
values you successfully decode are then sent on to your update
function.
A great example of how this works is Html.Events.onClick
, which doesn’t
extract any values from the event object at all. Instead it uses
Json.Decode.succeed
to ignore the JSON all together and successfully return
msg
:
onClick : msg -> Attribute msg
onClick msg =
on "click" (Json.succeed msg)
(Note: Json.Decode
has been aliased to Json
in the file where onClick
lives.)
The source docs don’t mention what happens when the decoder fails. But you can
try it out in this Ellie App example and see for yourself. Spoiler alert:
nothing happens. It is allowed to fail silently, and nothing is sent on to the
update
function. This can make them tricky to debug, but it gives us a way to
conditionally react to events.
Sometimes you have to Json.Decode.fail
to succeed
Elm developers can leverage this behavior to conditionally handle events and keep all the conditional logic inside of their handler function.
To write an onEnter
handler this way you would need to do the following:
Extract the keycode from the event object with a decoder
If the keycode is
13
(the enter key), useJson.succeed
to return a successful message, just like inonClick
. Otherwise, returnJson.fail
For the first step, the Html.Events
package includes a decoder for extracting
the keycode from an event object, called Html.Events.keyCode
. We can use
that.
For the second step, there is a handy function in the Json
package called
Json.Decode.andThen
that’s especially good at doing just this thing: it lets
us choose a decoder to return, based on the value passed in. Unfortunately,
diving into how andThen
works is beyond the scope of this short article but is
well worth exploring. In the mechanics of Maybe
my colleague Joël explores
Maybe.andThen
which works in a similar way to Json.Decode.andThen
. To
explore decoders in general, Joël has a few other great articles.
Ready to put all this together and make something?
Implementing onEnter
Using everything we just learned we could write an implementation of onEnter
like this:
onEnter : msg -> Html.Attribute msg
onEnter msg =
let
isEnterKey keyCode =
if keyCode == 13 then
Json.succeed msg
else
Json.fail "silent failure :)"
in
on "keyup" <|
Json.andThen isEnterKey Html.Events.keyCode
You can play around with onEnter
in this Ellie App.
Summary
The Html.Events
package authors decided to ignore when decoders fail in
event handlers. We were able to lean on that to write a decoder that decides
whether to fail or not based on some characteristic of the event. In our case,
we were interested in the event’s keycode. This allowed us to keep the
conditional logic out of our update
function and within our custom handler
function.