How to model a custom search form in Rails
12:00 PMDevelopment, RubyStefano
Often you need to create a search form to filter the rows in a table that corresponds to a specific model.
SearchLogic can be a valid solution but maybe you want to bet on a more customizable alternative.
The solution I propose is to create a Search.rb class that is able to collect the search parameters and to create the “where conditions” to be applied on our find query.
Suppose you want to filter events records defined by an Event.rb model with the following attributes:
- name, string
- address, string
- start_at, datetime
- end_at, datetime
The search mask will propose two text fields for name and address and possibly two DatePicker for start_at and end_at.
You may decide to put AND conditions rather than OR conditions or to define intervals based on the presence of one or both date fields.
Let’s now see how to shape our support class Search.rb (eg create app/models/)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | class Search attr_reader :options def initialize(model, options) @model = model @options = options || {} end def name options[:name] end def address options[:address] end def event_date_after date_from_options(:event_date_after) end def event_date_before date_from_options(:event_date_before) end def has_name? name.present? end def has_address? address.present? end def conditions conditions = [] parameters = [] return nil if options.empty? if has_name? conditions << "#{@model.table_name}.name LIKE ?" parameters << "%#{name}%" end if has_address? conditions << "#{@model.table_name}.address LIKE ?" parameters << "%#{address}%" end if event_date_after conditions << "#{@model.table_name}.start_at >= ?" parameters << event_date_after.to_time end if event_date_before conditions << "#{@model.table_name}.end_at <= ?" parameters << event_date_before.to_time.end_of_day end unless conditions.empty? [conditions.join(" AND "), *parameters] else nil end end private def date_from_options(which) part = Proc.new { |n| options["#{which}(#{n}i)"] } y, m, d = part[1], part[2], part[3] y = Date.today.year if y.blank? Date.new(y.to_i, m.to_i, d.to_i) rescue ArgumentError => e return nil end end |
Now we can see how to write the controller that will apply the search.
Specifically, I would make a search that responds to the action index of EventsController.
At this point, an URL without parameters (such as http://localhost:3000/events) will call an Event.find(:all), a request with parameters (such http://localhost:3000/events?name=rock+contest) will apply the search.
This requires that our search form responds to the GET method. We will see this aspect in detail later.
The controller code looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class EventsController < ApplicationController def index @events = [] @search = Search.new(Event, params[:search]) if is_search? @events = Event.search(@search, :page => params[:page]) else @events = Event.paginate(:page => params[:page]) end end private def is_search? @search.conditions end end |
As you can see, in the case of search, the class method search will be called.
Let’s see how it was defined in the model Event.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Event < ActiveRecord::Base def self.search(search, args = {}) self.build_search_hash search, args self.paginate(:all, @search_hash) end private def self.build_search_hash(search, args = {}) @search_hash = {:conditions => search.conditions, :page => args[:page], :per_page => args[:per_page], :order => 'events.created_at'} end end |
At this ponit we can code the search form in this way (using formtastic).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <% semantic_form_for :search, @search, :html => { :method => :get } do |form| %> <% form.inputs do %> <% form.inputs do %> <%= form.input :name, :label => t('search_form.name') %> <%= form.input :address, :label => t('search_form.address') %> <%= form.input :event_date_after, :as => :date, :label => t('search_form.event_date_after') %> <%= form.input :event_date_before, :as => :date, :label => t('search_form.event_date_before') %> <% end %> <% end %> <% form.buttons do %> <%= pretty_positive_button t('search') %> <% end %> <% end %> |
The search method is GET, so it will append search parameters to the url and will invoke the index method of EventsController.
Tags: rails, ruby, ruby on rails, search

















[...] ORIGINAL POST [...]
nitpicking:
not address.nil? and not address.empty?
better to user
address.present? #returns true unless address is nil, “”, [] or {} (and maybe some other classes have custom implementations as well)
address.blank? # opposite of address.present?
lots of lovely goodness like this in activesupport, worth reading the code for it to find loads of little cool bits like this
Thanks Ale.
I’ve updated the post with your suggestions.
Sometimes I’m too Java oriented
I think it would be good to include validations in your search class. For example,
start date and end date cannot be more than 1 month apart, price has to be positive,
need to choose some option, etc.
Would the Search class need some connection to the Event class? Call it EventSearch, or bind in some other way ?
Stephan
Hi Stephan.
Of course you can add all validations you need. I didn’t add them to keep the code readable and simpler.
In my project, the Search class is a bit more complex, because it covers not only the Event model but also other 2 models (Artist and LiveClub).
They have some attributes with the same name (name and address for example) so I’ve used the same Search class to build the where conditions (If I search for Artists, the event_date_after and event_date_before will be false).
Then I’ve extracted the search form as a partial and included it in the right template. In this way I can call the index method of the proper controller.
That’s why I didn’t call this class EventSearch but simply Search.
Finally note that I’ve binded the model class in the constructor.
[...] This post was mentioned on Twitter by Dev Interface, Stefano Mancini. Stefano Mancini said: How to model a custom search form in Rails http://bit.ly/aOgx0P [...]
[...] This post was mentioned on Twitter by Dev Interface, Stefano Mancini. Stefano Mancini said: How to model a custom search form in Rails http://bit.ly/aOgx0P [...]
Awesome, just tried it and it works perfectly =)
No need of searchlogic anymore !
Sorry, but how can I get it work under rails 3?
Im new to Ruby on Rails. SO please help me, I have followed the below forum to make a REST api for my app. “http://digg.com/newsbar/topnews/How_to_create_a_REST_API_for_Ruby_on_Rails_applications”.
But the ‘map.connect_resource :book’(mentioned in the 3rd page of the doc) causes the following error, when executes ‘rake test:functionals.
Error: undefined local variable or method `map’ for #.
In my app, Im trying to implement RoR with mysql DB with the following table data. Table Name: Object Fields: object_id, Object_name, Object_description etc…
I would like to create REST api object for querying the above database and retrieving the data as api object…