LiveView element scroll position reset by hiding/unhiding sibling element

I have a menu within a LiveView that I am hiding/unhiding based on a boolean that is toggled on button click. I also have a chat box that auto-scrolls downward as new items are added, via scrollIntoView in a Hook.

The problem: when I click the menu button to toggle the menu, the chatbox scroll position gets sent to the top of the box.

Notably (and unexpectedly), phx-update="ignore" on the div containing the chat messages does not prevent this behavior, and is not really an option as new messages are added via a handle_info in the LiveView, necessitating phx-update="append".

One hacky fix would be to scroll the chatbox to the end every time the menu is toggled, but there has to be a better way :stuck_out_tongue: Thoughts appreciated!

Code below (note, interstitial/irrelevant elements removed)

The parent LiveView:

<div>
<!-- In root LiveView -->
  <button id="toggle-menu" phx-click="toggle_menu"></button>

  <%= if @show_menu do %>
    <%= link "Some link", to: "/some_link" %>
  <% end %>

  <!-- The chat component -->
  <%= 
    live_component @socket,
    MyAppWeb.PageLive.ChatComponent,
    id: "chatbox-section",
    messages: @messages
    # etc
  %>
</div>

The toggle menu click event:

def handle_event("toggle_menu", _params, socket) do
  {:noreply, assign(socket, :show_menu, !socket.assigns.show_menu)}
end

The chat LiveComponent:

<section>
  <div id="chatbox-feed" phx-update="append">
    <%= for message <- @messages do %>
      <div id="msg-<%= message.id %>"
        phx-hook="NewMessage"
        phx-update="ignore"
      >
        <div class="msg-info-name">
          <%= message.username %>
        </div>
        <div class="msg-text">
          <%= message.text %>
        </div>
      </div>
    <% end %>
  </div>
</section>

The message hook:

Hooks.NewMessage = {
  mounted() {
    // Scroll to this message
    this.el.scrollIntoView({block: "end"});
  }
}

From what you’ve posted, ChatComponent looks like it’s a stateless component:

A stateless component is always mounted, updated, and rendered whenever the parent template changes. That’s why they are stateless: no state is kept after the component.

Each time you re-assign :show_menu it gets re-rendered, thus losing any kind of state (including client side state like scroll position).

1 Like

Thanks for the response and insight – apologies, I deleted the ChatComponent id from the code I posted initially; I am passing an ID to the ChatComponent like so:

      <%=
        live_component @socket,
        MyAppWeb.PageLive.ChatComponent,
        id: "chatbox-section",
        messages: @messages,
        # etc
       %>

I am then using this ID on the root tag for this component. I suppose this isn’t enough to make it stateful? :stuck_out_tongue: