Monday, January 11, 2010

Ajax sorting and pagination

Basically we need to put a link on every table header in order to make a request to the server passing in the parameters
needed to know by which attribute to order and in which order.

We need 2 methods:
The first one is a helper method to create the link with the appropiate parameters, the link must send the attribute that
is ordering by and the order(ASC or DESC).
And the second is a controller method that reads the sent parameters, and generate the appropiate order in the sql query.

So there are a helper and a controller method, I created the following module and put it in the lib directory
with the mentioned methods, to have the whole sorting solution in one file (I could also have added the methods to
application_helper and application_controller respectively)
In this case I created 2 helper methods for generating standard and ajax links.


module SortingMethods

def self.included(base)
base.class_eval do
helper_method :remote_sort_link, :sort_link
end
end

def order_or_default(default_attr, default_order = 'ASC')
if !params[:attr].blank? && !params[:order].blank? &&
params[:attr] =~ /\A\w+\z/ &&
(params[:order] == 'ASC' || params[:order] == 'DESC')
params[:attr] + ' ' + params[:order]
else
default_attr + ' ' + default_order
end
end

def remote_sort_link(text, attribute, extra_params = {})
query_params = params.reject{|key, value| key == 'controller' || key == 'action'}
if query_params[:attr] == attribute.to_s
link_text = text + " (-o-)"
query_params[:order] == 'ASC' ? query_params[:order] = 'DESC' : query_params[:order] = 'ASC'
else
link_text = text
query_params[:order] == 'ASC'
end
query_params[:attr] = attribute.to_s
@template.link_to_remote link_text, :url => request.url.split('?')[0] + '?' +
extra_params.merge(query_params).to_query, :method => :get
end

def sort_link(text, attribute, extra_params = {})
query_params = params.reject{|key, value| key == 'controller' || key == 'action'}
if query_params[:attr] == attribute.to_s
link_text = text + " (-o-)"
query_params[:order] == 'ASC' ? query_params[:order] = 'DESC' : query_params[:order] = 'ASC'
else
link_text = text
query_params[:order] == 'ASC'
end
query_params[:attr] = attribute.to_s
@template.link_to link_text, request.url.split('?')[0] + '?' +
extra_params.merge(query_params).to_query
end

end


The helper method check which is the actual ordering attribute and invert it, or just put the default ASC order. And also does a little bit of escaping.

To get the functionality, you need to include the module in the controller you are ordering by, or in application_controller to get it on every controller.

Suppose you are listing users with name and age.
If you are doing ajax calls like in this case you will have to update the sorting links in the ajax call, so you can put
the whole table in a partial and then update the partial, like I am showing here:


<div id="user_update">
<!-- this goes in _users.html.erb partial -->
<table>
<tr>
<th>
<%= remote_sort_link('Name', :name) %>
</th>
<th>
<%= remote_sort_link('Age', :age) %>
</th>
</tr>
<% for user in @users %>
<tr>
<td><%= user.name %></td>
<td><%= user.age %></td>
</tr>
<% end %>
</table>
<!-- --------------------------------------- -->
</div>


and in the controller:


def index
@users = User.all(:order => order_or_default('name'))
respond_to do |format|
format.html
format.js
end
end


In index.rjs you should update the partial:


page.replace_html 'user_update', :partial => 'users'


You can use it with will_paginate because the query string is mantained between requests. So the links will be
generated with the query string that was on the last request.
There is a previous post where I explain how I use will_paginate plugin with ajax, you could easily add that here and you get both ajaxified pagination and sorting.
Also note that I added " (-o-)" to the link text of the attribute that is currently being sorted by. You probably want
to change this to add anything you want, also you can easily change the generated link_to or link_to_remote class.