Manipulating History for Fun and Profit.
I was stuck for a good day and a half debugging some navigation issues on my site caused by some incorrect usage of the HTML5 history API. I think they're pretty easy mistakes for a beginner to make.
First a quick description of my app. It's implemented as one HTML page, but internally, I keep track of a stack of "views", with one view visible at a time. The user can navigate forward and backwards between the views. The views are actually individual <div> elements, but only one, the "top", is visible at a time. I have my own pushView() and popView() functions. It would be nice if the "back" button to mimic the view navigation, ie it would call popView().
It seems straightforward...
The solution seems pretty simple. Every time I navigate forward, I call:
history.pushState(data, title, url);
Then I need an onpopstate handler to handle the back button click. If I'm navigating from one of the my views, I would call popView().
Now I noticed in Chrome, I would sometimes get onpopstate events at weird times, like first thing after I load a page. It's easy to avoid these as long as a provide history data (the first param in pushState). Then I can check the onpopstate events to make sure I have data before I act on it.
The first thing that caught me is that onpopstate will return with the state data of the destination state, which is most likely NOT that last state you pushed. Here's an example. First you land on a page your history state would look something like this:
1. Landed on page state. state data = null.
Then you call pushState("mydata"). Your history state would then be:
1. Landed on page state. state data = null.
2. pushState("mydata"). state data = "mydata".
You then click the browser "Back" button. To me at least, intuitively, I expected an onpopstate event with "mydata". Instead, I get null. What happens is that state 2 is popped, and state 1 is the page that should be visible now. The onpopstate event carries the state data for state 1, which is null.
I had a bug where I couldn't navigate back to state 1 because I was checking for state data. The fix for me was to use history.replaceState("main") to set the state data for state 1.
Unexpected states from links
My second source of confusion was that I used a combination of <button> and <a href="#"> elements to allow users to navigate my web app. In both cases, Javascript handlers actually did the navigation.
The problem was the <a href="#"> elements. Even though they did not navigate away from the page, clicking on them would have the side effect of adding an unexpected entry to the history state queue. Make sure you add event.preventDefault() to the click handler for <a href="#"> links to avoid this.
Navigating back multiple pages is tricky
At some points in my app, I wanted to navigate back a two steps. So I tried calling history.back() twice within a click handler. This didn't work. I only got one onpopstate event, going back one page.
The second approach I tried was to call history.go(-2) instead of calling history.back() twice. The result of this was again a single onpopstate event, but this time, it gave me the state data from two pages back. I would have to have my own code to check the state data and figure out how many times I really wanted to pop my views. I haven't figured out how to handle the case where a view may be in the stack more than once.