Custom DOM event handlers in Elm are made up of 3 parts:
- The string name of the DOM event you want to react to
- A JSON decoder that gets values out of the event object. The decoded value
gets sent to your
Html.Events.on "click" clickDecoder
One important aspect of decoders is that they can fail. For example, you try
to read a property on the event object that doesn’t exist. What should the event
handler send to your
update function? The
Html.Events library has opted for
perhaps the simplest solution - ignore events that can’t be successfully
Authors of custom events have leaned into this behavior, writing decoders that
purposely fail. This allows them to write event handlers like
listen to the “input” event but only fires if the Enter key is pressed.
Silently ignoring events that fail to decode is a feature. Until it’s not. If you accidentally wrote a broken decoder, you won’t get any error when if fails. Instead you’ll just have an event handler that doesn’t seem to be firing any events. This problem is particularly likely if you’re writing a decoder where you are purposely failing the decoder but may be doing that in the wrong place.
Ideally, there would be a way to get some visibility into those failures. The problem is that JSON decoders aren’t aware of their failures. A decoder that tries to read a string from a non-existent field can’t say “log the error if I fail”.
brokenDecoder : Decoder String brokenDecoder = Decode.field "doesntExist" Decode.string
Normally this isn’t a problem because we get back a
Result, either from
directly decoding some JSON or indirectly from an HTTP response. There we can
easily add a
Debug.log to the
case Decode.decodeValue brokenDecoder someJson of Ok val -> -- HANDLE SUCCESS Err error -> Debug.log "decode error" error
The problem is that when writing a DOM event handler, we don’t get access to
Result. We need some way of getting access to it in the decoder. Time
to dive into some decoder fanciness!
If we had access to the raw event object we could manually call
Decode.decodeValue on it to get the
Result. But inside a decoder we don’t
have access to the event directly. Only whatever properties we’ve decided to
decode on it. This is where one of the weirder decoders in the
library becomes useful.
Decode.value is a decoder that doesn’t do any decoding. Instead, it just gives
you back the unparsed blob of JSON. Rather than asking for a particular field
from the DOM event, we can ask for “all of it”. Chain an
andThen onto it and
now you have a reference to the event that you can use in your code:
Decode.value |> Decode.andThen (\event -> -- YAY!!)
The whole point of getting the event was so we could call
manually on it so that we could get access to the
Result. Now we have access
to the error and can log it. Success!
Decode.value |> Decode.andThen (\event -> case Decode.decodeValue brokenDecoder event of Ok val -> -- HANDLE SUCCESS Err error -> Debug.log "decode error" error )
We still want to return a decoder so we re-wrap the values from our
Err branches with
Parameterize it to make it generic and we end up with a function that looks
loggingDecoder : Decoder a -> Decoder a loggingDecoder realDecoder = Decode.value |> Decode.andThen (\event -> case Decode.decodeValue realDecoder event of Ok decoded -> Decode.succeed decoded Err error -> error |> Decode.errorToString |> Debug.log "decoding error" |> Decode.fail )
Now we can wrap decoders we’d like to inspect with it. Any time the event handler drops an event because of a failed decoder you’ll see the error show up in your browser console.
Html.Events.on "click" (loggingDecoder brokenDecoder)
You can see it in action in this working example.