elmelm-port

How to auto-scroll to the bottom of a div in Elm


I add <div>s to a wrapper <div> and I need to be able to scroll to the last one added each time. How do I do this in Elm?

<div class="messages" style="height: 7em; overflow: scroll">
  <div>Anonymous: Hello</div>
  <div>John: Hi</div>
</div>

Intuitively, it seems like I could call a port that runs the JavaScript code element.scrollTop = element.scrollHeight:

AddChatMessage chatMessage ->
  ( { model | chatMessages = model.chatMessages ++ [ chatMessage ] } , scrollToBottomPort "div.messages" )

The problem is scrollToBottom gets called before the model gets updated. So no big deal, I convert that into a Task. But still, even though the model gets updated first now, the view does not get updated yet. So I end up scrolling to the 2nd item from the bottom!

This maybe leads to a more general question I'm curious about in Elm, how do you run a Cmd after the view is updated due to a change in the model?


Solution

  • You could use a port to scroll to the bottom of a list.
    In that case, you need to set a javascript to wait for 1 AnimationFrame before doing the scroll. To make sure your list is rendered.

    An easier way to do this is to use Elm's Dom.Scroll library.
    And use the toBottom function.

    If you include that in your code like this:

    case msg of
        Add ->
            ( model ++ [ newItem <| List.length model ]
            , Task.attempt (always NoOp) <| Scroll.toBottom "idOfContainer"
            )
    
        NoOp ->
            model ! []
    

    It should work. I've made a working example here in runelm.io