Conditionally handling events in Elm

Greg Fisher

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:

  1. Write an onKeyup handler and send every event’s keycode to the update function for evaluation.
  2. Write an onEnter handler and check the keycode in the handler function first. Only send on to the update 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:

  1. Extract the keycode from the event object with a decoder

  2. If the keycode is 13 (the enter key), use Json.succeed to return a successful message, just like in onClick. Otherwise, return Json.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.