-
Notifications
You must be signed in to change notification settings - Fork 432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Option to maintain current scroll position? #37
Comments
Would a pair of let scrollTop = 0
addEventListener("turbo:click", ({ target }) => {
if (target.hasAttribute("data-turbo-preserve-scroll")) {
scrollTop = document.scrollingElement.scrollTop
}
})
addEventListener("turbo:load", () => {
if (scrollTop) {
document.scrollingElement.scrollTo(0, scrollTop)
}
scrollTop = 0
}) |
@seanpdoyle That works in a pinch! 👏 |
Could you elaborate on how to implement this? I'm also looking for a similar solution, but I'm not exactly sure how to make it work. 🤔 |
We have a slightly different requirement. Instead of one HTML element, we have several elements for which we wanted to retain the scroll position. It is a Kanban board with multiple columns. The following worked for us:
We are essentially setting a unique value for |
Is there a solution to keep scroll position during form submission? |
This solution doesn't seem to be working anymore. It seems as though turbo is scrolling back to the top after the |
Our solution had been to set scroll in import { Controller } from '@hotwired/stimulus'
import { Turbo } from '@hotwired/turbo-rails'
export default class extends Controller {
connect() {
if (window.previousPageWasAPaneLaunchPage) {
document.removeEventListener('turbo:before-render', disableTurboScroll)
document.removeEventListener('turbo:render', pageRendered)
}
document.addEventListener('turbo:visit', fetchRequested)
document.addEventListener('turbo:before-render', disableTurboScroll)
document.addEventListener('turbo:render', pageRendered)
}
disconnect() {
document.removeEventListener('turbo:visit', fetchRequested)
}
}
function fetchRequested() {
window.previousPageWasAPaneLaunchPage = true
window.paneStartPageScrollY = window.scrollY
}
function disableTurboScroll() {
if (!window.previousPageWasAPaneLaunchPage) Turbo.navigator.currentVisit.scrolled = true
}
function pageRendered() {
if (window.previousPageWasAPaneLaunchPage) window.paneStartPageScrollY = null
if (window.paneStartPageScrollY)
window.scrollTo(0, window.paneStartPageScrollY)
} Here's the corresponding pane controller, which is added to the import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
connect() {
window.previousPageWasAPaneLaunchPage = false
}
} |
@daniel-nelson writing to I have a hunch that the changes made in 539b249#diff-78d8451f964182fd51330bac500ba6e71234f81aad9e8a57e682e723d0f517b3R105-R106 are the cause of the issues in Could you try and boil down your use case to the original work-around's HTML and JS? I'd like to use that as a test case to revert whatever regressions were introduced in 539b249. |
@tobyzerner does that behavior occur when using the changes introduced in #476? That PR attempts to better synchronize the after-load scrolling to occur within the next available animation frame. Your comment is from October 18, long before 539b249, so I'm curious if |
@daniel-nelson thanks for posting you comment! I was trying to solve exactly the same problem today while trying to migrate my project from turbolinks to turbo @seanpdoyle In my app the majority of interactions are centered around action buttons. Since I didn't want to do granular updates to the interface I decided that I would just code them as very simple stimulus controllers that would make an ajax call and then reload page (or optionally redirect to a different one) on success. Keeping the scroll position was challenge with the Turbolinks as well, I've found one of the solutions similar to what @daniel-nelson did, but in terms of turbolinks events. This is how my reload page function looks now when adopted to turbo code:
and that's the gist of my action controller:
I think Turbo might be missing one of the possible actions for the What could be an optimal way to solve this on your opinion? I would be happy to do a pr |
@can3p Turbo uses
If your Stimulus controller invokes |
No,
|
@seanpdoyle Unfortunately #476 does not fix the workaround. The problem is that Turbo is setting the scroll position after |
@seanpdoyle Here is a minimal Rails project reproducing the issue: https://github.com/daniel-nelson/turbo_scroll_pane. The latest commit reverts the |
@seanpdoyle I should have specified in the project readme that you need to have a narrow viewport to see this properly. On desktop, we just show the new pane. When building this minimal app, I had a weird scaling issue when using the actual Chrome devices simulator. But it worked fine just dragging the window to a mobile width. I'll go update the readme now, but wanted to mention it here in case you already cloned. |
Is there some native Is there any easy way to do this? And if so, how would you target just specific elements that you do not want to make the page scroll top? |
For now I use a simple stimulus controller which is not the most efficient way: import { Controller } from "stimulus";
export default class extends Controller {
initialize() {
this.scrollTop = 0;
}
connect() {
if (this.scrollTop) {
this.element.scrollTo(0, this.scrollTop);
this.scrollTop = 0;
}
}
position() {
this.scrollTop = this.element.scrollTop;
}
} <nav id="sidebar-nav" data-controller="tree-scroll" data-action="scroll->tree-scroll#position" data-turbo-permanent >
...
</nav> I believe that most should happen on disconnect but in my case the |
I created a small utility to freeze the scrolling for the next visit/render: const freeze = () => {
// @ts-ignore
window.Turbo.navigator.currentVisit.scrolled = true;
document.removeEventListener("turbo:render", freeze);
};
export const freezeScrollOnNextRender = () => {
document.addEventListener("turbo:render", freeze);
}; Then I simply use it where needed: import { Controller } from "@hotwired/stimulus";
import { freezeScrollOnNextRender } from "utils";
export default class extends Controller {
static targets = ["saveDraft"];
declare saveDraftTarget: HTMLButtonElement;
saveDraft() {
freezeScrollOnNextRender();
this.saveDraftTarget.click();
}
} I could make it so every form submission kept the scroll by just have a submit action in my stimulus controller: import { Controller } from "@hotwired/stimulus";
import { freezeScrollOnNextRender } from "utils";
export default class extends Controller {
static targets = ["form"];
declare formTarget: HTMLFormElement;
submitForm(event: Event) {
event.preventDefault();
freezeScrollOnNextRender();
this.formTarget.requestSubmit();
}
} |
@dillonhafer can you elaborate more? I am having this issue. I don't get why when you submit a form it scrolls you all the way up to the top. In a landing page with a form at the end is just a ridiculous behavior. 😒 |
BTW I get |
It is quite normal behavior to scroll the page to the top after a form submission, this is what browsers do. Turbo is merely following this standard behavior. |
Sorry @dillonhafer I said it bad. It happens when the form have errors and not when the form is submitted. |
It really needs to have something like InertiaJS https://inertiajs.com/links#preserve-scroll |
Finally made it work using |
Could you elaborate a bit further, please? What exactly did you do? |
https://turbo.hotwired.dev/reference/frames I thought the content of
|
For me I wanted to keep my left sidebar scroll intact when changing page. Here is what I have done
Only requirement is to have a unique ID on my sidebar. |
Before I added Hotwire, I had a form whose
This worked great. But, when I added Hotwire, and Turbo Drive takes over the form submission, it ignores the |
This was my solution: import * as Turbo from '@hotwired/turbo'
if (!window.scrollPositions) {
window.scrollPositions = {};
}
function preserveScroll () {
document.querySelectorAll("[data-preserve-scroll").forEach((element) => {
scrollPositions[element.id] = element.scrollTop;
})
}
function restoreScroll (event) {
document.querySelectorAll("[data-preserve-scroll").forEach((element) => {
element.scrollTop = scrollPositions[element.id];
})
if (!event.detail.newBody) return
// event.detail.newBody is the body element to be swapped in.
// https://turbo.hotwired.dev/reference/events
event.detail.newBody.querySelectorAll("[data-preserve-scroll").forEach((element) => {
element.scrollTop = scrollPositions[element.id];
})
}
window.addEventListener("turbo:before-cache", preserveScroll)
window.addEventListener("turbo:before-render", restoreScroll)
window.addEventListener("turbo:render", restoreScroll) <nav id="sidebar" data-preserve-scroll>
<!-- stuff -->
</nav> |
If you're submitting a form and want to maintain its scroll position, simply wrap it in a |
const TurboHelper = class {
constructor() {
if (!window.scrollPositions) {
window.scrollPositions = {};
}
document.addEventListener("turbo:click", (event) => {
if (this.isInsidePreserveScrollElement(event.target)) {
window.scrollPositions['page'] = window.scrollY;
}
});
document.addEventListener("turbo:before-render", () => {
if (window.scrollPositions['page']) {
requestAnimationFrame(() => {
window.scrollTo(0, window.scrollPositions['page']);
});
}
});
}
isInsidePreserveScrollElement(target) {
const preserveScrollElements = document.querySelectorAll('[data-preserve-scroll]');
for (let element of preserveScrollElements) {
if (element.contains(target)) {
return true;
}
}
return false;
}
} // entry point file
import './../bootstrap';
import './../turbo-helper'; <nav role="navigation" class="flex items-center justify-between" id="pager" data-preserve-scroll>
<div class="flex-1 flex items-center justify-between">
<div>
<span class="relative z-0 inline-flex">
your links ....
</span>
</div>
</div>
</nav> |
I don't see any movement on this issue, so want to explicitly state here: while there's a bunch of hacky workarounds for this issue, it's a crazy solution to fight Turbo and try to undo its scrolling. Like any hack, it's brittle, it's not a proper solution, and it might result in a bad UX in a pinch. We need an option to simply disable the scrolling behavior for a particular request. |
@andreyvit We'll get it in the upcoming Turbo 8, and with a way more powerful system (but apparently not too complex). See Turbo with Morphing: https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/ |
This works for me perfectly:
I have a long sidebar with nested elements and it can have a scroll. Normally when you choose something in the bottom and click on it, sidebar jumps to the top, and you don't see what you just have clicked. With this snippet it is kept in place, no flickering, works like a charm. |
Worth noting regarding Turbo 8 and morphing — this new system does allow you to retain scroll position, but only for Page Refresh events, meaning only for when you're redirected back to the same page you're already observing. I don't believe Turbo 8 will allow you to retain scroll position when simply navigating from one page to another in a traditional "click a link, click another link" sense. Even retaining the scroll position in Turbo 8 is just a nice side-effect — the system is designed around morphing after a form POST/PATCH. So, since this issue seems to be written more generically, I don't think Turbo 8 will / should close it. |
Just in case it helps someone else, this issue for me was caused by duplicate IDs on text fields. |
Yup I flagged this bug last week: hotwired/turbo-rails#575 (comment) Maybe I should open an issue Edit: #1226 |
Probably not the answer anyone is really looking for this issue specifically, but we solved this by using Turbo Frames instead of reloading the entire page. You can put almost the entire page into a single frame and send it as part of the response. Turbo will replace only that frame and maintain scroll position. |
Was literally going to write the same thing. In our case we needed the entire page to reload without the scroll issue. We solved this by splitting the page into it's logical two parts where all of the page is in one of either frames. Then in the controller we use turbo streams to simply re-render both frames. This achieves the same reload we needed before but without any scrolling. The ONLY downside is having to put turbo specific code in the controller which sucks a bit, but in the end, if we're aiming for SPA like UX then it's probably good to be explicit about it 🤷 |
I'm using a modified version of @vmiguellima's workaround. My fix makes it so scroll is also preserved when pressing "back" button after regular navigation.
I really wish this was built-in into Turbo 🙏 but at least we have the workarkound ;) |
In our case we do not want to maintain the original scroll position - but scroll to a specific HTML element once the page is rendered. For example: you submit a form and then the server responds with 422 as one form field did not validate. We then have a Stimulus controller that would automatically scroll the non-validating form field into view automatically. However, this doesn't work as the Stimulus controller (using I feel like there should be a way within Turbo to be able to handle such situations - I mean scrolling to an erronenous form field after submission shouldn't be that uncommon? Yes, you can wrap the |
@fritzmg Seconding the request to handle scrolling as part of Turbo navigation (maybe if the response redirect has an anchor, we could scroll to that anchor?) I guess the current canonical solution is to respond with |
Me neither and I absolutely do not understand why it is the way it is. It is very bad UX! |
Just to make sure: You are aware of this rather buried possibility?
|
I'm not arguing that it is a good or preferred idea, I'm merely stating that if you use a browser without javascript, and you submit a form, then the next page will be at the top when the browser renders the next document. I'm not arguing for it, I'm just saying browser have behaved that way since the 1990s. |
Yes, I am aware of that. It just shows again how developers have to fight the web standards in some way or another. |
Tried it and it leads to an error |
Yes, this is what I'm currently doing. However, this has a problem if you want it both ways — maintain scroll position for some form actions (say I have buttons to add/remove line items), but reset for others (when navigating away). I think there were other problems as well (perhaps redirects weren't being processed), because I couldn't use this approach everywhere I wanted. Sorry, I do not recall exact details now. (Update: also like the issue description says, “I could use Frames in this scenario instead, but then I'd lose the URL history/back button/etc”.) I haven't tried the latest Turbo 8 page refresh feature yet (with turbo-refresh-scroll); it probably solves this nicely, and seems to be targeting this exact use case. |
@MarcusRiemer But I guess a larger point is that what we'd all like to get from this ticket is what @fritzmg said:
That's definitely one of the motivating use cases for me, and I imagine there are other cases where you'd love to handle scrolling yourself, but find Rails overriding it. So it's less of “preserve scroll position” and more of “don't mess with the scroll position”. |
Yes, it does, but only if you're redirecting back to the same location the form is on (long post here) |
On a regular Drive page update, it's scrolling up to the top of the page. Normally that would be desired, but on some Drive links I want to maintain the current scroll position. I tried looking for a
data-turbo-preserve-scroll
option or something like that but couldn't find anything. I could use Frames in this scenario instead, but then I'd lose the URL history/back button/etc.The text was updated successfully, but these errors were encountered: