IdolHands.com :: Days in the Life of an Alpha Geek
The Index view is one of the standard RESTful pages you get with every controller in Rails. You've probably dressed up some of these pages with pagination and maybe even added sorting by column headers, but what about filtering? I've got a painless method for providing users with a control to automatically filter the items in a view.

Named scopes are a great way to keep business logic out of the controller, and the fact that you can stack them (like items.active.high_rated) is a bonus for clean, readable code.
named_scope :active, :conditions => { :is_active => true }
named_scope :inactive, :conditions => { :is_active => false }
named_scope :visible, :conditions => { :is_visible => true }
named_scope :invisible, :conditions => { :is_visible => false }
named_scope :high_rated, :conditions => [ "rating > ?", 3 ]
named_scope :low_rated, :conditions => [ "rating < ?", 3 ]
(These are really simple examples of named scopes, but bear with me.)
We also need to add a constant to map human-readable labels onto the named scopes that we will be using as user-accessible filters in the view:
FILTERS = [
{:scope => "all", :label => "All"},
{:scope => "active", :label => "Active"},
{:scope => "inactive", :label => "Inactive"},
{:scope => "visible", :label => "Visible"},
{:scope => "invisible", :label => "Not Visible"},
{:scope => "high_rated", :label => "High-Rated"},
{:scope => "low_rated", :label => "Low-Rated"}
]
Now, we add the following to /helpers/application_helper.rb:
def select_tag_for_filter(model, nvpairs, params)
options = { :query => params[:query] }
_url = url_for(eval("#{model}_url(options)"))
_html = %{<label for="show">Show:</label><br />}
_html << %{<select name="show" id="show"}
_html << %{onchange="window.location='#{_url}' + '?show=' + this.value">}
nvpairs.each do |pair|
_html << %{<option value="#{pair[:scope]}"}
if params[:show] == pair[:scope] || ((params[:show].nil? ||
params[:show].empty?) && pair[:scope] == "all")
_html << %{ selected="selected"}
end
_html << %{>#{pair[:label]}}
_html << %{</option>}
end
_html << %{</select>}
end
This method takes a model name, the filters array of hashes from our model, and any extra options needed to generate a URL as its arguments. It then constructs a select tag with an onchange handler out of the filters we passed it.
Since the user-selected filter will be passed to the index action as a parameter, we've preserved our RESTful routes. All we have to do is check for the show parameter in the params hash. Since we know better than to trust user input, we also validate that parameter against the list of filters that the model gives us access to.
def index
@filters = Item::FILTERS
if params[:show] && @filters.collect{|f| f[:scope]}.include?(params[:show])
@items = Item.send(params[:show])
else
@items = Item.all
end
end
Now, in index.html.erb, add the following line above the table that displays your objects:
<%= select_tag_for_filter("items", @filters, params) %>
This displays the select box populated with our named scopes and their corresponding human-readable labels:
We've now got a simple user control in the index view for filtering results:

If you have suggestions on how to improve this, please let me know by adding a comment below.
![]()
bradgessler
January 6, 2009 at 9:05 PMGreat tip but the eval would make me a bit nervous. Another way to do it would be: @items = Item.send(params[:show]) if Item.filters.keys.include?(params[:show])
![]()
Corey Ehmke
January 7, 2009 at 5:06 AMGood point-- updated the code above to reflect your suggestion. Thanks, Brad.
![]()
Robbye
March 28, 2009 at 3:17 AMThanks for good idea, but what about filter with many fields? I have 3 fields - keyword (item name), field with options for show items (by date, by rating, by views) and field with categories list. What about this filter?
![]()
tryton
August 31, 2009 at 8:06 AMmuch more comfortable is keeping filter criteria in tables this way multilple filtering (even cascaded) are the natural further step interesting thing named scopes, anyway, thanks