Slightly advanced guide to custom events in Elm
Here’s how to handle drag and drops and any other event!
For the most part, you will get by with the events that have already been built into Elm. But, if you program long enough in Elm — and, oh, why wouldn’t you want to — you will need to pen your own events. But, the documentation is thin on custom events. This article is intended to right that wrong and have you writing custom events with aplomb! With aplomb, I say!
Four functions
There are four functions in the Html.Events
module that you can use to define a custom event handler in Elm. Here are their signatures:
They differ in whether they prevent the default action or stop the propagation of the event:
on
Cannot stop propagation and cannot prevent default action.stopPropagationOn
Can stop propagation of the event but cannot prevent the default action.preventDefaultOn
Cannot stop the propagation of the event but can prevent the default action.custom
Can stop the propagation of the event and can also prevent the default action.
The on function
Here’s the signature of the on
function again:
- First argument: The
String
bit is easy enough — for the click event, type in “click”; for touch events, type in “touchstart”, “touchmove”, “touchend”; and so on. The bottom line is that we just need to type up the event name we’d have used in Javascript. (MDN has a full list of events here.) - Second argument: The custom function expects a decoder as it’s second argument. But, what will the decoder decode? Elm will supply it with the object corresponding to the event we are trapping here. We would have written this in Javascript:
You see e
being passed to the event handler? That’s what will get parsed by the decoder here. What’s the structure of that e
object? It’s the same as in Javascript.
If you have no use for the data in that object, there’s no need to decode it. Simply use Json.Decode.succeed
and have the msg
emitted. Like so:
If you do need the data however, then you’ll have to write a decoder. Suppose we wanted to get the numerical value of a range slider. In Javascript, you’d get it by doing this:
In Elm, you’d write the decoder like so:
The decoder is used in the custom event like so:
And, this is how this custom event could be used in an app:
In other words: when the range slider’s value is altered, the event object’s valueAsNumber
property will be parsed using the vanDecoder
. The resulting value will be wrapped with the GotAValue
message and emitted. We handle this message as usual in the update
section of the app. All good?
The other three functions
Now that you have a hang of the basics, you won’t have trouble using the other three functions. They differ from the on
function only in the second argument: instead of just a message, a configuration in the form of a tuple or a record is expected.
Again, the simplest scenario is when you don’t want to decode any data. The code would then look like this:
If we do want the data in the event object, then the solution is below:
Here’s what the above code is doing:
- The
eventObjectDecoder
is the decoder we write to extract the data from the event object. - The call to
Decode.map msg eventObjectDecoder
on line 7 will result in an output in the form ofDecoder msg
. But, thestopPropagationOn
function expects its second argument to be aDecoder (msg, Bool)
. So, we can’t just pairDecoder msg
with aTrue
value and push it through aDecode.succeed
like we did earlier, because we’d just end up with aDecoder (Decoder msg, Bool)
. - What we need to do is to somehow transform the
Decoder msg
into amsg
. Themap
functions do just that. And, since we are working in the realm of decoding, we need to usemap
functions within theDecode
module. - Looking at the documentation, the closest fit for our needs is
Decode.map2
. It takes twoDecoder
values and routes them through a function to get a singleDecoder value
. - So, instead of just passing a
True
value, we use aDecode.succeed True
. - Finally, we send
Tuple.pair
, theDecoder msg
from line 7, and theDecode.succeed True
from line 8 through theDecode.map2
function to get an output in the form ofDecoder (msg, Bool)
.
(Yes, this is a post hoc explanation; it’s not terribly easy to arrive at this through reasoning alone. But, hopefully, the explanation helps the steps and the concepts stick.)
The preventDefaultOn
function works similarly to stopPropagationOn
.
However, the custom
function requires a small change. Here is it’s signature:
As you can see, it expects us to send in the configuration, if you will, as a record instead of a tuple. To make life easier, we could define a type alias to hold those values:
And, this is how a custom event is defined using the custom
function:
The unique point in the above code is that, instead of a regular function, Decode.map3
combines the three decoders’ outputs using the type alias’ constructor. Neat, huh?!
Handling drag and drop of files
The code below pulls all the concepts we discussed above together and shows how files can be dragged into an app.
Note: I referred to this page to find out the structure of the drag event’s event object.
You might have noticed that the onDrop
custom event on line 49 simply passes a NoOp
message, which in turn doesn’t do a darn thing in the update
section of the code. The reason it’s put in at all is because there’s a quirk — if you don’t define it, the drag operation won’t work.
Conclusion
You can write custom events in Elm and trap any event you could with JavaScript. The small hurdle to writing custom events is mostly to do with how the data in the event object is extracted and used. Other than that, it’s easy-peasy, don’t you think?
That’s it for this article. If you have any particular topics you’d like me to write about, feel free to leave a comment. Cheers!
You can buy me a coffee if you’d like!