2007/07/27

Sortable headings

In my application, I want to be able to list records in tables, and have headings that the user can click on to sort the table in ascending or descending order.

  • Rather than clicking on the heading to sort and toggle the sort direction, I want 'up' and 'down' links for each column heading.
  • I haven't decided what the links are going to look like yet, so I want to use something simple for now, and be able to change them easily later on.
  • In true DRY fashion, I want to be able to reuse as much of this code as possible, between my various controllers.
To start with, I need a way to pass a field and a sort direction into my controller. So, my revenues controller's index method looks like this;

  def index
find_params = { :include => :client }.merge( order_by )
@revenues = Revenue.find :all, find_params
end



The ' :include => :client ' gives me cheaper access to revenue.client.name, so that I can put the name of the owning client into each table row, without going back to the database every time. It also joins the clients table in the underlying SQL, which allows me to order by 'clients.name'. i.e. I can sort my revenues table by the parent clients' names, rather than the 'client_id' property of the revenue object, which wouldn't be a very useful thing to sort on.

The ' order_by ' is a method that I'm going to re-use in all my controllers, so it goes into controllers/application.rb;

  def order_by
return {} if params[:order].blank?
direction = params[:direction] || "ASC"
{ :order => "#{params[:order]} #{direction}" }
end



My column heading links need to send in an 'order' parameter, which is the name of the field we want to order by, and a 'direction' parameter containing either 'asc' or 'desc'.

The column headings, and their associated fields to sort by are;

Date => :invoiced_on
Reference => :reference
Client => 'clients.name'
Net => :amount
VAT => :vat_amount

Before re-factoring, we might want something like this for our Date table header;

<th>

<%= link_to( '^', revenues_path( :order => :invoiced_on, :direction => 'asc' ) ) %>

Date

<%= link_to( 'v', revenues_path( :order => :invoiced_on, :direction => 'desc' ) ) %>

</th>



I'm using RESTful routes, so I can say 'revenues_path', instead of having a ' link_to ( :controller => 'revenues', :action => 'index' )'. But the principle is the same, even if your application isn't RESTful.

But, if I want to reuse my sortable headings code in different controllers, It won't be a link to 'revenues_path'. So, my code needs to be able to take the link destination method as a parameter. Fortunately, ruby makes that very easy with 'proc'. So, here is my view code, using my sortable_headings helper method;

<% link_proc = proc { |params| revenues_path params } -%>
<table id="revenues">
<tr>
<th> <%= sortable_heading( 'Date', :invoiced_on, link_proc ) %> </th>
<th> <%= sortable_heading( 'Reference', :reference, link_proc ) %> </th>
<th> <%= sortable_heading( 'Client', 'clients.name', link_proc ) %> </th>
<th> <%= sortable_heading( 'Net', :amount, link_proc ) %> </th>
<th> <%= sortable_heading( 'VAT', :vat_amount, link_proc ) %> </th>
<th> Gross </th>
<th> </th>
</tr>

<%= render :partial => 'revenue', :collection => @revenues %>

</table>


And the 'sortable_heading' method goes in my app/helpers/application_helpers.rb file;

  def sortable_heading( label, field, make_link )
up_params = { :order => field, :direction => 'asc' }
down_params = { :order => field, :direction => 'desc' }
"%s #{label} %s" % [
link_to( '^', make_link.call( up_params ) ),
link_to( 'v', make_link.call( down_params ) )
]
end



Voila! Bi-directionally sortable column headings, very DRY, and I can easily change the way they look by replacing the '^' and 'v' in my helper with suitable image links, or whatever, later on.