homeASCIIcasts

240: Search, Sort, Paginate with AJAX 

(view original Railscast)

Other translations: Es

Back in episode 228 [watch, read] we created a table of products that could be sorted by clicking a link at the top of each column. In this episode we’ll take this idea further and add searching and pagination. As well as that we’ll add unobtrusive JavaScript to make all of the functionality work with AJAX so that the table can be sorted, searched and paginated without reloading the whole page.

We’ve covered each of these topics individually in previous episodes, but it can be difficult sometimes to bring all of these features together, especially if we want an application to be Rails 3 compatible and the functionality to work through AJAX. As the most complex part of this is creating a table with sortable columns and we have already done this we’ll start with the code from episode 228. You can get the code for this episode from Ryan Bates’ Github pages and when the application runs it looks like this. Note that there is no pagination or searching and that the sorting is done by reloading the page, with the sort parameters in the query string.

The table of products.

Adding Pagination and Searching

The first change we’ll make is to add pagination and searching. Initially we’ll do this without AJAX. Once we know that the new features work we’ll add the JavaScript that will make it work without a page reload.

We’ll add pagination first. The will_paginate gem makes this easy to do, although only the pre-release version, currently 3.0.pre2, works with Rails 3 so we’ll need to specify the version number when we add will_paginate to our application’s Gemfile.

/Gemfile

gem 'will_paginate', '3.0.pre2'

As ever we’ll need to run bundle install after we’ve modified the Gemfile to make sure that the gem has been installed. Once Bundler has run we’ll modify our ProductsController’s index action to add pagination to the list of products that it returns by adding the paginate method to the end of the query. This method takes two parameters: the first is the number of items we want on each page, which we’ll set to 5; the second is the page number which we can get from the page parameter in the query string.

/app/controllers/products_controller.rb

def index
  @products = Product.order(sort_column + ' ' + sort_direction).paginate(:per_page => 5, :page => params[:page])
end

In the index view we’ll add the following line below the table of products so that the page has pagination links.

/app/views/products/index.html.erb

<%= will_paginate @products %>

When we reload the page now the table has pagination links and we can page through the list of products five at a time.

The table now has pagination.

The pagination is now in place, but doesn’t yet work via AJAX. Let’s take a look next at searching. There have been a number of Railscasts episodes that cover searching, the first, “Simple Search Form” is over 3 years old now, but is still applicable here and we can copy and paste some of the example code from it into our application with a few minor changes. First we’ll add the search form to our products list.

/app/views/products/index.html.erb

<%= form_tag products_path, :method => 'get' do %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= submit_tag "Search", :name => nil %>
  </p>
<% end %>

To make the example code from episode 37 work here we’ve had to change projects_path to products_path in the opening form_tag and also add an equals sign to the opening tag for form_tag to make it Rails 3 compatible.

In the controller we need to call search on the model in the index action.

/app/controllers/products_controller.rb

def index
  @products = Product.search(params[:search]).order(sort_column + ' ' + sort_direction).paginate(:per_page => 5, :page => params[:page])
end

Note that we call search before order so we need to make sure that search returns a scope rather than an array of records. We’ll add the search class method to the Product model. We’ll have to make a few changes to the code from episode 37 here for this code to work with Rails 3. Here’s the original code:

def self.search(search)
  if search
    find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
  else
    find(:all)
  end
end

The code above uses find(:all) which returns an array of records rather than a scope. (Also it is deprecated in Rails 3.0.) Instead we’ll use where. In the else condition where the code returns all of the records we could use all, but this will also return an array of records rather than a scope so we’ll use scoped which will perform an empty scope on the products and allow us to add on other queries afterwards. With these changes in place our Product model now looks like this:

/app/models/product.rb

class Product < ActiveRecord::Base
  attr_accessible :name, :price, :released_at
  def self.search(search)
    if search
      where('name LIKE ?', "%#{search}%")
    else
      scoped
    end
  end
end

Note that we’re just running a simple search against the name field here. For a more complex application that was going to go into production we could use a full-text search engine such as Sphinx. If you’re thinking of doing this then it’s worth taking a look at the episode on Thinking Sphinx.

If we reload the products page again we’ll see the search field and if we search for, say “video” we’ll get a filtered list of products returned.

The table with searching implemented.

This is good but there are a few issues that need to be fixed. When we click one of the columns to sort the filtered results the search term is forgotten and we see all of the results. Another problem is that if we sort by, say, price and then search for something the sorting reverts to the default of searching by name. We’ll have to modify sorting and searching so that they take note of each other’s settings.

To get the sorting persisting when we perform a search we’ll need to pass in the sort parameters as hidden fields to the search form. We can do this by just adding a couple of hidden fields to the form that store the sort and direction fields from the query string.

/app/views/products/index.html.erb

<%= form_tag products_path, :method => 'get' do %>
  <%= hidden_field_tag :direction, params[:direction] %>
  <%= hidden_field_tag :sort, params[:sort] %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= submit_tag "Search", :name => nil %>
  </p>
<% end %>

Next we need to get the search to persist when the sort field changes. To do that we’ll have to change the code that generates the sort links. We wrote this code back in episode 228 and it lives in a sortable method in the application helper.

/app/helpers/application_helper.rb

module ApplicationHelper
  def sortable(column, title = nil)
    title ||= column.titleize
    css_class = (column == sort_column) ? "current ↵ 
      #{sort_direction}" : nil
    direction = (column == sort_column && sort_direction == ↵ 
      "asc") ? "desc" : "asc"
    link_to title, {:sort => column, :direction => direction}, ↵
      {:class => css_class}
  end
end

The link is created in the last line of the method and to get the search term to persist we need to add the search term as a parameter to the link. This means, however, that if we add any other search parameters then we’re going to have to change this method every time. In Rails 2 we could make use of overwrite_params here, but this was removed in Rails 3 so we’ll need to find a different solution. What we can do is use params.merge instead.

/app/helpers/application_helper.rb

link_to title, params.merge(:sort => column, :direction => ↵
  direction), {:class => css_class}

This way all of the parameters that are outside of the parameters used for sorting will be carried across. That said we don’t want the page number to be included as we always want sorting to start at page one when we change the sort field or direction so we’ll pass in a nil page parameter.

/app/helpers/application_helper.rb

link_to title, params.merge(:sort => column, :direction => direction, :page => nil), {:class => css_class}

We can try this out now. If we sort the list by price and then search for “video” then the sorting will now persist.

Sorting now persists when a search is made.

If we change the sort order the search term will now be persisted too.

The search term is persisted when the sorting changes too.

Adding AJAX

Now that we have searching, sorting and pagination working let’s add some AJAX so that it all happens without the page reloading. Before you do this in one of your own applications it’s worth asking yourself if using AJAX will really improve the user experience. Often it’s best just to stop at this point as adding AJAX makes it difficult to keep the browser’s back button and bookmarks working as expected. If your application’s UI really will benefit from AJAX then read on.

We’re going to use jQuery to help us with this. The easiest way to add jQuery to a Rails application is to use a gem called jquery-rails so we’ll add this gem to our application and then run bundle install again.

/Gemfile

gem 'jquery-rails'

To install the jQuery files into our application we run the jquery:install command.

$ rails g jquery:install

When you run this command you may see an error. This error is fixed in the latest version of jquery-rails so if you see if then you’l have to specify the version number explicitly. Anything greater than version 0.2.5 should work.

/Gemfile

gem 'jquery-rails', '>=0.2.5'

Rerun bundle to install the new version of the gem and all should be fine. You’ll get a file conflict for the rails.js file but you can safely overwrite it.

Now that we have jQuery installed we can add some AJAX to our page. The first thing we need to do is to identify the part of the page that we want to update. In this case it’s the table that displays the list of products and so we’ll move it from the index view into a partial.

/app/views/products/_products.html.erb

<table class="pretty">
  <tr>
    <th><%= sortable "name" %></th>
    <th><%= sortable "price" %></th>
    <th><%= sortable "released_at", "Released" %></th>
  </tr>
  <% for product in @products %>
  <tr>
    <td><%= product.name %></td>
    <td class="price"><%= number_to_currency(product.price, :unit => "£") %></td>
    <td><%= product.released_at.strftime("%B %e, %Y") %></td>
  </tr>
  <% end %>
</table>

We’ll wrap the partial in a div with an id so that we can identify it from JavaScript.

app/views/products/index.html.erb

<% title "Products" %>
<%= form_tag products_path, :method => 'get' do %>
  <%= hidden_field_tag :direction, params[:direction] %>
  <%= hidden_field_tag :sort, params[:sort] %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= submit_tag "Search", :name => nil %>
  </p>
<% end %>
<div id="products"><%= render 'products' %></div>
<%= will_paginate @products %>
<p><%= link_to "New Product", new_product_path %></p>

We’re ready now to write the JavaScript code that adds the AJAX functionality.

/public/javascripts/application.js

$(function () {
  $('#products th a').live('click', function () {
    $.getScript(this.href);
    return false;
  });
})

This code starts with jQuery’s $ function. If this function is passed a function as an argument then that function will be run once the page’s DOM has loaded. The code inside the function uses a jQuery selector to find all of the anchor tags within the table’s header cells and adds a click event to them. We use jQuery’s live function here rather than just click to attach the events so that when the table is reloaded the events stay live and don’t need to be reattached.

When one of the links is clicked jQuery’s $.getScript function is called which will load and run JavaScript file from the server. The file we want to load has the same URL as the link so we can pass in the href of the link as the argument. Finally the function returns false so that the link itself isn’t fired.

If we reload the page now and try clicking the links at the top of the table they won’t work. This is because we haven’t yet written a JavaScript template for the index action that the links are calling. We’ll do that now.

We want the code in this template to update the products div with the output from the _products partial and it doesn’t take much code to do this at all.

/app/views/products/index.js.erb

$('#products').html('<%= escape_javascript(render("products")) %>');

Now when we reload the page the sort links work and the table is re-sorted without the page having to be reloaded. Obviously this is difficult to show here but if we open the page in Firefox and use Firebug to show the XMLHTTP requests we can see the requests and the response that is returned.

Sorting now works through AJAX.

Now that we’ve done this it’s easy to make the pagination links use AJAX too. All we need to do is add those links to the list of elements that trigger the AJAX call.

/public/javascripts/application.js

$(function () {
  $('#products th a, #products .pagination a').live('click', ↵ 
    function () {
      $.getScript(this.href);
      return false;
    }
  );
});

That’s it. The pagination will now work without reloading the page.

The Search Form

The final part of the page that we need to get working via AJAX is the search form. The first thing we’ll have to do is give the form element an id so that we can easily select it with jQuery.

/app/views/products/index.html.erb

<%= form_tag products_path, :method => 'get', :id => ↵
  "products_search" do %>
  <%= hidden_field_tag :direction, params[:direction] %>
  <%= hidden_field_tag :sort, params[:sort] %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= submit_tag "Search", :name => nil %>
  </p>
<% end %>

Now we need to add a little more JavaScript in to the application.js file.

/public/application.js

$(function () {
  // Sorting and pagination links.
  $('#products th a, #products .pagination a').live('click', 
    function () {
      $.getScript(this.href);
      return false;
    }
  );
  // Search form.
  $('#products_search').submit(function () {
    $.get(this.action, $(this).serialize(), null, 'script');
    return false;
  });
});

The new code selects the search form and listens for its submit event. The function that’s called when the form is submitted uses jQuery’s $.get function to make an AJAX request. We pass in the form’s action as the URL for the AJAX request, the form’s data by using $(this).serialize, null as we don’t want a callback function and then 'script' so that the response is evaluated as JavaScript. After that we return false so that the form isn’t submitted.

When we reload the page now we can submit a search and the table will filter the results without reloading the page. Against, this is difficult to demonstrate here but if we use Firefox and Firebug again we can see the AJAX request and the response.

Searching now works with AJAX too.

We can even easily change the JavaScript to make the search ‘live’ so that the results automatically update with each keypress. Note that this is only a quick demo and isn’t the best way to do this. There are several jQuery plugins that you can use if you do something like this in a production app. To do this we’ll replace the JavaScript that we used to submit the form through AJAX with this.

/public/application.js

$('#products_search input').keyup(function () {
  $.get($('#products_search').attr('action'), ↵ 
    $('#products_search').serialize(), null, 'script');
  return false;
});

Now, every time we enter a character into the text box the AJAX request is made and the table is updated.

The searching is now 'live'.

There’s one bug that we’ve introduced by adding AJAX to the search form. When we make a search the ordering reverts back to the default of searching by name. This is because the hidden fields on the form that store the sort field and direction are not automatically updated when the AJAX call is made. To fix this we need to move these fields into the products partial.

/app/views/products/_products.html.erb

<%= hidden_field_tag :direction, params[:direction] %>
<%= hidden_field_tag :sort, params[:sort] %>
<table class="pretty">
  <tr>
    <th><%= sortable "name" %></th>
    <th><%= sortable "price" %></th>
    <th><%= sortable "released_at", "Released" %></th>
  </tr>
  <% for product in @products %>
  <tr>
    <td><%= product.name %></td>
    <td class="price"><%= number_to_currency(product.price, :unit => "£") %></td>
    <td><%= product.released_at.strftime("%B %e, %Y") %></td>
  </tr>
  <% end %>
</table>
<%= will_paginate @products %>

We’ll also move the partial into the form.

/app/view/products/index.html.erb

<% title "Products" %>
<%= form_tag products_path, :method => 'get', :id => "products_search" do %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= submit_tag "Search", :name => nil %>
  </p>
 <div id="products"><%= render 'products' %></div>
<% end %>
<p><%= link_to "New Product", new_product_path %></p>

Now when we search the sort order is maintained.

Everything now works as we want.

That’s it, we’re done. We now have a nice interface with sorting, searching and pagination all done through AJAX.