1

When I use jQuery to animate the scrolling behavior the first click will cause the view to jump straight to the clicked anchor element. The second click, if issued within the animation-delay, will work flawlessly. If the animation-delay runs out and one issues a click again, it'll cause a straight jump to the element again.

Here's a short gif demonstrating the issue: enter image description here

As you can see, I click on services and instantly jump. I wait until the animation delay runs off and click portfolio, again an instant jump. But then I quickly click services again (animation delay didn't run off yet) and it works as expected.

My Elm-Application has onClick events assigned to some elements. When triggered, the href of the element gets passed to a JavaScript function through a port, like so:

-- partial view
view =
    div []
        [ nav [ class "navbar navbar-default navbar-fixed-top" ]
            [ div [ class "container" ]
                [ div [ class "navbar-header page-scroll" ]
                    [ button [ class "navbar-toggle", attribute "data-target" "#bs-example-navbar-collapse-1", attribute "data-toggle" "collapse", type' "button" ]
                        [ span [ class "sr-only" ]
                            [ text "Toggle navigation" ]
                        , span [ class "icon-bar" ]
                            []
                        , span [ class "icon-bar" ]
                            []
                        , span [ class "icon-bar" ]
                            []
                        ]
                    , a [ class "navbar-brand page-scroll", href "#page-top", onClick (Update.ClickedElem "#page-top") ]
                        [ text "Start Bootstrap" ]
                    ]
                , div [ class "collapse navbar-collapse", id "bs-example-navbar-collapse-1" ]
                    [ ul [ class "nav navbar-nav navbar-right" ]
                        [ li [ class "hidden" ]
                            [ a [ href "#page-top" ]
                                []
                            ]
                        , li []
                            [ a [ class "page-scroll", href "#services", onClick (Update.ClickedElem "#services") ]
                                [ text "Services" ]
                            ]
                        , li []
                            [ a [ class "page-scroll", href "#portfolio", onClick (Update.ClickedElem "#portfolio") ]
                                [ text "Portfolio" ]
                            ]
                        , li []
                            [ a [ class "page-scroll", href "#about", onClick (Update.ClickedElem "#about") ]
                                [ text "About" ]
                            ]
                        , li []
                            [ a [ class "page-scroll", href "#team", onClick (Update.ClickedElem "#team") ]
                                [ text "Team" ]
                            ]
                        , li []
                            [ a [ class "page-scroll", href "#contact", onClick (Update.ClickedElem "#contact") ]
                                [ text "Contact" ]
                            ]
                        ]
                    ]
                , text "  "
                ]
            ]
        , section [ id "services" ]
            [-- More content
            ]
          -- Much more content
        , section [ id "contact" ]
            [-- More content
            ]
        ]

-- partial update
 case msg of
    ClickedElem elem ->
        -- Sends the clicked element '#services' to the JS world
        ( model, clickedElem elem )

    ChangeLocation newLocation ->
        ( { model
            | location = newLocation
          }
        , Cmd.none
        )


-- the port
port clickedElem : String -> Cmd msg
port changeLoc : (String -> msg) -> Sub msg


-- subscription
subscriptions model =
    Sub.batch
        [ Window.resizes Update.Resize
        , Update.changeLoc Update.ChangeLocation
        ]

On the JS-side I then use the jQuery animate / scrollTo function, like so:

<script>
  app.ports.clickedElem.subscribe(function(id){
    console.log("Scrolling.");
    // Why isn't this working properly?
    $('html, body').animate({
      scrollTop: $(id).offset().top
    }, 2000);
    // return false;
    app.ports.changeLoc.send(id);
  });
</script>

Finally the new location get's passed back to Elm through a port called changeLoc which just updates my model.

What am I missing here? Is this a problem due to the virtual-dom of Elm? I can't seem to figure this out. The offsets of the provided anchors are all fine and the function get's called as it's supposed to.


Here's the way to fix this issue: Instead of using onClick I used the tip by @ChadGilbert and went for onWithOptions where I set preventDefault to True. The outcome:

onWithOptions "click" { stopPropagation = True, preventDefault = True } (Json.succeed (Update.ClickedElem "#anchor"))

instead of

onClick (Update.ClickedElem "#anchor")

Thanks!

Philipp Meissner
  • 5,273
  • 5
  • 34
  • 59
  • Your partial view does not install `id` attributes, so I'm a little unsure why the line `scrollTop: $(id).offset().top`---which would be `scrollTop: $("#services").offset().top`---would work at all? – Søren Debois May 25 '16 at 08:12
  • I didn't include the corresponding elements. Let me edit the first post quickly. – Philipp Meissner May 25 '16 at 08:14
  • All your `id`s are "contact". I take it in your actual app, you do have `id "selected"` etc.? – Søren Debois May 25 '16 at 09:28
  • Whoops, that was a mistake when copying. Yes, I ensure all the ids are there and in the correct form. Clicking a link in the navigation causes the page to jump to its section flawlessly, just the animation isn't working as expected. – Philipp Meissner May 25 '16 at 09:45
  • 1
    Instead of using `onClick`, can you try using [`onWithOptions`](http://package.elm-lang.org/packages/evancz/elm-html/4.0.2/Html-Events#onWithOptions) and setting `stopPropagation` and/or `preventDefault`? – Chad Gilbert May 25 '16 at 10:06

1 Answers1

1

The links in your header are created with onClick and since you're setting the href parameter, the event will bubble up to be handled by the browser, which is why you are getting the immediate jump.

In Javascript, the way to disable default browser behaviors and event propagation is to use event.stopPropagation() and event.preventDefault().

In Elm, you can create event attributes that set these values by using onWithOptions. You can use the following instead of onClick to suppress any further handling of the click event after your Elm code handles it:

onClickFullStop : msg -> Html.Attribute msg
onClickFullStop =
  let fullStop = { stopPropagation = True, preventDefault = True }
  in onWithOptions "click" fullStop << Json.Decode.succeed
Chad Gilbert
  • 36,115
  • 4
  • 89
  • 97