stopPropagation breaks onCheck value
Set up:
A checkbox element with onCheck, and a parent element with onClick and stopPropagation.
Expected behaviour:
When you click/change the checkbox the Checked message contains the new checked value.
Actual behaviour:
The Checked message contains the old checked value. Hence the checked value never changes.
In the code below when you change stopPropagationOn to ( Clicked, False ) the checkbox works as expected. But with ( Clicked, True ) the checkbox never changes (with mouse or keyboard).
This is also the case when onClick and stopPropagation is defined on the checkbox element.
SSCCE:
module Main exposing (main)
import Browser
import Html
import Html.Attributes
import Html.Events
import Json.Decode
type Msg
= Checked Bool
| Clicked
main : Program () Bool Msg
main =
Browser.sandbox
{ init = False
, update =
\msg checked ->
case msg of
Checked newChecked ->
newChecked
Clicked ->
checked
, view =
\checked ->
Html.div
[ Html.Events.stopPropagationOn
"click"
(Json.Decode.succeed ( Clicked, True ))
]
[ Html.input
[ Html.Attributes.type_ "checkbox"
, Html.Attributes.checked checked
, Html.Events.onCheck Checked
]
[]
]
}
Here’s what happens.
First, some facts:
- When you click a checkbox, the browser first emits the "click" event, and then the "change" event (which is what
Html.Events.onChecklistens for). - In Elm, stopping propagation also has a bonus effect: Rendering then happens synchronously, instead of at the next animation frame (which is the default behavior in Elm). See “Note 3” at https://elm.dmy.fr/packages/elm/virtual-dom/1.0.5/VirtualDom#Handler
-
checkedandvalueare special cases in Elm, where the latest virtual DOM is diffed against the real DOM, instead of against the previous virtual DOM. That’s because those two properties can be mutated by user actions (clicking a checkbox in this case).
Let’s go through what happens when not stopping propagation:
- The user clicks the checkbox, which is currently unchecked.
- The browser sets the
checkedproperty totrue. - The browser fires the "click" event.
- Elm synchronously runs
updatewith messageClicked. The model is unchanged (stillFalse). - Elm schedules a render at the next animation frame.
- The browser fires the "change" event.
- Elm reads the
checkedproperty, which istrue. - Elm synchronously runs
updatewith messageChecked True. The model is changed toTrue. - Elm wants to schedule a render at the next animation frame, but notices that one is already scheduled so it doesn’t need to do anything.
- The next animation frame triggers.
- Elm renders once with the latest model (which is
True). - The
viewsays that thecheckedproperty on the input should be set toTrue. - Elm diffs with the
checkedproperty on the actual input element. Both are true, so there is nothing to do.
And now what happens when we stop propagation:
- The user clicks the checkbox, which is currently unchecked.
- The browser sets the
checkedproperty totrue. - The browser fires the "click" event.
- Elm synchronously runs
updatewith messageClicked. The model is unchanged (stillFalse). - Since we stopped propagation, Elm synchronously renders straight away.
- The
viewsays that thecheckedproperty on the input should be set toFalse(since the model is stillFalse). - Elm diffs with the
checkedproperty on the actual input element. It istruewhileviewsaysFalse, so Elm sets thecheckedproperty back tofalse. - The browser fires the "change" event.
- Elm reads the
checkedproperty, which is nowfalse(since handling the "click" event resulted in setting it back tofalse). - Elm synchronously runs
updatewith messageChecked False. The model is kept asFalse. - Now it doesn’t matter if the listener for the "change" event renders synchronously or at the next animation frame – since the model is
Falsethat’s what we’re gonna get.
I have never understood why “stopping propagation” and “synchronous rendering” go hand in hand in Elm. If it was possible to control these separately, it would be possible to stop propagation without causing a synchronous render that resets the checkbox.