Smart Event Listeners
Tags: elm, webdev
Published: 2023-08-23 10:18:01

---
title: Smart Event Listeners
published: true
description: Learn how smart constructors, JSON decoders, and "Parse, don't validate" help you write better event listeners.
tags: elm, webdev
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-08-19 16:27 +0000
---

In Elm, **event listeners** establish a boundary between your application and the outside world. As a result they provide the perfect opportunity to apply the ideas of "[Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)". I call event listeners that apply these ideas, **smart event listeners**, because they serve a similar purpose as [smart constructors](https://wiki.haskell.org/Smart_constructors). In fact, smart event listeners usually make use of smart constructors in their implementation.

## What are event listeners?

All the functions (except for the decoders) in [`Html.Events`](https://package.elm-lang.org/packages/elm/html/1.0.0/Html-Events) are called **event listeners**. For e.g. [`onClick`](https://package.elm-lang.org/packages/elm/html/1.0.0/Html-Events#onClick), [`onInput`](https://package.elm-lang.org/packages/elm/html/1.0.0/Html-Events#onInput), and [`onFocus`](https://package.elm-lang.org/packages/elm/html/1.0.0/Html-Events#onFocus) are event listeners. As you're probably aware, you aren't limited to predefined event listeners because you can define custom event listeners using  [`on`](https://package.elm-lang.org/packages/elm/html/1.0.0/Html-Events#on) and [a few other functions](https://package.elm-lang.org/packages/elm/html/1.0.0/Html-Events#custom).

An event listener listens for a specific event and it may or may not grab information from the event using a [decoder](https://package.elm-lang.org/packages/elm/json/1.1.3/Json-Decode). It then produces a message that gets routed to your `update` function where you can subsequently handle the message.

## What are smart event listeners?

A **smart event listener** is an event listener that grabs unstructured information from an event and constructs a message that contains structured data pertinent to your application's needs.

## Parse, don't validate

In the popular article, [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/), Alexis King writes about the power of using data types to inform your code. The following points she makes are relevant to our discussion:

> The difference between parsing and validating lies in how information is preserved.

> A parser is just a function that consumes less-structured input and produces more-structured output.

> Parsers are an incredibly powerful tool: they allow discharging checks on input up-front, right on the boundary between a program and the outside world, and once those checks have been performed, they never need to be checked again!

Elm's [JSON decoders](https://package.elm-lang.org/packages/elm/json/1.1.3/Json-Decode) are parsers in this regard and as it turns out we can use JSON decoders to help us create smart event listeners.

## Example 1: A volume control

![A screenshot of Drum Machine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/slhqtto7no321tt48c3e.png)

[Drum machine](https://github.com/dwayne/elm-drum-machine) is an Elm app based on [freeCodeCamp](https://www.freecodecamp.com/)'s [Build a Drum Machine](https://www.freecodecamp.org/learn/front-end-development-libraries/front-end-development-libraries-projects/build-a-drum-machine) front-end project. One of the components present in the application is a volume control. The control allows you to adjust the volume of the machine from 0 all the way up to 100.

We can use a [range input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range) for the volume control.

```elm
view : (Volume -> msg) -> Bool -> Volume -> H.Html msg
view onVolume isDisabled volume =
	H.input
		[ HA.type_ "range"
	    , HA.min "0"
	    , HA.max "100"
	    , HA.step "1"
	    , HA.class "slider"
	    , HA.value <| Volume.toString volume
	    , if isDisabled then
		    HA.disabled True

		  else
			onVolumeInput onVolume  
	    ]
	    []
```

When the user changes the volume an [input event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event) is triggered and we can grab the new value of the volume off the event's `target.value` attribute which is a `String` (unstructured information). Rather than immediately wrap that `String` in a message that gets routed to our `update` function (which is what the [`onInput`](https://package.elm-lang.org/packages/elm/html/1.0.0/Html-Events#onInput) event listener does), we can instead "parse" that `String` into a value of type `Volume` (structured information) that has more meaning to our application. The smart event listener `onVolumeInput` takes that approach.

```elm
onVolumeInput : (Volume -> msg) -> H.Attribute msg
onVolumeInput onVolume =
	let
		decoder =
			HE.targetValue
		        |> JD.andThen
		            (\s ->
			            case Volume.fromString s of
			                Just volume ->
					            JD.succeed <| onVolume volume

			                Nothing ->
				                JD.fail "ignored"
		            )
	in
	HE.on "input" decoder
```

Notice how the smart constructor, `Volume.fromString`, is used in `decoder` to "parse" the `String` into a `Volume`. On success, a value of type `Volume` gets routed to our `update` function. Furthermore, if the decoder fails because the `target.value` represents an invalid volume then [Elm silently ignores the event](https://thoughtbot.com/blog/advanced-dom-event-handlers-in-elm) and we never have to worry about dealing with bad volumes in our `update` function.

Feel free to learn more by checking out [the original code](https://github.com/dwayne/elm-drum-machine/blob/1cfed90780adc5121efacae54f47d55d853d27ce/src/Volume.elm).

## Example 2: Keyboard movement

![A screenshot of Elm 2048](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x6agkljcgbczcr15zzl5.png)

In [my 2048 clone](https://github.com/dwayne/elm-2048) you can use the keyboard to move the tiles. A [keydown event](https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event) listener is attached to [the main application container](https://github.com/dwayne/elm-2048/blob/607a1219024d8422263ed3bec5bb1b4bdc040035/src/App/View/Main.elm#L146) in order to detect your key presses and determine what message (if any) to route to your `update` function.

```elm
onKeyDown : (Grid.Direction -> msg) -> msg -> H.Attribute msg
onKeyDown onMove onNewGame =
    let
        keyDecoder =
            JD.field "key" JD.string
                |> JD.andThen
                    (\key ->
                        case ( key, String.toUpper key ) of
                            -- Arrow keys: Up, Right, Down, Left
                            ( "ArrowUp", _ ) ->
                                JD.succeed <| onMove Grid.Up

                            ( "ArrowRight", _ ) ->
                                JD.succeed <| onMove Grid.Right

                            ( "ArrowDown", _ ) ->
                                JD.succeed <| onMove Grid.Down

                            ( "ArrowLeft", _ ) ->
                                JD.succeed <| onMove Grid.Left

                            -- Vim: KLJH
                            ( _, "K" ) ->
                                JD.succeed <| onMove Grid.Up

                            ( _, "L" ) ->
                                JD.succeed <| onMove Grid.Right

                            ( _, "J" ) ->
                                JD.succeed <| onMove Grid.Down

                            ( _, "H" ) ->
                                JD.succeed <| onMove Grid.Left

                            -- WDSA
                            ( _, "W" ) ->
                                JD.succeed <| onMove Grid.Up

                            ( _, "D" ) ->
                                JD.succeed <| onMove Grid.Right

                            ( _, "S" ) ->
                                JD.succeed <| onMove Grid.Down

                            ( _, "A" ) ->
                                JD.succeed <| onMove Grid.Left

                            -- Restart
                            ( _, "R" ) ->
                                JD.succeed onNewGame

                            _ ->
                                JD.fail "ignored"
                    )
    in
    keyDecoder
        |> JD.map (\msg -> ( msg, True ))
        |> HE.preventDefaultOn "keydown"
```

As you can see the `onKeyDown` event listener is another example of a smart event listener. It grabs the pressed key (unstructured information) and transforms it into an application specific data structure (structured information, `Grid.Direction` in the case of the movement keys). Notice that if any message ever gets routed to our `update` function we know with certainty that the user either pressed one of the movement keys or they tried to restart the game.

Feel free to learn more by checking out [the original code](https://github.com/dwayne/elm-2048/blob/607a1219024d8422263ed3bec5bb1b4bdc040035/src/App/View/Main.elm).

## More examples

Here are a few more examples:

- Selecting a one way or return flight in the [Flight Booker](https://dwayne.github.io/elm-7guis/flight-booker.html) task.
	- See [`viewFlight`](https://github.com/dwayne/elm-7guis/blob/f89c95275e991cf8b95ec41483a82e4dbaa04ee6/src/Task/FlightBooker.elm#L245-L260) and [`viewSelect`](https://github.com/dwayne/elm-7guis/blob/f89c95275e991cf8b95ec41483a82e4dbaa04ee6/src/Support/View/Control.elm#L69-L108).
- Changing the duration in the [Timer](https://dwayne.github.io/elm-7guis/timer.html) task.
	- See [`onDurationInput`](https://github.com/dwayne/elm-7guis/blob/f89c95275e991cf8b95ec41483a82e4dbaa04ee6/src/Task/Timer.elm#L147-L163).
- Changing the diameter of a circle in the [Circle Drawer](https://dwayne.github.io/elm-7guis/circle-drawer.html) task.
	- See [`onDiameterInput`](https://github.com/dwayne/elm-7guis/blob/f89c95275e991cf8b95ec41483a82e4dbaa04ee6/src/Task/CircleDrawer/Lib/Html/Events.elm#L56-L71).
- [elm-limiter](https://github.com/hayleigh-dot-dev/elm-limiter) allows you to do debouncing and throttling through custom `onClick` and `onInput` event listeners.
	- See [`Limiter.Events`](https://github.com/hayleigh-dot-dev/elm-limiter/blob/70e9f158f28466d630c1869d19ab3e7364d46b43/src/Limiter/Events.elm).

## Conclusion

[Smart constructors](https://wiki.haskell.org/Smart_constructors), [JSON decoders](https://package.elm-lang.org/packages/elm/json/1.1.3/Json-Decode), and "[Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)" all combine to give us a powerful tool, **smart event listeners**, to patrol the borders of our Elm web apps. As a result, we get to decide what data gets in and in what form we want to work with it.

**P.S.** *Evan has a short note he wrote about this very idea in the context of handling movement input for a game. I read it before but I can't seem to find it anywhere. If any of you come across the note please share it with me. Thanks!*