Custom DOM event handlers in Elm are made up of 3 parts:
- The
Html.Events.on
function - 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
update
function.
For example:
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
decoded.
Authors of custom events have leaned into this behavior, writing decoders that
purposely fail. This allows them to write event handlers like onEnter
that
listen to the “input” event but only fires if the Enter key is pressed.
Failures
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 Err
branch.
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
that Result
. We need some way of getting access to it in the decoder. Time
to dive into some decoder fanciness!
Logging Decoder
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 Json.Decode
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 Decode.decodeValue
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
)
Final product
We still want to return a decoder so we re-wrap the values from our Ok
and
Err
branches with Decode.succeed
and Decode.fail
respectively.
Parameterize it to make it generic and we end up with a function that looks
like this:
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.