html icon indicating copy to clipboard operation
html copied to clipboard

stopPropagation breaks onCheck value

Open harmboschloo opened this issue 6 years ago • 1 comments

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
                        ]
                        []
                    ]
        }

harmboschloo avatar Feb 27 '19 08:02 harmboschloo

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.onCheck listens 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
  • checked and value are 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:

  1. The user clicks the checkbox, which is currently unchecked.
  2. The browser sets the checked property to true.
  3. The browser fires the "click" event.
  4. Elm synchronously runs update with message Clicked. The model is unchanged (still False).
  5. Elm schedules a render at the next animation frame.
  6. The browser fires the "change" event.
  7. Elm reads the checked property, which is true.
  8. Elm synchronously runs update with message Checked True. The model is changed to True.
  9. 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.
  10. The next animation frame triggers.
  11. Elm renders once with the latest model (which is True).
  12. The view says that the checked property on the input should be set to True.
  13. Elm diffs with the checked property on the actual input element. Both are true, so there is nothing to do.

And now what happens when we stop propagation:

  1. The user clicks the checkbox, which is currently unchecked.
  2. The browser sets the checked property to true.
  3. The browser fires the "click" event.
  4. Elm synchronously runs update with message Clicked. The model is unchanged (still False).
  5. Since we stopped propagation, Elm synchronously renders straight away.
  6. The view says that the checked property on the input should be set to False (since the model is still False).
  7. Elm diffs with the checked property on the actual input element. It is true while view says False, so Elm sets the checked property back to false.
  8. The browser fires the "change" event.
  9. Elm reads the checked property, which is now false (since handling the "click" event resulted in setting it back to false).
  10. Elm synchronously runs update with message Checked False. The model is kept as False.
  11. Now it doesn’t matter if the listener for the "change" event renders synchronously or at the next animation frame – since the model is False that’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.

lydell avatar Nov 13 '25 12:11 lydell