Managing scrollTop in your Backbone single-page app
19:04
One of the great caveats of single-page apps is that they rely on taking over many of the browser’s default behaviours. One of the more significant ones is how the browser behaves on page-to-page navigation, be it forwards or backwards. By default, the browser will take you to the top of the page when you navigate forwards by following a link. In contrast, you expect to be returned to the position on the page where you followed a link, when you use your browser’s back-button.
In case you happen to be using Backbone to power your single-page app, this behaviour can be restored with very little effort and code. Unfortunately, this will only work in browsers supporting the History API, so old-IE (IE 9 and earlier) users are out of luck.
To pull it off, a global navigation event listener is used to store the current scrollTop
just-in-time.
<code>// A global a.onclick event handler for all your navigational needs
// see e.g. Backbone Boilerplate for a more complete example
$(document).on("click", "a", function (ev) {
ev.preventDefault();
// Replace current state before triggering the next route,
// storing the scrollTop in the state object
history.replaceState(
_.extend(history.state || {}, {
scrollTop: document.body.scrollTop || $(document).scrollTop()
}),
document.title,
window.location
);
Backbone.history.navigate(this.pathname, { trigger: true });
});</code>
Listening to the route
event from your Backbone.Router
you then restore scrollTop
when it exists in history.state
, or default to page top. To catch navigation happening via the back- and forward-buttons in the browser, Backbone.history
should be set up to listen for popState
events by setting { pushState: true }
in the options to Backbone.history.start()
.
<code>// Backbone.Routers trigger the route-event
// after the route-handler is finished
var router = new AppRouter(); // AppRouter extends from Backbone.Router
router.on("route", function () {
// Inspect history state for a scrollTop property,
// otherwise default to scrollTop=0
var scrollTop = history.state && history.state.scrollTop || 0;
$("html,body").scrollTop(scrollTop);
});</code>
For those who need to see before they believe, take a look at the demo.
NB. The code examples are simplified to the extreme and shouldn’t be treated as complete drop in code.
Sorry, the comment form is closed at this time.
4 Responses to “Managing scrollTop in your Backbone single-page app”
This is a clever solution- however I fear there is a problem: the state of the scroll is only saved when a link is clicked; but clicking on a link is not the only way to navigate: What happens if you click the back button? The link handler will never be called and so the state will not be stored.
Comment by Richard Hunter — 11.12.2016 03:30
Richard,
Thank you.
You’re right, scroll position is not stored on back/forward navigation, only recalled after the fact. There is, though, a note on how to add that missing functionality to your application.
Quickly trying out my (admittedly very simplistic) demo, both Firefox and Chrome managed to recall vertical position on their own for forward navigation. Of course the situation would probably be quite different if the page loaded any slower than instantly.
Comment by nikc — 11.12.2016 13:37
Unfortunately, pop state wont help you in this case because when it occurs it represents the state of the incoming page, not the one you are on at the moment.
An alternative technique is to have a scroll handler which periodically saves the scroll amount whenever the user stops scrolling so the history state always has the most up to date value.
Here is a Github repo of my experiments regarding this.
https://github.com/Richardinho/scroll-restore-for-spas
Comment by Richard Hunter — 05.03.2017 23:04
Richard, you’re right, you can store scroll state using an onScroll handler, but a more efficient way to do that is decorating the history handler, i.e. doing a scroll state serialisation just in time when history.pushState is called. This will of course rely on using an abstraction on top of the browser’s History API, but I think that’s perfectly acceptable.
Comment by nikc — 21.04.2017 14:23