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.

Comments

bradgessler
bradgessler
01/06/2009
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])
Corey Ehmke
Corey Ehmke
01/07/2009
Good point-- updated the code above to reflect your suggestion. Thanks, Brad.
Robbye
Robbye
03/28/2009
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?
tryton
tryton
08/31/2009
much 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
Fire Monkey
Fire Monkey
11/05/2010
Rails 3 will escape the select html in the view. Converting the html string using .html_safe tells rails not to escape the string. end _html << %{</select>} _html.html_safe # <-- added end
mohamed mosaad
mohamed mosaad
03/13/2011
hi i get html in my browser <label for="show">Show:</label><br /><select name="show" id="show" onchange="window.location='http://localhost:3000/products' + '?show=' + this.value"><option value="high_rated">High-Rated</option><option value="low_rated">Low-Rated</option></select> how can i fix it thanks!
Leave a Comment