How to paginate, sort and search a table with Ajax and Rails
Table of contents
Introduction and warnings
Important warning : unfortunately, I don't have the opportunity to work on Rails anymore these days. So some points in this tutorial may be a bit outdated for the most recent versions of the framework.
In this tutorial, we will try to use Ajax with Rails in order to display a table of items with several functionalities :
- pagination (split the table on several pages)
- sorting (table ordering by one of its column)
- searching (selecting items to be displayed with a query)
A live demonstration of this application is available at :
http://dev.nozav.org/ajaxtable/
This is a very common task in web application development. The interest to use Ajax for this is to provide a dynamic interface which doesn't need to reload the entire page when the table changes. The interest of using Rails is... well, if you are reading this you should already know, but Ajax is really nicely integrated with Rails, and using it is very easier with this great framework. Nevertheless, the code in this tutorial should work the traditional way (by reloading the entire page) if javascript is not available on client side. This is very important for accessibility.
The code which follows is highly inspired from several Rails wiki pages, in particular How to make a real-time search box with the Ajax helpers, and How to paginate with Ajax. I just put things together and sometimes slightly adapted the code.
Now, the warnings. I am not a Rails guru, and far from being an Ajax expert. I am a beginner in both areas. So take this document as a beginner introduction to beginners : code could be cleaner, explanations more accurate, and some things could probably have been designed more efficiently an elegantly. Moreover, as english is not my native language, you will find many faults and inaccuracies through this text. Please excuse me in advance.
In any case, don't hesitate to send me your feedbacks at the following address :
Application installation and configuration
The first thing we have to do is to install and setup the application. If you already had done this before, you could probably skip this section.
Requirements
In this tutorial you are supposed to have a recent Rails version
installed (at least version 2.0) and a functional database
system. Here we will use sqlite
, but you can replace it by the one
you prefer without any problem.
Files
First, we have to create the "skeleton" of our application in the directory of our choice1 :
$ rails ajaxtable $ cd ajaxtable
As for the 2.0 version of Rails, the pagination features have been moved from Rails core into a plugin called classic pagination. Unfortunately many people prefer to use another plugin called will_paginate, but I didn't manage to make this tutorial work with this plugin. So we have to install the classic pagination plugin with the following command :
$ ruby script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination
As it is a very basic application, we will use only one model, which will represent table items, and one controller for these items. We will generate the corresponding files with the Rails scripts :
$ ruby script/generate model Item $ ruby script/generate controller Item
Database
Once we have the files, we have to setup a data source. For simplicity
here we will use sqlite
in conjunction with Rails schema. This means
that we will describe our database inside Rails and let it manage
database creation.
First, we have to update the development data source in
config/database.yml
:
development: adapter: sqlite3 database: db/development.db
Next, we will create the database by using Rails migration tools.
$ ruby script/generate migration database_creation
This should have created a db/migrate/001_database_creation.rb
file in which we will define
our database schema the following way :
class DatabaseCreation < ActiveRecord::Migration def self.up create_table :items do |t| t.column :name, :string, :limit => 30 t.column :quantity, :integer, :null => false, :default => 0 t.column :price, :integer, :null => false, :default => 0 end end def self.down drop_table :items end end
This just defines one table named items
with three columns2 : a
string field called name
, and two integer columns called quantity
and
price
(nothing really original, yes...).
Then, we just have to do a :
$ rake db:migrate
And you should now have a development.db
file in your db
directory
which is your sqlite
database created by Rails with the items
table
inside.
Then you can insert some items in your table to have something to look
at while developing your application. You can make it by hand or by
saving the following SQL instructions in a db/dump.sql
file3 :
BEGIN TRANSACTION; INSERT INTO "items" VALUES(1, 'hoe', 3, 10); INSERT INTO "items" VALUES(2, 'wheelbarrow', 2, 60); INSERT INTO "items" VALUES(3, 'gherkin', 15, 3); INSERT INTO "items" VALUES(4, 'batman', 1, 3000); INSERT INTO "items" VALUES(5, 'fish sausage', 2, 8); INSERT INTO "items" VALUES(6, 'sauerkraut', 9, 9); INSERT INTO "items" VALUES(7, 'watering-can', 4, 13); INSERT INTO "items" VALUES(8, 'dandelions', 78, 1); INSERT INTO "items" VALUES(9, 'refrigerator', 12, 250); INSERT INTO "items" VALUES(10, 'flying matches', 8, 145); INSERT INTO "items" VALUES(11, 'broken accordion', 1, 18); INSERT INTO "items" VALUES(12, 'savage whisper', 5, 7); INSERT INTO "items" VALUES(13, 'hysterical snail', 8, 13); COMMIT;
And then doing a :
$ sqlite3 db/development.db < db/dump.sql
Creating the model
As you probably know, a Rails application is divided into three main types of components : models, views and controllers. We will create them one by one.
The model of our application will be very simple here. In fact, as we
don't have any complex query to send to the database, it will stay the
same as generated by Rails in our application installation, ie
empty. So we will not touch the app/models/item.rb
file.
Well, I hope this step has not been too difficult !
Creating the view
The view of our application will be split into two components : a layout, a view and a partial.
Layout
The layout is a page template which will be used to render several views. It contains some non-varying elements such as HTML header and footer, menus, design elements, etc. The utility of a layout is very limited in our example, as we will have only one page. But it is a tutorial, after all...
Here, the layout will be located in app/views/layouts/item.rhtml
, and
will contain something like :
<html> <head> <title>Ajax table manipulation attempt</title> <%= stylesheet_link_tag "style" %> <%= javascript_include_tag :defaults %> </head> <body> <div id="content"> <%= @content_for_layout %> </div> </body> </html>
An important thing here is the javascript_include_tag
tag which will
be replaced by the javascript libraries used by Rails to provide Ajax
features.
Also note the @content_for_layout
instruction, which will be replaced
by the generated content when needed.
View
The view component will be used to render a particular action of our
controller, which we will describe immediately after this. As it is a
list
action of our item
controller, the view will be located in
app/views/item/list.rhtml
.
The content of the file will be the following :
<h1>Welcome to my wonderful items list</h1> <p>This list is updated real-time from the world largest database of items by using bleeding-edge technology associated to full-featured Web 2.0 eye candy goodies.</p> <p>But be careful, there are plenty of bugs.</p> <h2>And here is the list...</h2> <p> <form name="sform" action="" style="display:inline;"> <label for="item_name">Filter on item name : </label> <%= text_field_tag("query", params['query'], :size => 10 ) %> </form> <%= image_tag("spinner.gif", :align => "absmiddle", :border => 0, :id => "spinner", :style =>"display: none;" ) %> </p> <%= observe_field 'query', :frequency => 2, :update => 'table', :before => "Element.show('spinner')", :success => "Element.hide('spinner')", :url => {:action => 'list'}, :with => 'query' %> <div id="table"> <%= render :partial => "items_list" %> </div>
There is nothing really complicated in the beginning. A stupid introduction text and a search form which will be used to filter items on their names.
Then, we have an image element whose id
is spinner
and which
is hidden by default. This image will be shown briefly during Ajax
operations on the page, and then hidden again when everything is
finished. You can get some public domain images here :
You have to put the chosen image file in the public/images
directory
of your application.
The following observe_field
instruction is a bit more
unusual. What this instruction does here is to add a new Ajax observer
on the query
field. This observer will periodically (here, every 2
seconds, look at the frequency
parameter) check the content of this
field and will react if this content has changed.
The action to be taken is described by the remaining parameters :
- update gives the id of the page element which will be updated. Here
it is the
table
<div> which encloses our partial call just a bit further in the code. - url gives the action that will render the new HTML content to be
inserted. Here the
list
action of our controller will handle every request. - with indicates the way the field content will be passed to the
url
action. Here we will add aquery
parameter to our Ajax request with value equals to the content of the observed field. - before indicates an action to be performed during the time the Ajax request is treated.
- success indicates an action to be performed when the Ajax request has been succesfully treated.
The concrete effect of all this stuff is quite simple. When the user
type something in the query
field, the observer will detect the new
content of the field, and then generate an Ajax request sent to the
server with the url
and with
parameters. As soon as the request is
sent, the before
action toggles the visibility of the spinner
XHTML
element, and the user can see the image. When the request answer his
received, the table
XHTML element is updated, and the success
action
hides the spinner image again.
Just for information, here is what the observe_field
function returns
with the given parameters :
<script type="text/javascript"> //<![CDATA[ new Form.Element.Observer('query', 2, function(element, value) {Element.show('spinner'); new Ajax.Updater('table', '/item/list', {asynchronous:true, evalScripts:true, onSuccess:function(request){Element.hide('spinner')}, parameters:'query=' + value})}) //]]> </script>
We spent some time here to detail the different options because we will meet them again in nearly all the other Ajax methods in this tutorial.
And, to finish this view description, we have a call to a partial
element called items_list
. We will describe this concept and its
content in detail after setting up our controller.
Creating the controller
The controller will handle different kinds of requests in order to update the view by calling the model depending on the request type and its parameters.
Our Item
controller will be very simple here, and will only include
one action called list
. We will not implement any other CRUD (Create,
read, update, delete) action in this tutorial.
So here is the content of app/controllers/item_controller.rb
:
class ItemController < ApplicationController def list items_per_page = 10 sort = case params['sort'] when "name" then "name" when "qty" then "quantity" when "price" then "price" when "name_reverse" then "name DESC" when "qty_reverse" then "quantity DESC" when "price_reverse" then "price DESC" end conditions = ["name LIKE ?", "%#{params[:query]}%"] unless params[:query].nil? @total = Item.count(:conditions => conditions) @items_pages, @items = paginate :items, :order => sort, :conditions => conditions, :per_page => items_per_page if request.xml_http_request? render :partial => "items_list", :layout => false end end end
Let's explain this a bit.
Our controller only defines one action, called list
. This action will
have to handle every request sent to our application.
First, we define a variable called items_per_page
which will represent
the number of lines our table will show on each page.
Next, we define another variable called sort
, depending on the request
parameter of the same name. The case
can be used to mask the real
fields name in our database, which can be more suitable for security
reasons. The reverse
string in the sort
parameter indicates that
sorting should be made in descending order.
Then, a conditions
variable is constructed if a query
request
parameter is present. It is an SQL-like instruction which will be used
to filter our database query results based on the content of the name
field.
After that, we assign the total number of items in our database
matching the conditions
to the @total
variable.
And eventually, we find a call to the Rails paginate
instruction. We
give to this instruction the model it should be associated to
(:items
), a sort field, conditions to be applied to the query, and the
number of items we want on each page. And it automagically returns us
a paginator object called @items_pages
and the items for the current
page number (this page number is passed as a page
request parameter,
which is transparent here). The paginator object will be used later to
display pagination links.
All we have seen for the controller until now applied to every request passed to the application, whatever its type. The most used kinds of HTTP requests are the traditional GET and POST, but Rails and Ajax can make an important use of a third type, whose name is XmlHttpRequest4.
This kind of request is launched by javascript, which will send it in the background via HTTP to the server. One use of the kind of request is to get a fragment of XHTML page used to update a part of the browser display, without reloading the entire page. This can give the user the impression of a fastest and more responsive interface.
That is what the final part of our controller deals with : it tests if
the request it has received is of type xml_http_request
5. In this
case, it will not render the entire list
view, but only a fragment of
it, the now famous partial whose name is items_list
.
So, as we can see, this xml_http_request
test is the only "really
Ajax" thing we met in the controller. That is because Ajax things are
linked to the interface of our application, ie the view, and more
specifically the part of it which will be managed with Ajax, ie the
partial.
So, could you guess what we are going to do now ?
Creating the partial
A partial view component is used to render only a part of a page. It is very useful to separate some repeated view constructions in order to reuse them and to follow the Don't Repeat Yourself (DRY) Rails principle. But it is also really useful with Ajax.
Indeed, the effect of our Ajax actions in this tutorial will always be to redisplay the table of items, whereas it is to change the page, the order or the search. So we will have to refresh the table, but not the entire page, otherwise an Ajax call is of very limited utility. That's why we will separate all the elements which will have to be updated in a partial file6.
Partial filenames always begin with an underscore, so here our partial
will be in app/views/item/_items_list.rhtml
.
Its content will be the following :
<% if @total == 0 %> <p>No items found...</p> <% else %> <p>Number of items found : <b><%= @total %></b></p> <p> <% if @items_pages.page_count > 1 %> Page : <%= pagination_links_remote @items_pages %> <% end %> </p> <table> <thead> <tr> <td <%= sort_td_class_helper "name" %>> <%= sort_link_helper "Name", "name" %> </td> <td <%= sort_td_class_helper "qty" %>> <%= sort_link_helper "Quantity", "qty" %> </td> <td <%= sort_td_class_helper "price" %>> <%= sort_link_helper "Price", "price" %> </td> </tr> </thead> <tbody> <% @items.each do |i| %> <tr class="<%= cycle("even","odd") %>"> <td><%= i.name %></td> <td><%= i.quantity %></td> <td><%= i.price %></td> </tr> <% end %> </tbody> </table> <% end %>
Some explanations may be needed here. The partial contains the table pagination and sorting management. We will see both in a more detailed way.
Pagination helpers
In the beginning, we have a test to see if the total number of items found is greater than zero. In this case, we display this total number and then a paragraph which will be empty if there is only one page in our items pagination.
If we have more than one page of results, we clearly have to display pagination links. Rails already have methods to help dealing with them, but we will have to customize them a little. For this, we will create a helper.
A helper is a Ruby function which is used to help generating the view. The aim is to separate the code in these functions from the view itself, and to make this code reusable.
Our helpers will all be located in app/helpers/item_helper.rb
. Each
method in this file will be accessible from our view. If we wanted
them to be usable by every view of our application, we could have put
them in application_helper.rb
.
Well, enough blabla, here is the code of our pagination_links_remote
helper :
def pagination_links_remote(paginator) page_options = {:window_size => 1} pagination_links_each(paginator, page_options) do |n| options = { :url => {:action => 'list', :params => params.merge({:page => n})}, :update => 'table', :before => "Element.show('spinner')", :success => "Element.hide('spinner')" } html_options = {:href => url_for(:action => 'list', :params => params.merge({:page => n}))} link_to_remote(n.to_s, options, html_options) end end
This method takes a paginator object as argument. This is a Rails object which keeps information about the current pagination state (number of pages, current page, etc.).
We then define a page_options
hash which contains only one item called
window_size
. This is a parameter which tells Rails how many pages it
has to show around the current one. For example, if window_size
equals one, we will have something like :
1 ... 5 6 7 ... 13
If window_size
equals 2 :
1 ... 4 5 6 7 8 ... 13
We could then make a call to the pagination_links
function, which
would generate the correct XHTML code for our links. But the problem
is that it will generate "classic" XHTML links, but not the Ajax
one. So we will have to redefine the links ourselves. This is done
with the pagination_links_each
method.
This method iterates over the pages to be displayed and then applies the block passed as its argument. Our block first defines two kinds of options :
- options are defined for the Ajax link generation. These are very
similar to those defined in the
observe_field
element. The only new thing is the call toparams.merge
, which will add the current request parameters to the link by replacing an existing page param by the page number in the block. - html_options are just defined to generate the "classic" XHTML link, in order to let pagination work if javascript is not available.
Then, there is just a call to the link_to_remote
function, which will
generate the complete XHTML for our link, including the javascript and
the classic XHTML href
part.
For example, here is what the helper returns if there are two pages, the first one being currently displayed :
1 <a href="/item/list?page=2" onclick="Element.show('spinner'); new Ajax.Updater('table', '/item/list?page=2', {asynchronous:true, evalScripts:true, onSuccess:function(request){Element.hide('spinner')}}); return false;">2</a>
Sorting helpers
Back to our partial. After the pagination links we start to define the table by itself. The definition of the table header is a bit complicated, because that is where we define the links to sort our data by a column or another. Each header cell makes use of two helpers.
The first helper is nothing but necessary. It is called
sort_td_class_helper
, and his only goal is to add a
class="sortup"
if the column is currently the one used to
sort the table, or a class="sortdown"
if it is used to
sort in reverse order. The only utility is to allow, with CSS, to
indicates to the user which column is currently used as a sorting
field.
The code is nothing interesting :
def sort_td_class_helper(param) result = 'class="sortup"' if params[:sort] == param result = 'class="sortdown"' if params[:sort] == param + "_reverse" return result end
Then, we have a second helper, called sort_link_helper
.
def sort_link_helper(text, param) key = param key += "_reverse" if params[:sort] == param options = { :url => {:action => 'list', :params => params.merge({:sort => key, :page => nil})}, :update => 'table', :before => "Element.show('spinner')", :success => "Element.hide('spinner')" } html_options = { :title => "Sort by this field", :href => url_for(:action => 'list', :params => params.merge({:sort => key, :page => nil})) } link_to_remote(text, options, html_options) end
This helper is in fact very similar to the pagination_links_remote
one, seen above. It takes two arguments :
- text, which is just the text to be displayed as the column header and sort link
- param, which is the name of the request parameter associated with the column.
The first two lines define a new variable, called key
, which gets the
value of the param
argument, ie the sort key. The string _reverse
is
concatenated to it if param
is already the sorting key. This is used
to implement sorting in both ascending and descending order. If the
user select a sort link, it will sort by ascending order ; if it
selects the same link again, it will sort by descending order, and so
on. You can look at the controller if you don't find these
explanations clear enough.
The rest of the helper is to define the options for the final call to
the link_to_remote
function. They are very similar to those of
pagination_links_remote
:
- the options hash, used for the javascript Ajax link, contains the
url
to be called to generate the new HTML, the id of the element to update, and twobefore
andsuccess
actions to show/hide the spinner image. - the html_options hash, used for the HTML link. It generates the
content of the
href
attribute with theurl_for
Rails function.
Here is what the sort_link_helper
returns for a call with the strings
"Quantity" and "qty" as text
and param
arguments :
<a href="/item/list?sort=qty" onclick="Element.show('spinner'); new Ajax.Updater('table', '/item/list?sort=qty', {asynchronous:true, evalScripts:true, onSuccess:function(request){Element.hide('spinner')}}); return false;" title="Sort by this field">Quantity</a>
Table body
The end of the partial just displayed our table body, with one item on
each line. The only thing which may be noted here is the use of the
cycle
Rails function, which will automatically and alternatively add a
"odd" or "even" style class to our table rows, which can be very
useful to display it in a more visible way.
That's all folks !
We have now seen every pieces of our application more or less in detail. In theory you could see the result by starting the WebRick server and point your browser to :
I hope this document could have been useful to you. I repeat that you can send me any feedback at the mail address given in the introduction.
About this document
This document is published under a Creative Commons Attribution licence.
Thanks to Nicolas St-Laurent, Rachel McConnell and Michel Loiseleur for their feedbacks.
Thanks to Juan Lupión for the spanish translation of this tutorial.
1. These instructions are shown in a linux command line environment, as it is mine.
2. In fact, our table has four columns, as an id
field is
automatically added by Rails.
3. If you use MySQL as database, the correct syntax is INSERT INTO
items VALUES (1, 'hoe', 3, 10);
4. In fact, the XmlHttpRequest is not really another HTTP request type. It is a traditional POST or GET request sent and treated by javascript on client side asynchronously.
5. The request.xml_http_request?
instruction can be abbreviated in
request.xhr?
6. Note that there could have been other ways to do it. In particular
we could have created another action (ajax_list
for example) to deal
with xml_http_request
requests, with an associated view but without
any partial. One problem of this method here would have been to
duplicate a great part of the code between the list
and ajax_list
actions. So, TIMTOWTDI, but DRY.