Page scrolls to top after turbo_stream replace

Hey!

Hotwire is super nice and it’s so productive to work with it :slightly_smiling_face:
Yet, I’m struggling with a stupid bug that already took me way too long to fix.

I have a list view with turbo_frame inline editing. Everything works well but when I submit the form, I issue a turbo stream to replace the item and the page jumps back to the top. I really can’t figure out what causes this jump to the top.

Here’s a short video of the issue: CleanShot_2023-07-24_at_18.35.17.mp4 - Google Drive

Do you have any idea how I can fix this?

Regards,
Lukas

It’s hard to tell from just the video, but are you posting the form as a turbo-stream request with the option format: :turbo_stream? It sort of looks like you’re receiving the turbo-stream, but instead of handling the stream action, it’s using a normal Turbo request, which would jump to the top of the page.

Thanks a lot for your reply.

This is the relevant code:

question partial

<div id="<%= dom_id question %>" data-action="click->slideover#open" class="p-6 mb-2 rounded-md border-solid border-2 flex items-center justify-between cursor-pointer hover:bg-slate-100 transition-all duration-200" data-question-url="<%= questionnaire_question_path(question.questionnaire, question) %>">
    <div class="w-2/6">
        <%= form_with model: [question.questionnaire, question], class:"contents", data: {controller: "submit-form", "submit-form-target": "form"} do %>
            <%= turbo_frame_tag dom_id(question, "title_turbo_frame"), class:"contents inline-edit w-2/6 no-slideover" do %>
                <%= link_to edit_questionnaire_question_path(question.questionnaire, question, inline_form: true) do %>
                    <span class="border-transparent border-2 p-1 rounded hover:border-gray-200 cursor-text">
                        <%= question.title %>
                    </span>
                <% end %>
            <% end %>
        <% end %>
    </div>
    <% if question.loading? %>
        <div aria-label="Loading..." role="status">
            <svg class="h-6 w-6 animate-spin" viewBox="3 3 18 18">
                <path class="fill-indigo-200" d="M12 5C8.13401 5 5 8.13401 5 12c0 3.866 3.13401 7 7 7 3.866.0 7-3.134 7-7 0-3.86599-3.134-7-7-7zM3 12c0-4.97056 4.02944-9 9-9 4.9706.0 9 4.02944 9 9 0 4.9706-4.0294 9-9 9-4.97056.0-9-4.0294-9-9z"></path><path class="fill-indigo-800" d="M16.9497 7.05015c-2.7336-2.73367-7.16578-2.73367-9.89945.0-.39052.39052-1.02369.39052-1.41421.0-.39053-.39053-.39053-1.02369.0-1.41422 3.51472-3.51472 9.21316-3.51472 12.72796.0C18.7545 6.02646 18.7545 6.65962 18.364 7.05015c-.390599999999999.39052-1.0237.39052-1.4143.0z"></path>
            </svg>
        </div>
    <% else %>
        <%= form_with model: [question.questionnaire, question], class:"contents", data: {controller: "submit-form", "submit-form-target": "form"} do %>
            <%= turbo_frame_tag dom_id(question, "answer_turbo_frame"), class:"inline-edit w-full text-left" do %>
                <%= link_to edit_questionnaire_question_path(question.questionnaire, question, inline_form: true) do %>
                    <span class="border-transparent border-2 p-1 rounded hover:border-gray-200 cursor-text no-slideover">
                        <%= question.answer %>
                    </span>
                <% end %>
            <% end %>
        <% end %>
        <%= render 'questions/question_assignments', question: question %>
        <div class="ml-6">
            <%= render "questions/status", question: question %>
        </div>
        <div class="relative justify-self-end no-slideover ml-4" data-controller="dropdown">
            <div class="text-gray-500">
                <%= inline_svg_tag "icons/kebab-menu.svg", class: "cursor-pointer h-5 fill-current", data: {action: "click->dropdown#toggle click@window->dropdown#hide touch->dropdown#toggle touch@window->dropdown#hide"} %>
            </div>
            <div data-dropdown-target="menu" class="z-20 hidden mt-2 absolute right-0 dropdown-menu">
                <div class="overflow-hidden bg-white border rounded shadow-sm">
                    <%= button_to t("global.delete"), questionnaire_question_path(question.questionnaire, question), method: :delete, class: "kebab-menu-action-item", data: { turbo_confirm: t("global.confirm_delete") } %>
                </div>
            </div>
        </div>
    <% end %>    
</div>

Questions controller update action

def update
      if @question.update(question_params)
        if params["redirect_to_details_view"]
            redirect_to questionnaire_question_path(@questionnaire, @question)
        else
          respond_to do |format|
            format.turbo_stream { render turbo_stream: turbo_stream.replace(@question) }
          end
        end
      else
        respond_to do |format|
          format.turbo_stream { render turbo_stream: turbo_stream.update("form", partial: "form"), status: :bad_request }
        end
      end
    end

submit form stimulus controller

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "form" ]

  submit() {
    this.formTarget.requestSubmit()
  }

  filter_with_delay(){
    clearTimeout(this.timeout)
    this.timeout = setTimeout(() => {
      this.formTarget.requestSubmit()
    }, 200)
  }
}

I finally found the error. It had a modal stimulus controller on the same page with this line:

    this.element.addEventListener('turbo:submit-end', (event) => {
      if (event.detail.success) {
        if (event.detail.formSubmission.delegate?.element?.localName == "turbo-frame"){
        } else {
          this.close();
        }
      }
    });

This caused the page to always jump to the top after a successful form submit. That was a tricky one to figure out :sweat_smile: