Disclaimer 1: this code is probably using all kinds of ruby anti-patterns :) - but it is a very simple implementation and so I just went with it...
Disclaimer 2: the title of the post might be a little miss leading as you can use this with or without the will_paginate plugin. In my case I am using it with the will_paginate plugin - and I highly recommend it!
put this file in your rails lib directory /lib/sort_index.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# see http://gist.github.com/341290 (sort_index_controller_usage.rb) | |
# see http://gist.github.com/341295 (sort_index_view_usage.html.erb) | |
# The classes in this module help to enable sorting on index pages | |
# building sql order clauses and rendering html table header links | |
module SortIndex | |
SORT_KEY_ASC = 'asc' | |
SORT_KEY_DESC = 'desc' | |
SORT_DIRECTION_MAP = { | |
SORT_KEY_DESC => 'DESC', | |
SORT_KEY_ASC => 'ASC' | |
} | |
# The +SortIndex::Config+ class specifies all possible sort columns | |
# for a given controller action including the default column and the default order | |
class Config | |
attr_accessor :columns | |
attr_accessor :default_direction | |
def default | |
return @default | |
end | |
# The +initialize+ method; | |
# both the default and columns parameters contain key value pairs | |
# where the key is passed in the query string to the action and the | |
# value contains the sql order by value | |
# === Parameters | |
# * _default_ = Hash; must contain only one pair; automatically gets added to the columns member | |
# * _columns_ = Hash; one pair per sortable column | |
# * _default_direction_ = String; optional, if not specified order will be DESC | |
def initialize(default, columns, default_direction = nil) | |
@columns = columns | |
@default_direction = default_direction || SORT_KEY_DESC | |
raise "default only supports 1 pair" if default.length != 1 | |
default.each_pair { |key, value| | |
@default = value | |
@columns[key] = value | |
} | |
end | |
end | |
# The +SortIndex::Sortable+ class enable sorting on index pages | |
# avoids sql injection by only using values from the SortIndex::Config#columns | |
# Hash and not the values passed in the query string | |
class Sortable | |
# The +initialize+ method; | |
# === Parameters | |
# * _params_ = the controllers params Hash | |
# * _config_ = SortIndex::Config | |
# * _index_url_ = String; optional, if not specified will be the name of the controller | |
# ** Examples | |
# *** not specified /employees (the index action) | |
# *** specified /employees/special_action | |
def initialize(params, config, index_url = nil) | |
@config = config | |
@params = params | |
@index_url = index_url || params[:controller] | |
# sets up for building the sql order by | |
@sort_direction = SORT_DIRECTION_MAP[@params[:sort_direction]] || @config.default_direction | |
@sort_by = @config.columns[@params[:sort_by]] || @config.default | |
end | |
# The +order+ method returns the sql order criteria | |
# use with your find calls or via paginate from will_paginate plugin | |
def order | |
specified_sort_by || "#{@sort_by} #{@sort_direction}" | |
end | |
# The +header_link+ method returns a string of html containing the table header and a tags | |
# Example: <th><a href="/employess?sort_by=first_name&sort_direction=desc">First Name</a></th> | |
# If the column is the currently sorted column then a css class of current-sort-asc or current-sort-describe | |
# is applied; this allows you to use css to add visual indicators such as up and down arrows | |
# this method is called from the view; once per column | |
# === Parameters | |
# * _sort_key_ = String; must be one of the key values from SortIndex::Config | |
# * _display_ = The display text | |
# * _sortable_ = Boolean; default is true; | |
# ** passing false will not render an anchor tag; instead the display will be wrapped in a span | |
def header_link(sort_key, display, sortable = true) | |
if @config.columns[sort_key].nil? and sortable then | |
raise "Sort key of '#{sort_key}' not found. Check your controllers SortIndex::Config variable" | |
end | |
class_attr = "" | |
if @config.columns[sort_key] == @sort_by then | |
class_attr = " class='current-sort-#{@sort_direction.downcase}'" | |
end | |
a_href = "<a href=\"#{@index_url}?sort_by=#{sort_key}&sort_direction=#{next_direction}\" title=\"Sort by #{display}\">#{display}</a>" | |
if sortable == false then | |
a_href = "<span>#{display}</span>" | |
end | |
return "<th#{class_attr}>#{a_href}</th>" | |
end | |
# The +next_direction+ method is called by header_link and specifies which way the rendered | |
# links should sort. Returns the opposite of the current sort | |
def next_direction | |
sort_direction = SORT_KEY_ASC | |
if (@params[:sort_direction].nil?) then | |
sort_direction = (@sort_direction == SORT_KEY_ASC) ? SORT_KEY_DESC : SORT_KEY_ASC | |
elsif (@params[:sort_direction] == SORT_KEY_ASC) then | |
sort_direction = SORT_KEY_DESC | |
end | |
return sort_direction | |
end | |
# The +specified_sort_by+ method is called by order returns the sql order by criteria | |
# This can be more than one sql column; when it is multiple columns we apply the same direction to all | |
# For Example if you had one header column for Employee#full_name which mapped to two | |
# database columns first_name and last_name of the employees table the result would look like | |
# first_name DESC, last_name DESC or last_name DESC, first_name DESC depending on your configuration | |
def specified_sort_by | |
sort = @config.columns[@params[:sort_by]] | |
return nil if sort.nil? | |
return sort.split(',').map {|order_criteria| "#{order_criteria} #{@sort_direction}"}.join(',') | |
end | |
end | |
end |
in your controller code, set up the SortIndex::Config (you can have more than one if you have multiple actions that need to support sorting
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# see http://gist.github.com/341278 (sort_index.rb) | |
# see http://gist.github.com/341295 (sort_index_view_usage.html.erb) | |
# this example is an employees controller and using the will_paginate plugin | |
# however code should work fine with a standard ActiveRecord#find(:all, :order => ... | |
# put the sort_index.rb code in your rails lib directory | |
class EmployeesController < ApplicationController | |
# index sort constant config for sorting index action | |
# default is order by updated_at DESC | |
INDEX_SORT = SortIndex::Config.new( | |
{'updated_at' => 'updated_at'}, | |
{ | |
'full_name' => 'UPPER(first_name), UPPER(last_name)', | |
'email' => 'email' | |
} | |
#, optionally SortIndex::SORT_KEY_ASC | |
) | |
def index | |
@sortable = SortIndex::Sortable.new(params, INDEX_SORT) | |
@employees = Employee.paginate :page => params[:page], :order => @sortable.order | |
# render index.html.erb | |
end | |
end |
then in your view code render your table headers using the sort object
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<%- | |
# see http://gist.github.com/341278 (sort_index.rb) | |
# see http://gist.github.com/341290 (sort_index_controller_usage.rb) | |
-%> | |
<table style="width:100%"> | |
<thead> | |
<tr> | |
<%= @sortable.header_link('full_name', 'Name') %> | |
<%= @sortable.header_link('email', 'Email') %> | |
<%= @sortable.header_link('updated_at', 'Updated at') %> | |
<%= @sortable.header_link('manage', 'Manage', false) %> | |
</tr> | |
</thead> | |
<tfoot> | |
<tr> | |
<td colspan="4"> | |
| |
</td> | |
</tr> | |
</tfoot> | |
<tbody> | |
<% for employee in @employees %> | |
<tr class="<%= cycle('oddrow', 'evenrow') %>"> | |
<td><%= employee.full_name %></td> | |
<td><%= employee.email %></td> | |
<td><%= employee.updated_at.to_s %></td> | |
<td> | |
<%= link_to 'Edit', edit_employee_path(employee) %> | |
</td> | |
</tr> | |
<% end %> | |
</tbody> | |
</table> |
Does not support the following:
- additional attributes on the anchor tags
- additional attributes on the table header tags
- additional query string parameters - might add this later, would be nice for search results