homeASCIIcasts

175: AJAX History and Bookmarks 

(view original Railscast)

Other translations: Cn

In the last episode we added AJAX functionality to a paginated list of products. Instead of acting as normal HTML links, the paginated links had click events added to them via unobtrusive JavaScript so that when clicked they made an AJAX request that updated the main content of the page with a new page of products.

Our products page.

Problems With AJAX

Because we’re using AJAX to update the page the URL doesn’t change when a new page of products is loaded. This means that if we reload the page, or bookmark it and return to it later, the first page of products will be shown no matter what page of products we were on. Another consequence of using AJAX is that changing the page of links we’re viewing doesn’t add the previous page to the browser’s history, so we can’t use the back and forward buttons to move through the pages we’ve just viewed.

So there are disadvantages to our AJAX approach; how can we work around them? That’s what we’ll show you in this episode. Be warned that this episode will be more about JavaScript than Ruby on Rails as the issues we’ll be dealing with are on the client-side.

There are a number of JavaScript libraries we could use to solve this problem. One of the oldest is reallysimplehistory, available on Google code. There is also Yahoo’s Browser History Manager. Yahoo have a number of JavaScript libraries available so it’s worth a look. Another option is Asual’s SWFAddress, which works with Flash as well as JavaScript.

All of the libraries mentioned above work with plain JavaScript, but as we’re using jQuery in our application it would be better if we could find one designed to work with that. Asual, mentioned above, have released a plugin called jQuery Address that would work, but instead we’re going to use a different one called jQuery URL Utils plugin. This plugin has a lot of functionality despite its small size (less than 4KB minified) and comes with good documentation.

To use the plugin we’ll need to download it and add it to our application’s /public/javascripts directory as jquery.ba-url.js. Next we’ll include it on our products index page. It needs to be referenced before our pagination.js file so we’ll have to modify the line that includes the JavaScript to add the file before the pagination one.

<% javascript 'jquery.ba-url', 'pagination' %>

An Improvement to Our Pagination Code

Before we start modifying our code to use the plugin we’ll take a brief look at the pagination code we wrote in the last episode.

$(function () {
  $('.pagination a').live("click", function () {
    $('.pagination').html('Page is loading...');
    $.get(this.href, null, null, 'script');
    return false;
  });
});

We can make a small improvement to this code. Instead of using $.get to make the AJAX request we can instead use $.getScript, passing it the URL we want to call.

$.getScript(this.href);

The $.getScript function makes a GET request to the URL passed as a parameter and executes any JavaScript that is returned, which is the same as $.get does, but with a neater syntax.

Giving Each Page a Unique URL

We can now return to our application and start implementing new features. What we want to do is update the URL through JavaScript each time one of the pagination links is clicked. This way each page of products will have a unique URL so that it can be bookmarked. We’ll do this by adding an anchor to the URL that uniquely identifies the current page of products we’re on. For example, for page 2 when the application is running locally the URL will be:

http://localhost:3000/#page=2

We can set the URL by making use of the plugin we downloaded earlier. It provides a $.setFragment method that updates the anchor part of the current URL. We can use it in our pagination code like this:

$(function () {
  $('.pagination a').live("click", function () {
    $.setFragment({ page: $.queryString(this.href).page });
    $('.pagination').html('Page is loading...');
    $.getScript(this.href);
    return false;
  });
});

It works like this: if we pass $.setFragment a literal JavaScript object it will turn it into a querystring-like set of keys and values. We want an anchor of the form page=x, so we can pass an object with page as a key, but how do we get the page number? Well, the page number is in the querystring of the pagination link’s URL, which we’re passing in our AJAX call. We can get this value out by using the $.queryString function, which turns the keys and values in the querystring into a JavaScript object, and getting the value of the page property.

With our pagination code updated we can try it out. If we load up our products page and click “next” the AJAX request is made and the URL changes. Note that the browser’s back button is now enabled too, which means that the previous page has been added to the browser’s history.

The current page is now show in the URL’s anchor.

Fixing The Back Button

We’re not there yet. Although the back and forward buttons now work, the page’s contents don’t change when we use them. Also, if we bookmark the page and come back to it, or just reload it then the first page of products will be displayed no matter what the page value in the URL’s anchor.

We’ll sort out the back and forward buttons first. To get them working we need to listen for an event that’s fired when the anchor part of the URL changes. This is something that is supported by the plugin we’re using. To enable it we need to add a line of JavaScript to our pagination code.

$.fragmentChange(true);

We can now listen for that event. To do so we call the $(document).bind function, passing the name of the event we want to listen out for. If we also pass the name of the section of the anchor we’re interested in, the event will only fire when that section changes. We also need to pass bind a function, and this will be executed when the event fires. For now we’ll just show an alert so that we can test that everything works.

$(document).bind("fragmentChange.page", function () {
  alert('fragment changed!');
});

If we reload the page now then click one of the links we’ll see the alert, as the anchor part of the URL has changed. The alert will also be shown when we click either the back or forward buttons.

The fragmentChange event is fired when the back button is clicked.

Given that the fragmentChange event is trapped each time we click one of the links, we can now move our AJAX request into the function that’s called when the event fires. That way the AJAX request will also be made whenever the back button is clicked. This does give us a slight problem, however. This is the line of code we’re moving.

$.getScript(this.href);

In the line above this refers to the clicked pagination link which we no longer have access to, so we’ll have to get the page number another way. The code that runs when the link’s clicked is still updating the URL’s anchor, though, so we can retrieve the page number from that.

$.getScript(
  $.queryString(
    document.location.href, { 'page':$.fragment().page }
  )
);

The code we use to do this is a little complicated, so we’ve split it up across a few lines to make it clearer. To build the correct URL we use the $.queryString function passing it document.location.href to get the main part of the page’s URL and an JavaScript object with a page property to build the querystring. To get the correct page number we call $.fragment().page to get it from the anchor part of the URL. Once we’ve built the correct URL up we can pass it to $.getScript to make our AJAX call.

When we reload the page now and start clicking through the links the back button becomes available as before, but now when we click the back button the correct page of products is shown as the fragmentChange event is fired and an AJAX call made to update the products list.

Bookmarking and Reloading

We’re almost there now, but there’s still one part of the page that’s not working as it should. If we visit a specific page of products then click reload we’re shown the first page of products again. The same happens if we bookmark a specific page and go back to it later.

The way to fix this problem is to trigger the fragmentChange event when the page loads, if there’s an anchor in the page’s URL. To do that we just need to add a few more lines of JavaScript to our pagination code.

if ($.fragment().page) {
  $(document).trigger("fragmentChange.page");
}

This code checks for an anchor in the URL when the page loads and if it finds one triggers the fragmentChange event so that the correct page of products is loaded.

Our final pagination JavaScript looks like this:

$(function () {
	$('.pagination a').live("click", function () {
		$.setFragment({ page: $.queryString(this.href).page });
		$('.pagination').html('Page is loading...');
		return false;
	});
	
	$.fragmentChange(true);
	$(document).bind("fragmentChange.page", function () {
		$.getScript($.queryString(document.location.href, { 'page': $.fragment().page }));
	});
	
	if ($.fragment().page) {
		$(document).trigger("fragmentChange.page");
	}
});

We now have a product list that we can page though via AJAX, but which still works with the browsers’ forward and back buttons and which allows specific pages to be bookmarked.

While we’ve used jQuery to develop the paging in our application there are still many Prototype users out there. If you’re using Prototype the Prototypextensions library offers similar AJAX history functionality.