05. 12. 2010

HTML 5 Ajax History and Adress Bar Integration

Today I was searching for a way to change the url in the browser's address bar for a Rails application I'm working on. Why would I need to do that? Well, my app uses ajax to setup various views of a page and I want users to be able to bookmark each individual view. I was not having much hope to find a solution as I remembered reading before that it is not possible to change the address bar. However, what I found was even more: not only is it possible to change the address bar, but additionally you can easily make the browser's back button work properly with pages that load content using ajax requests. The caveat is that the technique I found only works with the very latest browsers that support the HTML 5 history interface, like the latest versions of Chrome and Safari for example. However, there are fallback solutions for older browsers out there like history.js (which I haven't tried so far).

Some background: The Rails app I am working on has an event list that can be filtered using various select boxes, like country, type etc. Each of the select boxes triggers an ajax call that updates the listing accordingly. Furthermore, each different type of filtered event list should be directly accessible with a unique url. A filtered url could for example look like

/events?country=uk?type=type1

while the unfiltered list can be found at /events. Quite straight forward.

To get the url change and history working for this example, we need to push the new page state to the browser history after the ajax call successfully returned the filtered list. For now we assume there is only one select tag to filter the list by country. Note that I'm using Prototype here, so you need to adjust the syntax if you are using jQuery or some other js library:

  window.history.pushState({ country : $('country').value }, '', '/events?country=' + $('country').value); 

The first parameter of the pushState function takes the data that is necessary to rebuild the current page's state later, when the user presses the back (or forward) button. The second parameter is a title for that state. We don't need it to get the technique working so I set it to an empty string here. And the last parameter takes the url for the page.

If you test out the app at this stage you will see that the the browser address bar already changes correctly when selecting a different filter setting, as well as when navigating back and forth with the browser's back and forward buttons. However, the rest of the page does not change in the latter case. To fix this we need to monitor the window's onpopstate event to rebuild the page to show the correct listing state when the user navigates back to it. Note that we also have to set the select box to the correct filter value, as the ajax request alone doesn't touch it:

window.onpopstate = function (e) {   
  if (e.state){     
    new Ajax.Updater('event_list', '/events', { parameters: e.state } ); 
    $('country').selectedIndex = $('country').select('[value=' + e.state.country + ']')[0].index;  
  } 
};

This already works quite well. After the filter is applied, the user sees the new url in the browser's address bar. However, the back button doesn't quite work yet. When clicked, the addressbar changes, but the page content doesn't. This is because the initial page's state from the non-ajax request is not rebuilt. To make it work you can add a variable initialParams which is set to the initial list params when the page is opened by a non ajax-request. Then, after the first ajax list update we add these parameters to the current history state before we push the new state:

 
if (initialParams){ 
  window.history.replaceState(initialParams, ''); 
  initialParams = null; 
}   
window.history.pushState($('settings_form').serialize(true), '', url);

And, voilà, we have an ajaxified event listing which is fully integrated with the browser history and address bar!

One more thing: to avoid javascript errors for browsers that don't support this technique you can add the following condition before all history updates: 

if(window.history.pushState)
   ... 

For a great, more general, architectural discussion of the HTML 5 history management see here.

Comments


No Comments

Comments closed