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.
1 <code>// A global a.onclick event handler for all your navigational needs
2 // see e.g. Backbone Boilerplate for a more complete example
3 $(document).on("click", "a", function (ev) {
4 ev.preventDefault();
5
6 // Replace current state before triggering the next route,
7 // storing the scrollTop in the state object
8 history.replaceState(
9 _.extend(history.state || {}, {
10 scrollTop: document.body.scrollTop || $(document).scrollTop()
11 }),
12 document.title,
13 window.location
14 );
15
16 Backbone.history.navigate(this.pathname, { trigger: true });
17 });</code>
[Toggle line numbers]
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()
.
1 <code>// Backbone.Routers trigger the route-event
2 // after the route-handler is finished
3 var router = new AppRouter(); // AppRouter extends from Backbone.Router
4
5 router.on("route", function () {
6 // Inspect history state for a scrollTop property,
7 // otherwise default to scrollTop=0
8 var scrollTop = history.state && history.state.scrollTop || 0;
9
10 $("html,body").scrollTop(scrollTop);
11 });</code>
[Toggle line numbers]
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.