I'm trying to find a clean solution to working with the browser history in the most effective way. (I'm using GWT, but this question is really more general than that.)
Here's my situation (I would think it's pretty standard):
I have a web application that has several different pages/places/locations (whatever you want to call it), which I display in response to changes to the browser history. Beyond the usual "Home", "Features", "Contact", etc. which are mostly static HTML pages, there is a "User" section, where people can log into their user accounts and (let's call it) a "Project" section, where users can work on their projects.
So, now I simply use local links called #Home, #Features, #Contact, etc., as well as #User, #Project to get to the different pages. And everything is fine, except the following scenario:
If someone opens the link #Project, for example, that person is shown a project login dialog. This login dialog has a cancel button, which I would like to implement simply by calling the browser's back button from my app (easy enough). The reason I would like to do this, is two-fold:
- You can get to this login dialog from several different locations in the app, as well as your bookmarks, and with this approach I wouldn't need to keep track of where the user came from.
- Much more importantly: If I instead did remember where the user came from (e.g. #Home) and linked the user "forward" to return to that place, I get the following effect:
- Let's say the user visits #Home, then #Features, and then clicks on project login.
- If the user clicks the cancel button and I send him "forward" to #Features, a click on the browser's back button afterwards would bring back the login dialog, then again #Features and finally #Home. Not really what you would expect.
- Instead you'd want to get back to #Home immediately, just what you get if I simply implement cancel via the browser's back function.
At this point everything is great, except when the user initially goes to this login dialog via a direct bookmark link to #Project. Because then, if I simply have cancel = back, the user is sent away from the page entirely back to his browser's start-page or wherever he was before. So, in this case, I do need to link "forward" to #Home.
Now, I've tried to think of several ways to fix this and have come up with a couple of solutions, but none seem very desirable to me, but let me share them anyways to maybe stir up some creativity:
- When the page is first opened, grab the history token. If it's #Project or #User or anything else that triggers a cancelable dialog, place the following items onto the history stack: #Home, #Project, where the last one is the saved initial token. This, then, allows my cancel button to function correctly... once... But, if a user clicks back afterwards, he will get the login dialog again (since the original history token is still in the history stack and I don't know how to clear it). A click on cancel then would take him off the page (inconsistent behavior).
- I could instead place #+++, #Home, #Project onto the stack, which would then allow me to catch a back click that would take the user off the page by detecting the #+++ link and simply re-adding the #Home token onto the stack. This would solve the problem and works beautifully overall, except that I hate websites that don't let you back out of them without hammering the back button quickly enough...
- The cleanest solution would be, if I could somehow keep track of the length of the history stack as far as places in my app are concerned. Easy enough at the start: It's got one item on it. But what if I get a sequence of places like this: #Home, #Features, #Home? Did the user click back to get back to #Home, i.e. the history is now length 1 in the browser, or did he click on the #Home link, i.e. the history is length 3? My idea for detecting this would be:
- Define, that #Home is the same as #home.
- Have all links in the page link only to the upper-case version of the link.
- Whenever you get a history change notification that starts with an upper-case letter, immediately add two more items to the history, the first beginning with a lower-case letter and the second also beginning with an upper-case letter. I.e. #Home is turned into #Home, #home, #Home.
- If you ever get a history change that starts with a lower-case letter, you know the user just clicked back rather than a link and you simply click back two more times for him to actually get back to the previous page.
- Now you can distinguish between "backward" and "forward" links and maintain an accurate history model in the code.
- But, unfortunately, there's two problems with this: For one, the browser history get's cluttered with crap (not very elegant) and, second, the system starts to break down, if the user ever clicks back quickly enough so that your app doesn't have time to react to the message.
It seems like this should be a very common problem and I am hoping that one of you can point me to a more useful direction than my thoughts have so far.
window.history
has alength
attribute that will show you how many entries are in the history for the current tab. It is unfortunately not filterable (so there is no way to saywindow.history.localURLs.length
).If you are doing this almost entirely on the client side (i. e. partial updates, very few full-page loads, using hashes or the
history.(push|pop)State
API) then you may want to look into incorporating a client-side routing framework into your application to avoid re-inventing the wheel.Before your user can access #LOGIN dialog, your entry point class is executed. In this class you can remember the last known page. In Login activity/presenter you can add a browser history event handler, which checks if the last known place is null or not. If null, send a user to the #HOME page.
You don't need to ask the browser which places in your app a user has visited. You can remember the entire history of each session, if you want to: i.e. [#HOME, #LOGIN, #FEATURES, #HOME].
How about you have a
Queue<String> historyQueue;
When the page loads for the first time, i.e.
onModuleLoad()
you initalize theQueue
. When you capture a history event, check if its a back or a new history token. If its a back, you pop it out from the queue, if its a new one you add it to the queue. This way, the cancel button is just atoken = historyQueue.pop()
and check if the token is null. If it is, then like you saidback = cancel
, so just do the appropriate thing.