I'm a 37'ish year old web application developer from South Portland, Maine. I love meeting fellow techies, drop me a line if you want to talk shop.
Posted on 03/04/2008 at 02:30 AM
I’ve been working with Rails about 4 months now. Though I am just a newbie, I’m quite a fan of the restful design approach Rails 2.0 seems to favor. I recently needed to add basic search functionality to my app, but had to think a bit on how that would work with a ‘mostly’ restful app.
What follows is a basic description of the implementation I settled on.
There are 3 parts to my design ...
The Basic Search Form:
The search form itself can be quite simple, it just needs to submit via an HTTP GET request. This will pass your search terms in a url to the index action of your specific resource controller. Don’t forget to modify my code with your own resource_path. I’ve used my events_path of my events controller in my example.
<% form_tag events_path, :method => :get do -%>
<%= text_field_tag :search_terms, params[:search_terms] %>
<%= submit_tag 'Search' %>
<% end -%>
The ApplicationController Methods:
Here I’ve done a lot of the work for you, just paste this code in your ApplicationController. I’ll decribe in brief whats going on here, though it might be pretty obvious when you look at it.
Requesting the index action from any one of your controllers will trigger the execution of two before_filters (init_find_conditions and init_find_parameters). The filters initialize two instance array variables for us (@conditions and @parameters) that we’ll play with later to define our search criteria. You’ll also find two separate methods we use to set values for those instance variables. As you add in conditions and parameters you’ll eventually want to retrieve the formatted result of both of them. You’ll use get_find_conditions for that purpose.
Worth noting: in the case that get_find_conditions has nothing to return, a nil value will be returned instead. This will work fine as a default condition in a Model.find.
class ApplicationController < ActionController::Base
before_filter :init_find_conditions, :only => :index
before_filter :init_find_parameters, :only => :index
# BEGIN SEARCH RELATED METHODS =============================
def search_terms
params[:search_terms] || ''
end
def add_find_condition(condition)
init_find_conditions
@find_conditions << condition
end
# accepts string or array
# array is allowed for cases of condition string substitution
def add_find_parameter(parameter)
init_find_parameters
if parameter.kind_of? Array
@find_parameters += parameter
else
@find_parameters << parameter
end
end
def get_find_conditions
@find_conditions.empty? ? nil : @find_parameters.unshift(@find_conditions * ' and ')
end
# END =============================
private # -------------------------------------------------------
def init_find_conditions
@find_conditions = [] unless defined? @find_conditions
end
def init_find_parameters
@find_parameters = [] unless defined? @find_parameters
end
end
The SubController Index Code:
The code to execute an actual search is quite brief. Using my methods, you are able to add multiple sets of conditions and parameters, building sequentially, so that later you can insert them (properly formatted) into your Model.find. Notice how I have included one set of search conditions no matter what, and the second set only if a param[:search_term] is available to the page. Finally we use out accumulated conditions in our Model.find as ... :conditions => get_find_conditions. If there are no conditions the statement is ignored my Active Record.
class EventsController < ApplicationController
# GET /events
def index
# always-on search parameters
add_find_condition "events.start_at >= ? and events.start_at <= ?"
add_find_parameter [Time.now.beginning_of_month, Time.now.end_of_month]
# optional search parameters
if params[:search_terms]
add_find_condition "(events.title LIKE ? OR events.description LIKE ?)"
add_find_parameter ["%#{search_terms}%", "%#{search_terms}%"]
end
@events = Event.find(:all, :include => [:default_address, :categories],
:order => 'events.start_at ASC, events.created_at ASC',
:conditions => get_find_conditions
)
respond_to do |format|
format.html # index.html.erb
end
end
end
Ideally, there is nothing to change in your index view code. The three pieces I have described should plug right in and satisfy your appetite for a quick and simple approach to restful searching.
Again I should mention that I am new to rails and would benefit from comments on my code. Let me know if it was of use to you or if you have ideas on how I can improve it.
Thanks!
thx for the code.
Often times I will release code for free or go that extra distance to help others online. If my skills were useful to you, please consider a small donation. Thank you very much.
@ github.com
@ twitter.com
@ calendaraboutnothing
Foundation's Edge, RJones Family, We're Not.com (only for staging), Ailee Jones (same as rjones for now)
Aaron, Barnaby, Brian, Chris, Dirk, Frank, Four, Justin, Matt, Mike, Monty, Paul, Sean, Travis
I can usually be found lounging on irc.freenode.net while I work, on the following channels: #fauna, #github, #hello-heroku, #jquery, #passenger, #ruby, #rubyonrails, #slicehost, #sproutcore, #textmate, #werenot.
5 minutes after writing this post it was brought to my attention that :with_scope may be the preferred way to contain these searches in your models. I may document this other approach in the near future. Here’s a cool link I found on :with_scope in the meantime ... http://www.caboo.se/articles/2006/2/22/nested-with_scope