Slightly advanced guide to custom events in Elm

Here’s how to handle drag and drops and any other event!

azurewaters
5 min readJan 15, 2023
Photo by Kelly Sikkema on Unsplash

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:

  1. on Cannot stop propagation and cannot prevent default action.
  2. stopPropagationOn Can stop propagation of the event but cannot prevent the default action.
  3. preventDefaultOn Cannot stop the propagation of the event but can prevent the default action.
  4. 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:

  1. The eventObjectDecoder is the decoder we write to extract the data from the event object.
  2. The call to Decode.map msg eventObjectDecoder on line 7 will result in an output in the form of Decoder msg. But, the stopPropagationOn function expects its second argument to be a Decoder (msg, Bool). So, we can’t just pair Decoder msg with a True value and push it through a Decode.succeed like we did earlier, because we’d just end up with a Decoder (Decoder msg, Bool).
  3. What we need to do is to somehow transform the Decoder msg into a msg. The map functions do just that. And, since we are working in the realm of decoding, we need to use map functions within the Decode module.
  4. Looking at the documentation, the closest fit for our needs is Decode.map2. It takes two Decoder values and routes them through a function to get a single Decoder value.
  5. So, instead of just passing a True value, we use aDecode.succeed True.
  6. Finally, we send Tuple.pair, theDecoder msg from line 7, and the Decode.succeed True from line 8 through the Decode.map2 function to get an output in the form of Decoder (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!

--

--

Responses (1)