Add Filters to Views Using Named Scopes in Rails
Posted by Corey Ehmke on January 7th, 2009 in Ruby on Rails | Permanent Link | Share/SaveTags: Ruby on Rails
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.

1. Add Named Scopes and a Filter Mapping to the Model
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"}
]
2. Add the select_tag_for_filter Method to application_helper.rb
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.
3. Add Filter Handling Code to the Controller
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
4. Add the Filter Select Box to the View
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:

That’s It!
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.
January 7th, 2009 at 3:05 am
Great 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])
January 7th, 2009 at 11:06 am
Good point– updated the code above to reflect your suggestion. Thanks, Brad.
March 28th, 2009 at 8:17 am
Thanks 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?