Radio Bantik:
Days in the Life of an Alpha Geek

Corey Ehmke’s home on the web since 1996, IdolHands.com is an alpha-geek blog covering topics in Ruby on Rails development, Mac OS X, electronics, robotics, and other stuff important in the life of a technologist and tinkerer.

Add Filters to Views Using Named Scopes in Rails

Posted by Corey Ehmke on January 7th, 2009 in Ruby on Rails | Permanent Link | Share/Save
Tags:

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.

view_with_filter.png

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:

view_with_filters_detail.png

That’s It!

We’ve now got a simple user control in the index view for filtering results:

view_with_filter_2.png

If you have suggestions on how to improve this, please let me know by adding a comment below.


3 Responses to “Add Filters to Views Using Named Scopes in Rails”

  1. bradgessler Says:

    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])

  2. Corey Ehmke Says:

    Good point– updated the code above to reflect your suggestion. Thanks, Brad.

  3. Robbye Says:

    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?

Leave a Reply