How to implement my own history stack in a single page mobile web application?
Asked Answered
I

5

22

I have a single-page mobile application developed with Backbone and Zepto.

It works correctly with the back/forward buttons in the browser.

When the user navigates to a page, the new content slides in from the right as the old contents slides away to the left (and out of the viewport). I want the same thing to happen if the user presses the "forward" browser button. This all works.

I've got a class that I add to the body element navigate-back that will flip this behaviour, so when the user navigates back with the browser's back button, they see the content sliding back in from the left and the other content sliding into the right. Basically just the opposite of going forward.

I need to detect if the user is navigating backwards so I can invoke the alternate behaviour. I have tried implementing my own history stack, but I've ran into lots of problems where sometimes it marks a forward as a back navigation which ruins the visual cue. It's descended into a kludge of hacks now and probably would only embarrass me if I posted it.

What is the best way to implement my own history stack so I can detect if the user is navigating forward/back in the context of a single-page Backbone mobile application?

Ingham answered 1/6, 2012 at 7:34 Comment(3)
Just to clarify: are you using backbone routing to navigate between pages? Do your pages have an inherent order (going from page B to page A is always "back") or is it just the order in which you are browsing the items?Germanous
@Germanous I have a Backbone router, but I'm not necessarily calling router.navigate() for every change, however I do have a custom event that is fired when each page changes. There is also no defined order of pages.Ingham
Ok the problem I dealt with was related but not the same: I have buttons in the UI that act as back and wanted the browser history to reflect that. I got a partial solution to this: I keep a record of the pages that have been added to the history stack and instead of router.navigate I check if the page is already in the stack and use history.go instead of navigate if it's already there, router.navigate if not. Works in the normal case, but if the user reloads the page in the middle of the app, the state is lost.Germanous
T
23

I don't know about backbone.js1, but I have helped develop a mobile application which had to implement exactly this behavior in html5, so I should be able go give some good advice:

First of all it's good to know that the history.pushState function exists. The big problem with it though is that it is supported up to android 2.3, but not on android 3 till android 4.0.3. As kiranvj points out correctly this can be solved by using the popular history.js library which provides a polyfill solution for the lack of the history functionality.

Now, getting to your actual problem, the way I implemented the history direction animations was by adding data to the pushState function ( history.pushState(data,title,url) ) with which I identified the logical position of the page. In my application I wasn't only limited to a horizontal bar, but in your case you would keep track of position where any new loaded page get's a position which is one higher then your current page. E.g.

History.pushState({position:History.getState().data.position+1},"Your title","Your URL");

Next, when the window.onstatechange or window.onanchorchange event triggers you observe whether the position is higher or lower than your current page (e.g. by using the history.js History.getState() function which I used above) and depending on this you decide in which direction to move (lower is to the left, and higher is to the right), as is illustrated by the image below:

Illustration of history events

You will also note that I already assumed on the first page that you have {position:1}, whereas normally the first page will have no state information. The way this can be achieved is by using history.replaceState which replaces the current empty state with a more informative state. Alternatively you can also check for an empty state on any of the previously mentioned events and if it's empty you assume it to be the left most one ({position:1}).

Hope this helps and if you have any additional questions feel free to ask.

Please note that this answer assumes you are using history.js and you would need to listen to slightly different events (such as onpopstate) and use slightly different structures (history rather than History) if you would want to build your own solution.

It is also useful to note that it is possible to build this with your own queue array which gives you a lot more control, but will not work in combination with the browser's back button. This is a big issue with browser sites, however is far easier in case you are building a cordova (a.k.a. phonegap) web application.


1 Just read about it and it appears to do some history handling of its own, which might make it more complex to integrate the technique described above.

Tantalous answered 5/6, 2012 at 8:25 Comment(4)
This sounds like a good solution. Also, +1 for history.js, even though some of its functionality available within backbone.js; I've heard that backbone's history has some bugs in Safari.Pilewort
Based on the comment with the bounty "The question is widely applicable to a large audience. A detailed canonical answer is required to address all the concerns." I decided to give a universal answer detailing a technique that can be used in any situation, although I did mention in the footnote as well that backbone's own implementation might cause trouble.Tantalous
@DavidMulder: +1 based on the assumption that history persists on a page refresh and/or navigating to an external url and back. Correct me if I'm wrong, but wouldn't it be a giant privacy loophole? i.e. any page that has access to history would be able to track the user's previous sites visited within the session.Anallese
@o.v.: You seem to be misunderstanding that it would be possible to read out the history. The only thing that is possible to associate 'state information' with a certain page load which you can read out again when the user navigates back and forth using the browser navigation. Of course, within your own page you already know the current page so that makes things like transitions possible.Tantalous
E
1

I had the same issue when working with Zepto on mobile with single page - multiple views.

Initially I used html5 statechange and onhashchange. It all have some issues in one or other mobile device. Finally I used Zepto history plugin from here https://github.com/browserstate/history.js

It somewhat solved most of the issues. Try it, it will be useful, it handle html4 and html5 features wherever possible.

Epagoge answered 3/6, 2012 at 5:11 Comment(5)
History.js is a really helpful library, but how it can help to detect browser back button click?Lexine
I could not see anything in this library to detect if the user was navigating backwards. Are you able to elaborate?Ingham
@Epagoge You should make a fiddle, a zip isn't very convenient.Ingham
I have doubts whether this will work fine with fiddle because we are using browser back and forward buttons in the demo.Epagoge
@kiranvj: Your code isn't giving an example of how to implement the direction behavior alex is asking for (you only show a basic history example), nor does the history.js library help with that directly. (just added this comment to explain my downvote)Tantalous
A
1

If you're working on a true single-page app, why not you set up an array to hold history urls in a js variable (as opposed to relying on something like history.pushState and its support)?

Whenever a new page is navigated to, you can push its url into the array, whenever a "back" button is pressed, you can retrieve the url needed as far back as you want. This will work perfectly as long as you correctly discard urls when the user goes back a few steps and then navigates to a new link.

I've never tried implementing this to be used for page history, but this worked perfectly well for in-page undo-redo logic.

Update:

After further research, the approach above would not work for a page reload as it would be an action occuring outside of history handling available through JS. It would still work for tracking back/forward transitions, but such history will be lost on navigating to a url external to the app or a page refresh. David Mulder's answer seems to lack this limitation by relying on browser-level history that persists outside of the page scope.

Anallese answered 6/6, 2012 at 23:28 Comment(3)
How does one determine if a user is going back, forward or reloading though?Ingham
@alex: one could check whether or not the url matches the current/previous/next url in the history; I am now however under the impression that this could be done with backbone more natively. Will update when confirmedAnallese
The problem with this is (as I mentioned in my answer) that it's unacceptable inside a browser where the native browser button has to work, although it is a fairly easy solution to execute if working in an environment like cordava.Tantalous
T
-1

Use this thing in single page mobile application this will allow to the history and move the user to back.

function onBackKeyDown() {
    history.go(-1);
    navigator.app.backHistory();
}
Tertiary answered 27/11, 2015 at 13:36 Comment(0)
T
-2

Sammy.js's v.6.x branch (the one that relies just on hash changes) is a perfect, simplest, most browser-compatible approach to tracking history. There, history is not tracked at all, as Sammy just watches for hashchange.

Relying on "#/slide/123" allows you to support hard page reloads, and simplifies the work

Peel off the last part (slide number) on each page view, push into global. On new route, see if number is more or less than what is stored in global and do the correct (left or right) animation. If global is undefined, no animation.

Taurus answered 9/6, 2012 at 20:0 Comment(6)
You are describing the same technique as I did in my answer with a number of notable disadvantages: hash change history manipulation rely on polling, have a lot of browser quirks and should thus not be used if pushState exists. An application can not be described in slides, so using your method what you would get is: #page/nameOfPage/numberDescribingPosition, next if somebody where to copy the url the numberDescribingPosition wouldn't make sense anymore.Tantalous
@David good points. Except hash changes don't always rely on polling. There is a hashchange event.Ingham
If there is no order to pages, then left, right sliding effect does not make sense to me. Especially the slide back effect. It's a matter of taste, but I would commit to one of two: Either pages have order and, thus, highlight it with slide left, right effect, or they don't have order and pick some other effect that does not make user they are sliding through pages. Even if there is no order, you can still keep "last page's name" in some global, so if the new hash = name of "previous" page, instead of sliding forward, you slide back.Taurus
@ddotsenko: the order is of course the order in which the user visited the pages. Now, the entire question is about the thing you are proposing in your last sentence I believe, but the problem with your proposed solution is that it will get messed up once you get move around for example (back twice, forward twice) and you would need to keep custom arrays of pages in that case (ending up with a solution similar to mine, except that the state data is not linked to the actual state but to the position in an array (causing problems with duplicate names).Tantalous
@alex: Wow, I have created various applications by now requiring statefullness including history management and didn't know about the hashchange event, my bad. Hmm, the only two browsers that matter which do support hashchange and not popstate are (sadly) IE8 and 9 (30% market share), so depending on your audience that might be quite useful yes. Still, if you need to have older browser support and you do need the polling etc. it's quite a pain in the *** ('played' around with it once, but ended up using a library).Tantalous
Backbone.history + Backbone routing uses this technique already. There's no reason to roll your own.Stu

© 2022 - 2024 — McMap. All rights reserved.