Elm, preventDefault, and onWithOptions
Keeping your model and the DOM in sync can be tricky business. In many cases your model drives what your view looks like and everyone’s happy.
But users being users they want to do things like click and type and whatnot. They make changes directly to the DOM that you have to try and sync up with your model.
The Lightning Talk app has this problem with checkboxes. We have our own custom checkboxes that look like this.
Whenever a user selects a date then a row of checkboxes should appear. Time slots that already have someone signed up should be disabled. The Time slot that is currently selected should appear checked (the 11:30 above). Other time slots that are available should appear as clickable (11:10).
My co-worker Evan Williams was working on a bug fix. Users should not be able to directly deselect a time slot. In other words, users should not be able to uncheck a checkbox by clicking it.
This SSCCE is the situation we found ourselves in. We wanted a checkbox that could not be directly unchecked by clicking on it. However, it’s clear from the example that after a couple of clicks the model will get out of sync with the DOM which is obviously very undesirable. We want the Model and what’s displayed in the view to be in sync.
Preventing events with onWithOptions
One possible solution is to tell the browser that we’re going to handle this checkbox’s behavior ourselves by preventing the default behavior.
Elm’s documentation isn’t entirely clear on how to equivalently perform event.preventDefault(). Basically, what we need to do is use onWithOptions. As of Elm 0.18 this function takes three arguments:
- String - The name of the event that we’re binding to, such as “click”.
- Options - A simple record that we can specify for stopping propagation and preventing default behavior.
- Decoder Msg - A decoder that transforms an event into a Msg. In many situations we don’t care about the event and can simply succeed with the Msg.
Our usage is pretty straightforward. Rather than using onCheck we’re using onWithOptions. We’re telling the browser that on click, instead of doing the default behavior of checking the box, we’re instead going to dispatch an OnCheckboxChecked Msg and update our Model accordingly. If we decide to update the Model then the view will re-render with the checkbox state that we want.
This updated example can be found here.
There are likely other solutions to our problem. But I particularly like this one because we are explicitly keeping the view reactive to our model. I hope this helps and leave a comment if you have questions!