How to model a custom search form in Rails

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: , , ,


About Stefano

Stefano Mancini is a co-founder of DevInterface.

After graduating in Computer Science, he first specialized in Java/J2EE development by participating in several international projects in the pharmaceutical and banking ambits.

Enthusiast of agile development, like SCRUM for project management and eXtreme Programming for code writing, he then moved to dynamic languages like Ruby and Python.

About DevInterface

We are an information and communication technology agency. Our mission is to provide web application development, design services and communication strategies. We specialize in building web applications with modern and efficient frameworks.

Related Post

10 Responses to “How to model a custom search form in Rails”

  1. Ale says:

    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

  2. Stefano says:

    Thanks Ale.

    I’ve updated the post with your suggestions.

    Sometimes I’m too Java oriented :-)

  3. 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

  4. Stefano says:

    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.

  5. [...] 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 [...]

  6. Steve says:

    [...] 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 [...]

  7. Awesome, just tried it and it works perfectly =)
    No need of searchlogic anymore !

  8. Globolus says:

    Sorry, but how can I get it work under rails 3?

  9. Ratheesh R says:

    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…

Leave a Reply

Insert code beetween <code lang="ruby"> and </code>

Copyright 2012 DevInterface s.n.c.

DevInterface Blog is proudly powered by WordPress