240: Search, Sort, Paginate with AJAX
(view original Railscast)
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.
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 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.
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.
If we change the sort order the search term will now be persisted 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.
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.
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.
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.
That’s it, we’re done. We now have a nice interface with sorting, searching and pagination all done through AJAX.