#174 Pagination with AJAX
- Download:
- source codeProject Files in Zip (115 KB)
- mp4Full Size H.264 Video (19.3 MB)
- m4vSmaller H.264 Video (13.3 MB)
- webmFull Size VP8 Video (33.3 MB)
- ogvFull Size Theora Video (25.9 MB)
In any Rails application that displays a list of items it’s great to be able to paginate the list so that you can display a number of items at a time. There are a couple of gems you can use for pagination; a good one is the will_paginate gem, which we’ve used in the application below.
Below is the index page from the application, which shows a paginated list of products. Clicking one of the links at the top of each page reloads the page and shows a different part of the list. The current page of products we’re viewing is shown in the query string part of the URL.
The pagination uses standard HTML links, but instead we’d like the links to trigger an AJAX request when they’re clicked so that we can update only the part of the page that changes.
Pagination
The will_paginate gem can be installed from Github and installed in our application by adding
config.gem 'mislav-will_paginate', :lib => 'will_paginate', :source => 'http://gems.github.com'
to /config/environment.rb
. We make use of it in our products controller’s index
action where we use paginate
instead of find
to get our list of products.
class ProductsController < ApplicationController def index @products = Product.paginate(:all, :order => "name ASC", :per_page => 10, :page => params[:page]) end end
The two parameters worth noting in the paginate method are :per_page
, which determines how many items will be retrieved at one time and :page
which determines the offset. We’re retrieving 10 items per page and getting the page number from the query string parameters.
Finally, in the index view we’re using will_paginate
to display the paging links. This will give us “previous” and “next” links along with shortcut links to each page.
<%= will_paginate @products %>
Adding The AJAX Functionality
To make the paging work with AJAX we need to modify the pagination links so that they make an AJAX request when clicked instead of linking to another page. We could achieve this by delving into the will_paginate code and modifying it, but this would be a lot of work and result in an obtrusive solution that would modify the HTML that generates the links. A better solution is to leave the HTML as it is and to add some unobtrusive JavaScript that adds the AJAX functionality to the links. This way the page will still work for users who have JavaScript turned off.
We’re going to use the jQuery library to make implementing the AJAX functionality easier. To do this we’ll need to download jQuery and put it in our application’s /public/javascripts
directory as jquery.js
. We can then reference it by adding this line in the <head>
section of our application’s layout file.
<%= javascript_link_tag 'jquery' %>
If you prefer you could use prototype instead of jQuery, but the code we’ll be writing to enable the AJAX links would need to be changed to work under prototype.
We’ll add the JavaScript for the pagination into a new JavaScript file called pagination.js
that will live in the same directory as our other JavaScript files. The first thing we’ll want to add to the file is some onclick
events for the pagination links on the page. However, we don’t want to add the events until the page’s Document Object Model has loaded. We can the browser execute a function when the DOM has loaded by calling the $
function, passing it the function as an argument. To test that our code is set up correctly we’ll start by showing an alert
when the DOM loads.
$(function (){ alert('DOM has loaded.'); });
Before the code will work we’ll have to include our new JavaScript file on the products page. As we’ve used Ryan Bates’ nifty layout generator in our application we can use the javascript helper method it provides. In the index view file we can add the following line.
<% javascript 'pagination' %>
Now, when we reload the page the pagination JavaScript is loaded and we see the alert
when the page loads.
Now that we know that the JavaScript is being called correctly we can start to modify it to make our pagination links use AJAX. If we look at the source code of the page we can see that the pagination links are all contained within a div
with a class of pagination
. We can use this to create a jQuery selector that matches all of the links.
<div class="pagination"> <a href="/products?page=1" class="prev_page" rel="prev start">« Previous</a> <a href="/products?page=1" rel="prev start">1</a> <span class="current">2</span> <a href="/products?page=3" rel="next">3</a> <!-- Other links omitted --> <a href="/products?page=3" class="next_page" rel="next">Next »</a> </div>
Once we’ve matched the links we can use the click
function to modify their behaviour. We’ll modify our pagination code so that it looks like this.
$(function () { $('.pagination a').click(function () { $.get(this.href, null, null, 'script'); return false; }); });
We still have our $
function that fires when the page’s DOM loads, but now instead of just showing an alert
it does a little more. First it calls the $
function with a string argument to match all of the anchor elements in the pagination div
. The matching elements then have their click
event modified so that an AJAX request is made when they are clicked.
The AJAX request is a GET request so we can use jQuery’s $.get
method. This method takes a number of parameters. The first is the URL that the request will be made to. We want to make our AJAX request to the same URL that the anchor links to so we can use this.href
to pass the value of its href
attribute.
The second parameter is for any data we want to pass. We are passing the page number but this is in the URL’s query string so we can use null
here. Likewise the third parameter is for a callback function but as we don’t need one we can pass null
again.
The last parameter is important. It tells jQuery how to treat the response from our AJAX request. As we’ll be sending back JavaScript that we want the browser to execute we’ll pass ‘script’
for this value.
Finally our function will return false;
so that the link’s default behaviour isn’t triggered as this would cause the page to reload making our AJAX request pointless.
Modifying The Controller
When an AJAX request is triggered by clicking one of the pagination links it will call the same action that would have been called had we not added the JavaScript: the ProductController’s index
action.
def index @products = Product.paginate(:all, :order => "name ASC", :per_page => 10, :page => params[:page]) end
This action will work equally well for an AJAX request, but the associated view file returns HTML. We’ll need to create a additional view that will return the list of products as JavaScript. To do this we’ll create a file called index.js.erb
in the /app/views/products
directory. This new view can contain any JavaScript that we want the browser to execute in response to a paging link being clicked.
To test that our AJAX code works we’ll start by adding a simple alert
to the view.
alert("This is an AJAX request.");
If we reload the page again and click one of the pagination links we’ll see the alert.
The browser is now executing the JavaScript that the index
action returned when the AJAX request was made.
Obviously we want to do more than just show an alert
when a link is clicked. Instead we want to replace the part of the page that shows the links and the list of products. To do this we’ll have to extract the links and the list into a partial.
The index view’s code looks like this:
<% title "Products" %> <% javascript 'pagination' %> <%= will_paginate @products %> <% @products.each do |product| %> <div class="product"> <h3> <%= link_to product.name, product %> <%= number_to_currency(product.price, :unit => "£") %> </h3> <div class="actions"> <%= link_to "Edit", edit_product_path(product) %> | <%= link_to "Destroy", product, :confirm => "Are you sure?", :method => :delete %> </div> </div> <% end %> <%= will_paginate @products %> <p><%= link_to "New Product", new_product_path %></p>
The part of the view we want to extract is the part between (and including) the two will_paginate
lines. We’ll put this code into a partial file called _product.html.erb
, then reference the partial from the index.
<% title "Products" %> <% javascript 'pagination' %> <div id="products"> <%= render 'products' %> </div> <p><%= link_to "New Product", new_product_path %></p>
One other change we’ll make is to wrap the partial in a div
so that we have a wrapper element around the content that we can reference from JavaScript. This means that we know exactly which part of the page we’ll replace. Note that since Rails 2.3 we can render a partial by just passing its name as a string to render
.
We can now move back to our index.js.erb
file and replace the alert
with the code that will update the contents of the div
with the id
of products with the contents of the partial. All it takes is one line of jQuery code.
$("#products").html("<%= escape_javascript(render("products")) %>");
The code above gets the HTML from the products partial, uses escape_javascript
to make it safe for putting into a JavaScript string, then replaces the contents of the products div
with the partial’s output.
When we refresh the page now and click the “Next” link the page updates and shows the next page of products without the page’s URL changing, but if we click the link again then the page reloads fully and the URL changes. Each subsequent click will then alternate between the AJAX update working and the page fully reloading.
Why is this happening? Well, our pagination JavaScript wires up click events to the pagination links when the page’s DOM loads. When the links and content are replaced by the response from the AJAX request the event-wiring is lost, so the links behave like traditional links again. The next link we click reloads the whole page and so the links have their click events wired up again, only for the events to be disconnected again when another link is clicked and another AJAX update takes place.
Thankfully, jQuery makes it easy to fix this problem. Instead of using click
to attach the click events to the links we can make use of the live
function. This function takes an extra parameter, which is the name of the event you want to wire up, but is otherwise the same.
$(function () { $('.pagination a').live("click", function () { $.get(this.href, null, null, 'script'); return false; }); });
Using live(“click”, function () { ... }
instead of click()
will mean that jQuery will use event delegation to ensure that any new matching elements will also respond to the event. If we refresh the page now and click the links the reloading on each alternate click will be gone and the page will always update the list via AJAX.
Adding a Loading Notification
We’ll finish off this episode by adding a notice that gives the user an indication that the page is loading when they click one of the links. While we’re running the application on our local machine it responds almost immediately, but this may not be the case when it goes live so we’ll need to provide feedback to the user.
To simulate the delay locally we can add a two-second sleep
to the index
action.
def index sleep 2 @products = Product.paginate(:all, :order => "name ASC", :per_page => 10, :page => params[:page]) end
If we click on one of the pagination links now there’ll be a short pause while the page updates, but no indication given that the next set of results is being fetched.
We can add the notification in our pagination JavaScript. There are a number of ways we could do this; one popular way to indicate that something is going on is to have a small spinner graphic that is hidden by default and shown when necessary. To keep things simple we’ll replace the contents of our pagination div with a message when one of the links is clicked.
$(function () { $('.pagination a').live("click", function () { $('.pagination').html('Page is loading...'); $.get(this.href, null, null, 'script'); return false; }); });
Now, when we click a link the pagination links will be replaced by the “Page is loading...” message until the results come back.
A Final Note
If you’re using an earlier version of jQuery you might need to add another line of JavaScript to make the AJAX requests are handled properly on the server. Without it the requests might trigger the HTML view rather the JavaScript one. If this is the case the code you’ll need to add is:
jQuery.ajaxSetUp({ 'beforeSend': function (xhr) {xhr.setRequestHeader('Accept', 'text/javascript')} });
That’s it for this episode. Hopefully it will have given you a good overview of handling page requests with jQuery, not just for pagination but for any kind of AJAX functionality you might want to add to your site.
In the next episode we’ll be expanding on this topic. Currently if we reload the page the list of products won’t hold its position but return to the first page of results. Also there is currently no way of bookmarking a page or using the browser’s back button to visit previous pages. The next episode will cover all of these topics and improve the usability of our application.