Unable to scroll to bottom of page after turbo event

Simply put, I’m polishing off the chat app and I would like to pull the scroll bar down when new items are added to the chat. Does anyone know how to fire a JS function after a turbo stream channel transmission concludes? I’m simply trying to scroll down the container with id conversation_comments after a message is added to the page.

   <%= turbo_stream_from @conversation %>
        <div class="chat-room" id="chat-room-container">
          <%= turbo_frame_tag "conversation" do %>
      .................
          <% end %>
          <div class="convo-comments-container-<%= @conversation.id %>" id="conversation_comments">
            <%= render @conversation.conversation_comments %>
          </div>
        </div>
        <div class="chat-box">
          <%= turbo_frame_tag 'new_conversation_conversation_comment', src: new_conversation_conversation_comment_path(@conversation), target: "_top" %>
        </div>

<script>
  $('#conversation_comments').animate({scrollTop: 20000000}, "slow");

  const msgs = document.getElementById('conversation_comments');

  function getMessages() {
      // Prior to getting your messages.
      shouldScroll = msgs.scrollTop + msgs.clientHeight === msgs.scrollHeight;

      // After getting your messages.
      if (!shouldScroll) {
          scrollToBottom();
      }
  }

  function scrollToBottom() {
      msgs.scrollTop = msgs.scrollHeight;
  }

  scrollToBottom();

 // setInterval(getMessages, 2000);

  document.addEventListener("turbo:render", function (){
      getMessages();
  });
</script>
1 Like

I ran into this. My solution was to scroll into view when the new stuff is inserted into a DOM. A really simple Stimulus controller did the trick:

export default class extends Controller {
  connect() {
    this.element.scrollIntoView()
  }
}

So in your stream you’d append something that was attached to that controller. Say you had a chat line, you’d do:

<turbo-stream action="append" target="chat_lines">
  <template>
    <article data-controller="chat-line">
      <!-- some chat line -->
    </article>
  </template>
</turbo-stream>
1 Like

I’m using the rails version of turbo & never had time with stimulus before now. Maybe you can help me a little bit more. Since rails generates views - the way my system looks a tad bit unique compared to your example.

my structure looks like the one below.

    <div class="col-6">
     <turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-name=".....">
    </turbo-cable-stream-source>
    <div class="chat-room">
    <turbo-frame id="conversation">
    <div class="chat-room-title">
      <h4>Fun Stuff</h4>
    </div>
    </turbo-frame>

// chat messages below

<div data-controller="conversation" class="convo-comments" id="conversation_comments">
 div.id = conversation_comment_1
 div.id = conversation_comment_2
</div>

// chat box form |
<%= turbo_frame_tag 'new_conversation_comment', src: new_conversation_comment_path(@conversation), target: "_top" %>

<turbo-frame id="new_conversation_conversation_comment" src="/conversations/wwww/conversation_comments/new target="_top"
</turbo-frame>

conversation_controller.js`

    import {Controller} from "stimulus"
    export default class extends Controller {
        connect(){
      this.element.scrollIntoView() 
       }
    }

yeah so if i’m understanding correctly, it looks like you need to hook up your “conversation comments” to the Stimulus controller, rather than the conversation itself. Every time one of those comments is added to the DOM, the connect method will be called and thus you’ll get scrolled to the bottom.

If I use the data-target attribute on a comment itself – can I track when its being inserted into the container using the conversation_controller.js connect method?

<div class="p-1 conversation_comment_container d-flex" id="<%= dom_id(conversation_comment) %>">
   <div class="flex-shrink-0">
     <img src="https://placekitten.com/50" alt="Placekitten" class="rounded-circle">
   </div>
  <div class="flex-grow-1 pl-3">
    <div class="conversation-comment">
      <small><strong><%= conversation_comment.user.full_name  %></strong> <span><i class="fas fa-pencil-alt text-success"></i></span> <span class="text-pink"><%= time_ago_in_words(conversation_comment.created_at) %></span> ago</small>
      <br>
      <%= conversation_comment.content %>
    </div>
  </div>
</div>

yeah, connect gets called when you add something to the DOM. So when your turbo stream appends your comment, connect gets called, so long as it’s on the comment

this works on initial load

 connect() {
        //this.element.scrollIntoView()
        const {scrollHeight, clientHeight, offsetHeight} = this.element
        if(scrollHeight >= clientHeight) {
            this.element.scrollTop = scrollHeight - clientHeight
        }
    }

now I have to get it to pickup on turbo events that are taking place inside of the div

Not sure Turbo has the events you want. The best approach here does seem to be to rely on the connect method being called on the comments that are added. A la:

<div class="conversation-comment" data-controller="conversation-comment">
</div>

The Stimulus docs are fairly good on this: Stimulus Reference: Lifecycle Callbacks

Oddly, its not running when I add new comments. Even though, data-controller attribute is present on the comment div itself.

Ah weird, not sure what’s going on there.

Are you using stimulus_include_tags in Rails? That’s what hotwire:install generates. If so, maybe it’s an issue with the dash naming? What if you had a comment_controller.js and did data-controller="comment"?

Failing that, I’m out of ideas.

Trying to solve the same problem. Is there a way to get Turbo to reload a parent element (disconnect then reconnect) whenever a new child element is added? Something similar to below? If so, I think I can solve the issue.

<div class="is-parent" >  //Disconnect then reconnect this from the DOM
<%= render @conversation.comments %> //Whenever a new child element is added
</div>

On the other hand, if there’s an alternative to the success Lifecycle callback in Stimulus Reflex, maybe that could be an option.

I tried using the turbo callbacks as well…those don’t work either.

I found the solution. Stimulus example: smart scroll · GitHub

1 Like

Hi, I’ve found a really simple solution:

= turbo_stream_from @chat_channel
div class="flex flex-col h-screen overflow-hidden w-full"
  div class="flex-1 overflow-y-auto w-full p-4 pb-20" id="messages-container-chat-channel-#{@chat_channel.id}" data-controller="chat-scroll" data-action="new-message-event-dispatcher:new_message@window->chat-scroll#scrollToMessage"
    - @chat_channel.chat_messages.each do |chat_message|
      = render ChatChannels::ChatMessagesComponent.new(chat_message: chat_message)

  = render ChatChannels::ChatMessages::FormComponent.new(chat_channel: @chat_channel)
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="chat-scroll"
export default class extends Controller {
  connect() {
    this.#scrollToBottomSmooth()
  }

  scrollToMessage(event) {
    this.#scrollToBottomSmooth()
  }

  #scrollToBottomSmooth() {
    this.element.scroll({
      top: this.element.scrollHeight,
      behavior: "smooth"
    })
  }
}
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="new-message-event-dispatcher"
export default class extends Controller {
  connect() {
    this.dispatch("new_message", {})
  }
}

and finally, this is broadcasted from the model

= turbo_stream.append "messages-container-chat-channel-#{chat_message.chat_channel_id}" do
  div data-controller="new-message-event-dispatcher"
  = render ChatChannels::ChatMessagesComponent.new(chat_message: chat_message)

Maybe this could be improved, but it works for me so far :slight_smile:
The key I think is the connection between controllers taking advantage using dispatch