History and Back Button Support

Elijah Manor | May 13, 2010

 

Developing interactive websites is great for usability, but one common pitfall of modern websites is the lack of History and Back button support when JavaScript libraries and AJAX techniques are used.

Users expect to return to the previous state of the web page when they click the Back button; however, in a typical AJAX application, the Back button might take a user to a state from several actions ago. Not only does this confuse users, but most likely they think that your software has a bug and are less satisfied with their experience overall.

Part of the reason for this lack of support is that developers have to add additional logic to intercept the Back button and respond to it accordingly. Because the web browser isn’t aware that you are manipulating the DOM behind the scenes, it does not keep track of that for you.

To address this issue, several projects have been written, such as Mikage Sawatari’sjQuery History plugin, Asual’sjQuery Address plugin, Jim Palmer’sjQuery jHistory plugin, and Ben Alman’sjQuery BBQ plugin. These libraries all provides techniques to support the Back button and enable bookmarkable links, but some of them are more successful than others when you take into account ease of use, cross-browser support, and active development.

The jQuery BBQ Plugin

In this article I focus on the jQuery BBQ plugin written by Ben Alman. The plugin’s name, BBQ, is an acronym for Back Button and Query library. From here on, I refer to the jQuery BBQ plugin as BBQ.

Ben put a lot of effort into making his plugin supportable across browsers. In his words, the plug-in has been “Tested with jQuery 1.3.2, 1.4.1, 1.4.2 in Internet Explorer 6–8, Firefox 2–3.7, Safari 3–4, Chrome 4–5, Opera 9.6–10.1, Mobile Safari 3.1.1.”

One of the many things I look for in a library before I invest my time and risk adding it to my project is active involvement by the author. I want to know that bugs are being addressed and that enhancements are being made. The last thing I want is a library that gets stagnant and prevents me from upgrading to a new version of jQuery.

Ben has made an intentional effort to keep BBQ up to date with the latest version of jQuery, provided full documentation of his plugin, and written a suite of more than three hundred unit tests. You can check on the progress of his project on the GitHub project page.

Before I get into the details of BBQ, let’s briefly review the current state of the browser and fragment identifiers. Browsers currently support the #hash syntax in a URL to navigate up and down the same page. For example, I’m sure you’ve done something like this sometime (you can view, run, and edit this code example from jsFiddle):

<!-- You can view, run, and edit this code example from https://jsfiddle.net/elijahmanor/rn7As/ -->

<!-- Deprecated technique using a named anchor -->

<a name="top">Top of Page</a>



<!-- Preferred approach using element with an id attribute  -->

<div id="top">Top of Page</div>



<!-- The rest of your HTML -->



<!-- Anchor to fragment 

    (deprecated named anchor or element with id attribute) -->

<a href="#top">Go To Top</a>

If you click the Go To Top anchor tag, the page navigates to the location of the element with the id attribute of “top” (or a named anchor with the name attribute of “top”).

Not only are you sent to Top of Page, but the URL is appended with the fragment identifier “#top”. If you bookmark this page and return to it at a later time, you go to the same position.

By using this syntax, developers can mark URLs with special data and then respond by using JavaScript, which is just what our first example will do.

The jQuery hashchange Event

Underneath the covers, the BBQ uses the HTML5 hashchange event to respond when changes occur to the fragment identifier (#hash) in the URL. Ben has given hashchange cross-browser support inside his jQuery hashchange event. Currently, only Internet Explorer 8, Firefox 3.6, and Chrome 5 provide native support. Ben used some special tricks to get the hashchange event to work in the other browsers and in older versions, but you can read more about that on his website if you are interested.

Example Using the hashchange Event

Depending on your application, you might get away with using only the jQuery hashchange event and not the entire BBQ plugin. If your application already uses #hash to maintain state inside your web page, you can use the jQuery hashchange event by itself. In a later example, I’ll show how to use the full BBQ plugin and describe when that’s necessary.

I’ve put together a simple jQuery tab example (see Figure 1) that uses #hash values in the href attribute of each tab anchor element.


Figure 1 A simple jQuery tab example using #hash links in the anchor href attribute.

 

<!-- You can view, run, & edit this code example from https://jsfiddle.net/elijahmanor/BWrQP/ -->

<div class="tabs">

    <ul>

        <li class="tab"><a href="#div1">Tab 1</a></li>

        <li class="tab"><a href="#div2">Tab 2</a></li>

        <li class="tab"><a href="#div3">Tab 3</a></li>

    </ul>

    <div id="div1" class="content">Div 1</div>

    <div id="div2" class="content">Div 2</div>

    <div id="div3" class="content">Div 3</div>

</div>



//When a tab is clicked, pass it to the updateTabs method

$(".tabs .tab a").live("click", function(e) {

   updateTabs($($(this).attr("href")));

});



//Grab hash off URL (default to first tab) and update

$(window).bind("hashchange", function(e) {

   var anchor = $(location.hash);

   if (anchor.length === 0) {

      anchor = $(".tabs div:eq(0)");

   }

   updateTabs(anchor);

});



//Pass in the tab and show appropriate contents

function updateTabs(tab) {

   $(".tabs .tab a")

      .removeClass("active")

      .filter(function(index) {

         return $(this).attr("href") === '#' + tab.attr("id");

      }).addClass("active");

   $(".tabs .content").hide();

   tab.show();

}



//Fire the hashchange event when the page first loads

$(window).trigger('hashchange');

In this example, I used the standard #hash technique to maintain state in the browser, much like the Back To Top example shown earlier. I didn’t need to do anything special to get the #hash value in the URL. The browser does this for you because it recognizes this technique. Instead of navigating to a different location on the page, I used the hashchange event to respond to the change.

As you might expect, I have an event handler attached to the anchor element inside the tab, and once it’s clicked, I update the tabs accordingly.

There are two special things that you should notice that are different in this example.

One is the presence of $(window).bind("hashchange", function() {});. This is the key entry point for the hashchange event. We are listening for any change to location.hash and responding to it in the event handler.

This event handler grabs location.hash, which happens to be the hreffrom the tab’s anchor hrefattribute. The location.hash matches the id attribute of the tab’s contents div. If the #hash is not present, we default to the first tab.

The second thing you should notice is the $(window).trigger('hashchange'); statement at the end of the code sample. The only reason for this trigger event is the initial page load. If someone navigates to the page with a #hash as part of the URL, you want to honor that request and display the appropriate tab that matches the #hash.

Introduction to the jQuery BBQ Plugin API

Before I start showing another code example, let’s examine the jQuery BBQ plugin a little bit and highlight part of its API.

You should know about three main methods when you first start using the jQuery BBQ plugin. The three main actions are to push the current “state” (think of this as a snapshot of a point in time) of the page, to get the state again so you can respond to it, and then to remove the state when you are not using it.

jQuery.bbq.pushState

Adds the current state to the browser history

Updates location.hash

Triggers the hashchange event

jQuery.bbq.getState

Retrieves the current state from the browser history

Returns either a specific key from location.hash or the entire state

jQuery.bbq.removeState

Removes one or more keys from the current browser history

Creates a new state by updating location.hash

Triggers the hashchange event

Example Using the jQuery BBQ Plugin

Let’s take a slightly different approach from the previous example, and this time not use the href attribute to include a #hash value. Since we aren’t using #hash values as part of our anchor’s href attribute, we need to handle pushing, getting, and removing #hash values to the URL manually.

This example (see Figure 2) will look and behave the same as the previous one, but it uses the BBQ plugin behind the scenes to manage the historical state.


Figure 2 A simple jQuery tab example using the jQuery BBQ plugin to manage the history state of the tabs.

 

<!-- You can view, run, & edit this code example from https://jsfiddle.net/elijahmanor/cskSw/ -->

<div class="tabs">

   <ul>

      <li class="tab">

         <span class="{contentId: '#div1'}">Tab 1</span>

      </li>

      <li class="tab">

         <span class="{contentId: '#div2'}">Tab 2</span>

      </li>

      <li class="tab">

         <span class="{contentId: '#div3'}">Tab 3</span>

      </li>

   </ul>

   <div id="div1" class="content">Div 1</div>

   <div id="div2" class="content">Div 2</div>

   <div id="div3" class="content">Div 3</div>

</div>

The preceding HTML snippet provides metadata inside the tab’s class attribute to associate an idof the div contents that should be displayed. This metadata can be retrieved using the jQuery Metadata plugin. For those of you not familiar with the jQuery Metadata plugin, it provides a way to embed JSON information inside an attribute and to retrieve the deserialized version at run time.

//Push state of tabIndex to BBQ and handle logic in hashchange

$(".tabs .tab span").live("click", function(e) {

    $.bbq.pushState({ tabIndex: $(this).parent().index() });

    return false;

});



//Get tabIndex state from BBQ and update based on selection

$(window).bind("hashchange", function(e) {

    var tabIndex = $.bbq.getState("tabIndex") || "0",

        tab = $('.tabs .tab').eq(tabIndex);

    

    updateTabs(tab);

});





//Pass in the tab and show appropriate contents

function updateTabs(tab) {

    var title = tab.find('span');

    $(".tabs .tab span").removeClass("active");

    title.addClass("active");

    $(".tabs .content").hide();

    $(title.metadata().contentId).show();         

}



//Fire the hashchange event when the page first loads

$(window).trigger('hashchange');

This time, instead of having both the tab’s click and the window’s hashchange event handlers call the updateTabs method, I’ve removed this from the tab event handler, and now it only pushes its state to the jQuery BBQ plugin. This allows the hashchange event handler to handle the logic to update the user interface accordingly.

The tab’s click event handler needs to push its state to the jQuery BBQ plugin because we aren’t using the same #hash technique that the browser understands. (See the Back To Top example shown at the beginning of the article.) Behind the scenes, the jQuery BBQ plugin updates the URL so that the browser understands that you’ve updated the state of the page, and it keeps that information in its history.

Advanced jQuery BBQ Plugin Example

The tabs code snippets you’ve seen in the last two examples aren’t the most complicated scenarios in the world. Let’s next focus on something a little more advanced.

Here, we’re going to build a sortable list that you can reorder by dragging items up or down. If you double-click one of the items, a dialog appears showing information obtained via AJAX. After a user interacts with the application for a while (reordering items, opening dialogs, closing dialogs, and so on), the goal is to revert each action when the browser’s Back button is clicked.

First let’s take a look at the HTML we’re dealing with. We have an unordered list with a series of list items, each containing a Twitter userName and a count indicating the number of Tweets to retrieve. There is also a divdialog element in which the output of the AJAX request is displayed. (See Figure 3.)


Figure 3: Complex example of reordering and dialog boxes with back button support via BBQ

 

<!-- You can view, run, & edit this code example from https://jsfiddle.net/elijahmanor/YLDGX/ -->

<div class="demo">

   <ul id="sortable">

      <li id="1" class="ui-state-default { userName : 'jquery', count : '1' }"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>jQuery</li>

      <li id="2" class="ui-state-default { userName : 'jeresig', count : '2' }"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>John Resig</li>

      <!-- Other List Items -->

      <li id="7" class="ui-state-default { userName : 'elijahmanor', count : '3' }"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>Elijah Manor</li>

   </ul>

</div>



<div id="dialog" title="Tweets" style="display: none;">

   <div id="output"></div>

</div>

When a list item is sorted, the code generates a unique key (number of milliseconds since midnight January 1, 1970) and uses it to cache the order of list items inside an array. The unique sequence key is then pushed to the jQuery BBQ plugin.

In a similar fashion, when a list item is double-clicked, the id of the item is pushed to BBQ. This information is then used to open a dialog. It is also noteworthy that when the dialog is clicked, the dialog information is removed from BBQ. (You can view, run, and edit the following code example from jsFiddle.)

//Setup sortable list and save sequence array in cache

var sortable = $("#sortable").sortable({

   delay: 500,

   update: function(event, ui) {

      //Generate unique id & store list item ids in cache

      var indexes = $(this).sortable("toArray");

      var sequenceId = new Date().valueOf().toString();

      $(this).data('sequence').cache[sequenceId] = indexes;

      $.bbq.pushState({ sequenceId: sequenceId });

   }

}).disableSelection();



//When a list item is clicked push that dialog id to BBQ

$("#sortable li").live('dblclick', function(e) {

   $.bbq.pushState({ dialogId : $(this).attr('id')});

   return false;

});



//Set initial sequence state of page on 1st page load

var initialSequence = sortable.sortable("toArray");

sortable.data('sequence', {

   cache: {

      '': initialSequence

   }

});



//Setup dialog and when closed remove the dialog id from BBQ

var dialog = $('#dialog').dialog({

   autoOpen: false,

   width: 400,

   modal: true,

   buttons: {

      "Close": function() {

         $(this).dialog("close");

      }

   },

   close : function(event, ui) {

      $.bbq.removeState("dialogId");

   }

});



//Open dialog and sort list items if BBQ contains state

$(window).bind("hashchange", function(e) {

      //sequenceId: Key in cache for sequence indexes

   var sequenceId = e.getState("sequenceId") || '',

      //dialogId: Id of the list item used in dialog

      dialogId = e.getState("dialogId") || '',

      //metadata: Metadata attached to the list item

      metadata = dialogId ? 

         $('#' + dialogId).metadata() : null,

      //indexes: Sequence array of the list item ids

      indexes = sortable.data('sequence').cache[sequenceId];

    

   //Open dialog using id of list item or close if not present

   if(dialogId) {

      appendTweets(metadata.userName, 

         metadata.count, $("#output")); 

      dialog.dialog('open');

   } else {

      dialog.dialog('close');

   }

    

   //Sort the list items based on the indexes stored in cache

   $("#sortable li").reorder(indexes);

});



//Pull X number of tweets for userName and append to selection

function appendTweets(userName, count, selection) {

   var url = 'https://twitter.com/status/user_timeline/' + 

      userName + '.json?count=' + count + '&callback=?';

    

   dialog.dialog('option', 'title', '@' + userName);

   selection.empty();

   $.getJSON(url, function(data) {

      console.dir(data);

      var list = $('<ol />');

      $.each(data, function(i, item) {

         console.log(item.text);

         $('<li />', { text : item.text }).appendTo(list);

      });

      list.appendTo(selection);

   });

}



//Reorder items based on the ids within the indexes parameter

$.fn.reorder = function(indexes) {

   if (!indexes) { return this; }

    

   $.each(indexes, function(index, id) {

      var item = $('#' + id);

      if (item.attr("id") === id) {

         var parent = item.parent();

         item.remove().appendTo(parent);

      }

   });

    

   return this;

};



//Fire the hashchange event when the page first loads

$(window).trigger('hashchange');

Again, the code is rearranged slightly from how you might write it without supporting the Back button. In that case, you would probably place the logic inside your event handlers. Instead, I used those locations to push their state to BBQ so that the hashchange event handler could handle the logic.

Inside the hashchange event handler, we pull out the id of the list item from the BBQ. If an id exists, that means a dialog should be displayed. Metadata is attached to the list item with Twitter information that is used for the AJAX content to display in the dialog.

The hashchange event handler also grabs the sequence ID, which is the key for the cached list of ordered items from a particular state (point in time). The code then reorders the unordered list using a little jQuery plugin that I wrote.

A Deeper Look at the Documentation

The previous examples in this article mainly revolve around the three methods pushState,getState, and removeState.

However, the jQuery BBQ plugin provides many more features that you can utilize in your applications. Many of these methods are used behind the scenes when you call the three state methods.

For example, you might be aware of the jQuery.param method that is provided by the jQuery core library. This method serializes an array or object into a format suitable for a URL query string. However, the jQuery core library doesn’t provide a method to deserialize the URL query string. That is where the BBQ comes into play.

BBQ provides a jQuery.deparam method to deserialize the URL query string into an object. In addition, Ben provides the jQuery.deparam.querystring and jQuery.deparam.fragment methods that deserialize certain portions of the URL. As you might imagine, these methods are vital to the inner workings of the plugin, but the awesome part is that they are also exposed for you to use as well!

If you are interested in these methods and many others, feel free to check out the extensive BBQ documentation and supporting examples.

Who Is This Ben Alman?

In addition to the jQuery BBB plugin, Ben Alman has contributed many other useful projects, including the following:

jQuery equalizeBottoms   Allows you to set the height of multiple items to the same value.

jQuery outside events   Allows you to trigger an event when the user interacts outside a particular element.

jQuery replaceText   Allows you to replace text within a selection without messing up all the elements and attribute values.

jQuery resize event   Allows you to bind to a resize event of a particular DOM element.

jQuery throttle / debounce   Allows you to limit your methods by time. This can be useful if you don’t want to bombard your server with tons of requests.

Not only has Ben developed many high-quality projects, but he has also contributed valuable changes back to the jQuery core library. If you are serious about jQuery, you would be wise to follow Ben Alman on his blog, Twitter, and GitHub.

Conclusion

In today’s highly interactive JavaScript- and AJAX-based solutions, you don’t want to lose functionality that the user is used to and expects, such as support for the History and Back buttons and bookmarks. With the jQuery BBQ plugin, you can provide these features to users without jeopardizing high user satisfaction.

If you are interested in continuing your jQuery learning, I encourage you to follow me on Twitter for a fresh set of jQuery links and to check out my blog for my daily Tech Tweets roundup that contains numerous jQuery links to aid in your learning process.

 

About the Author

Elijah Manor is a Christian and a family man. He develops at appendTo as a Senior Architect providing corporate jQuery support, training, and consulting. He is an ASP.NET  MVP, ASPInsider, and specializes in ASP.NET MVC and jQuery development. He enjoys blogging about the things he learns. He is also active on Twitter and provides daily up-to-date Tech Tweets.

Find Elijah on: