Our Back Button Story

By james

Ah, the back button. That thing users want and don’t understand why it isn’t so simple anymore. Our project has a user story for the back button. It described a specific set of scenarios and what the users expectations were for when the back button was clicked. Suprisingly enough, it was easily one of the biggest stories for us in terms of difficulty and time to implement. I just wanted to talk through our path to making this story work. The back button is a fun, difficult challenge to be sure ;)

For the impatient, I’ll lay it out for you right up front. Put simply, the approach we were able to take to tackle the back button was as follows:

  • Have a client-side model that represents the state of the app (the user’s search criteria in our case)
  • Serialize that model (JSON) and post it to an iframe in response to user actions (actually, ours had a 300ms dirty-check interval that would post to the iframe).
  • Have the content that is loaded in the iframe notify (call a function) on the parent window passing it the result and the model used to get that result (basically just echo back the serialized model alongside the results).

And the longer-winded walk-through:

The site is for a real estate company. It allows users to find listings based on the typical (and some custom) criteria they specify via some widgets on the page. As they make their selections, the results are ajax-erifically loaded for them in the results area. They can page through the results and click on a listing to see further detail. The detail page is a full page load with a restful url.

From what I’ve described so far, the back button should behave as follows:

  • If the user is viewing page 3 of the results, then goes to page 4, the back button should take them back to page 3 of the results.
  • If the user is on a detail page, the back button should take them back to the results (and it should be on the correct page if the user was paging).

Sounds simple enough right? In Web1.0, this is no big deal. The user changes search criteria and clicks ’submit’, server answers with the results and pre-populates the page with the users’ criteria. One full-page load later, the page has everything it needs wired right into its very source code. User adds more criteria, clicks submit and voila, another page load and another full page that has all the data it needs. Click the back button and that first page returns…still wired up with the results AND the criteria for that page…as though the last page load never even happened.

Web2.0(-ish, there really isn’t a social aspect to this app, but its more lively that the typical sites) we want to stay on the same page…just load the results portion. This makes the user experience more fluid as the page reloads tend to be more jarring. It should also be more performant as there is less to render (fewer database queries) and ultimately less data to transfer.

The first task to handle is actually getting some results via ajax, and this is fairly simple. The user makes their selections and an ajax call is made to get the matching results. The results are then plopped into a div on the page when they return. The server code is pretty straight forward, simpler than 1.0 even, because it just has to answer the specific question and render only the results. No need to re-populate the user’s criteria. Now, how to handle the back button?

As described, the back button will do what it was designed to do: take the user to the previously viewed page. When the user is searching in our app, they are only ever on one page, so nothing they are doing is added to the browser history. The actions that they expect to be able to be ‘backed out’ of are not. For the back button to EVER work, we needed to have our ajax code add to the browser history.

One approach (described http://dojotoolkit.org/node/100) is to simply add to the fragment identifier portion part of the url (everything after the #). The browser treats these as links to elements within the current document and will attempt to scroll to the element whose id matches the hash value. So it will add to the history, but it won’t reload the page. In our case, we’d do this for every criteria change or pagination change (switch from results page 3 to results page 4).

At this point our app would be adding to the ‘history’ and the back button would be changing the url…but the results won’t be changing just yet because we don’t have anything to respond to/reset the results when the back button was clicked. Essentially, we need to store some state (the users criteria and the results) with the value we put on the url, so that when the back button is clicked, that state can be restored. That is exactly what the dojo undo and the RSH and others do for us…but we have to have clean urls so adding #’s to the current url won’t do.

The other approach is pretty old-school. Load the results via a hidden iframe. As the user changes criteria we modify the src of an iframe to get the results instead of via an XmlHttpRequest. The iframe src changes are added to the browser’s history as well. We’d need a way to know when the iframe was ready with the results so that the page could pick them up and display them. We knew that some ajax frameworks would degrade to using an iframe approach when a proper XHR is not available…but the simplest thing we could get to work at the time was to have an onload in the result content that the iframe loaded that would tell the parent window what the results were by calling a javascript function on the parent. Basically the results would announce themselves to the containing window. (In hind-site our final approach may be more re-usable if we switch to monitoring the readystate of the iframe via and onreadystatechanged handler).

So at this point, we have results loading in the iframe, then the iframe telling the parent that results were loaded and the parent placing the results on the page. When the back button is clicked, the iframe loads the previous page of results (because iframe src changes are added to the browser history) and the iframe again tells the parent that results were loaded and the parent displays those results. And we still have our pretty urls ;)

However…we don’t have the correct criteria. The iframe only loads the results. So if the user changes the criteria a few times, then clicks the back button, the results will ‘go back’ but the criteria will not change, so they still appear to be the last criteria used. Not only that, but if we load a detail page (those are full page loads) and hit the back button, the correct results page will load, but there will be NO criteria present (because the criteria change on the page dynamically and hence are not part of the source that is restored by the back button…and the iframe that IS restored only has the results). If only the criteria were part of the results…

That’s pretty much what we did. We decided that we needed to have the results page also pass along the search criteria. When loaded, the results AND the criteria used to get those results would be passed along to the parent. Then the parent would render the results and restore the criteria.

The real key in simplifying the back button problem was to organize our javascript into true objects and have a model that could be used to represent the state of the page. That model could be passed around via the iframe and restored with ease in response to the back button. The road to refactoring the javascript to get to this point is another interesting topic for another day.

Thinking out loud, it would be interesting to revisit the RSH and dojo approaches and see if there could be a hybrid to what we needed to do here (or maybe they could already support what we need and we just didn’t see it ;) ). I’m thinking like an api that would make an XHR call, then when the results come back, adds a # to the src of an iframe, then puts the result text as well as the model into an input in the iframe. There would be a readystatechanged listener on the iframe that would attempt to restore the state of the parent page from the ‘results’ and model stored in the iframe.

Sorry, no code for this one…just our story.