elm / browser

Create Elm programs that run in browsers!
https://package.elm-lang.org/packages/elm/browser/latest/
BSD 3-Clause "New" or "Revised" License
312 stars 64 forks source link

Example in Browser.Events.onClick doc does not work (click event evaluated twice) #62

Open ymtszw opened 5 years ago

ymtszw commented 5 years ago

The example is as follows:

Subscribe to mouse clicks anywhere on screen. Maybe you need to create a custom drop down. You could listen for clicks when it is open, letting you know if someone clicked out of it:


import Browser.Events as Events
import Json.Decode as D

type Msg = ClickOut

subscriptions : Model -> Sub Msg subscriptions model = case model.dropDown of Closed _ -> Sub.none

Open _ ->
  Events.onClick (D.succeed ClickOut)

The stated use case is exactly what I wanted to achieve. So I tried, but it seems not working.
https://ellie-app.com/4r8RQNZX4P4a1

```elm
module Main exposing (main)

import Browser
import Browser.Events
import Html exposing (..)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick)
import Json.Decode exposing (succeed)

type alias Model =
    { dropDown : Bool }

initialModel : () -> ( Model, Cmd Msg )
initialModel _ =
    ( { dropDown = False }, Cmd.none )

type Msg
    = DropDown Bool

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        DropDown isOpen ->
            ( { model | dropDown = isOpen }, Cmd.none )

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick (DropDown (not model.dropDown)) ] [ text "Drop Down" ]
        , ul
            [ style "border" "solid black 1px"
            , style "display" <|
                if model.dropDown then
                    "block"

                else
                    "none"
            ]
            [ li [] [ text "Item 1" ]
            , li [] [ text "Item 2" ]
            ]
        ]

subscriptions : Model -> Sub Msg
subscriptions model =
    let
        _ =
            Debug.log "subCalled" model
    in
    if model.dropDown then
        -- Browser.Events.onClick (succeed (DropDown False)) -- Toggle this line to see the bug
        Sub.none

    else
        Sub.none

main : Program () Model Msg
main =
    Browser.element
        { init = initialModel
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

When you uncomment the commented line above to provide "click out", the dropdown button itself stops working. Dropdown part is not shown at all even if you click the button.

Apparently, a click event is evaluated twice, within a single frame, before AND after model update? To prove that, I introduced "step" state before activating "click out" subscription: https://ellie-app.com/4r9djjXTZ4Fa1

module Main exposing (main)

import Browser
import Browser.Events
import Html exposing (..)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick)
import Json.Decode exposing (succeed)

type alias Model =
    { dropDown : DD }

type DD
    = JustOpened
    | ReadyToClose
    | Closed

initialModel : () -> ( Model, Cmd Msg )
initialModel _ =
    ( { dropDown = Closed }, Cmd.none )

type Msg
    = Open
    | GetReady
    | Close

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Open ->
            ( { model | dropDown = JustOpened }, Cmd.none )

        GetReady ->
            ( { model | dropDown = ReadyToClose }, Cmd.none )

        Close ->
            ( { model | dropDown = Closed }, Cmd.none )

view : Model -> Html Msg
view model =
    div []
        [ button
            [ onClick <|
                case model.dropDown of
                    Closed ->
                        Open

                    _ ->
                        Close
            ]
            [ text "Drop Down" ]
        , ul
            [ style "border" "solid black 1px"
            , style "display" <|
                case model.dropDown of
                    Closed ->
                        "none"

                    _ ->
                        "block"
            ]
            [ li [] [ text "Item 1" ]
            , li [] [ text "Item 2" ]
            ]
        ]

subscriptions : Model -> Sub Msg
subscriptions model =
    case model.dropDown of
        JustOpened ->
            Browser.Events.onAnimationFrame (\_ -> GetReady)

        ReadyToClose ->
            Browser.Events.onClick (succeed Close)

        Closed ->
            Sub.none

main : Program () Model Msg
main =
    Browser.element
        { init = initialModel
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

This works, since it avoids suspected "double evaluation" by delaying state transition with onAnimationFrame.

Actually, this problem had alreaday reported when it was Mouse.clicks in Elm 0.18. https://discourse.elm-lang.org/t/mouse-clicks-subscription-created-and-executed-following-click-event/1067 The problem continued in Browser.Events.onClick but I do not see the issue on the repository, so let me file it here.

mdbenito commented 4 years ago

Actually, I think that the problem is not that the event is "evaluated twice" but that subscriptions are called after update with the new model, i.e. with model.dropdown = True. The first branch of your if then gets called and the Dropdown False message is immediately emitted.

See this related thread in discourse.

This doesn't change the fact that the example in the documentation is indeed confusing.