Ajax Scaffold live search
Currently I’m working on a project using Rails and heavy Ajax coding for a browser based application for address, room and personal management. [Ajaxscafold](http://www.ajaxscaffold.com/) is an excellent tool for this and took a lot of ‘routine’ work, by providing the whole edit/new and whatnot, from me.
**Step 1** - Getting rails to work.
———————————–
Okay to get the easy part done quickl:
gem install rails
gem install ajax_scaffold_generator
rails /path/to/todo
cd /path/to/todo
rm public/index.html
Now we’ve a basic rails app set up. If there are any problems with this steps: [Google](http://google.com) is your friend, there are about one million trillion tutorials on that topic and it might be a good thing to start of with one of this, as I’m not going in details here.
**Step 2** - Setting up a database and the Scaffold.
—————————————————-
We’re going to use migrations for this, actually only one but that will be just fine. Get to the directory we set up the rails application into. And type the following:
script/generate migration CreateTodoTable
create db/migrate
create db/migrate/001_create_todo_table.rb
This will create a database migration file we can later easily patch into the tables. Here the content of the migration file:
class CreateTodoTable < ActiveRecord::Migration
def self.up
create_table 'todos' do |t|
t.column 'name', :string
t.column 'description', :text
end
end
def self.down
drop_table 'todos'
end
end
With that done the next step will be to run `rake migrate` to have rake set up our table, and we’re done her too. At this point we’ve an all nice and shiny database, table and a basically running rails, (hopefully) if not just make it work.
All left to do is to generate the scaffolds, which again is ‘ruby simple’ to be exact only a single command:
script/generate ajaxscaffold Todo
**Step 3** - Adjust the templates.
———————————-
To add the search features we need to make a few adjustments to the templates, fist we alter the `app/views/todos/component.rhtml`and replace:
With the new code for search field, we don’t need a form as we want to make it dynamically updating, and use a observer instead of it. We set the update to an empty string so it don’t overwrites any other elements, as we will only return an java script:
Okay so far the first step, but it would be too easy if that would be all. We need to give the footer an ID, so we can later easiely set new paginnation links via javascript. In the same file, at the very bottom change:
by adding of `id=”todo-pagination”` to the `
<% new_params = params.merge(:controller => ‘/todos’, :action => ‘new’) %>
<%= loading_indicator_tag(new_params) %>
<%= link_to_remote "Create New",
{ :url => new_params,
:loading => “Element.show(‘#{loading_indicator_id(new_params)}’);” },
{ :href => url_for(new_params),
:class => “create” } %>
<% new_params = params.merge(:controller => ‘/todos’, :action => ‘new’) %>
<%= loading_indicator_tag(new_params) %>
<%= link_to_remote "Create New",
{ :url => new_params,
:loading => “Element.show(‘#{loading_indicator_id(new_params)}’);” },
{ :href => url_for(new_params),
:class => “create” } %>
<% search_params = params.merge(:controller => ‘/todos’, :action => ‘search’) %>
<%= text_field_tag :search %><%= loading_indicator_tag search_params %>
<%= observe_field(:search,
:frequency => 0.5,
:update => ”,
:loading => “Element.show(‘#{loading_indicator_id(search_params)}’)”,
:complete => “Element.hide(‘#{loading_indicator_id(search_params)}’)”,
:url => search_params) %>
`tag to:
One more to go, we need to adjust the pagination links to make sure they work correctly with the search, and don’t screw us over once they are used. Open the file `app/views/todos/_pagination_links.rhtml`. We are making two adjustments, first we need to make sure the pagination links call the `search` action and not the `update_component` action, also we need pass the search string along. For this we need to change:
<% pagination_params = params.merge(:controller => ‘/todos’, :action => ‘component_update’) %>
to the following:
<% if @search %>
<% pagination_params = params.merge(:controller => ‘/todos’, :action => ‘search’, :search => @search) %>
<% else %>
<% pagination_params = params.merge(:controller => ‘/todos’, :action => ‘component_update’) %>
<% end %>
Last to go in this section, we need to change the `:updata` parameter for the two links to:
:update => @search ? ” : scaffold_content_id(pagination_params) },
Okay I was not abselutely honest, we still need to create a new file `app/views/todos/search.rjs` to format our search results and have a javascript output. It is fairly simple and should look like this:
if @successful
page.replace_html scaffold_tbody_id(params), :partial => ‘todo’, :collection => @todos, :locals => { :hidden => false }
page.replace_html ‘todo-pagination’, :partial => ‘pagination_links’, :locals => { :paginator => @paginator, :search => @search }
else
page.replace_html scaffold_messages_id(@options), :partial => ‘messages’
end
**Step 4** - The code!
———————-
First we need to slightly adjust the `pagination_ajax_links` helper function, the place to to put it is `app/helpers/todos_helper.rb`. The function should look like the following (nearly the same as the original just that it changes the ‘update’ section):
def pagination_ajax_links(paginator, params)
pagination_links_each(paginator, {}) do |n|
link_to_remote n,
{ :url => params.merge(:page => n ),
:loading => “Element.show(‘#{loading_indicator_id(params.merge(:action => ‘pagination’))}’);”,
:update => params[:search] ? ” :scaffold_content_id(params) },
{ :href => url_for(params.merge(:page => n )) }
end
end
Well we’re coming close to what we want, mainly left now is to add the search action to the controller, so we open the file `app/controllers/todos_controller.rb”` and insert the following function:
def search
@search = params[:search] || request.raw_post || request.query_string
@options = { :scaffold_id => params[:scaffold_id], :action => “search”, :search => @search }
update_params :default_scaffold_id => “todo”, :default_sort => nil, :default_sort_direction => “asc”
@sort_sql = Todo.scaffold_columns_hash[current_sort(params)].sort_sql rescue nil
@sort_by = @sort_sql.nil? ? “#{Todo.table_name}.#{Todo.primary_key} asc” : @sort_sql + ” ” + current_sort_direction(params)
search = ‘%’ + @search + ‘%’
search_fields = [‘name’, ‘description’]
conditions = [
search_fields.map{|p| “#{p}” }.join(’ LIKE ? OR ‘) + ’ LIKE ?’
];
search_fields.each do ||
conditions << search
end
@paginator, @todos = paginate(:todos, :conditions => conditions, :order => @sort_by, :per_page => default_per_page)
@successful = !@todos.empty?
@flash[:error] = “Nothing found for search string #{@search}.” if !@successful
render(:action => ‘search.rjs’)
end
A little explanation, line **2** we get the search string in whatever way it may be passed, line **3-6** are usual handling for sorting the output. In line **7** we add `%` around our search string, this are wild-cards in SQL you can add only one in the end but I prefer this way. Right next we define the fields over what we want to search, in our case, both name and description. `conditions` holds the search’s ‘WHERE’ statement for the SQL SELECT, the each loop over the `search_field` in line **12** fills the `conditions` array with the search elements, you can read up details on how this works in the [rails API](http://api.rubyonrails.org), for now all that matters is that, every `?` in our search string gets replaced with one of the following elements of the array. Finally we call the pagination and do some error handling and in the very end render the `search.rjs`.
**Step 5** - Last words.
————————
Okay we’re done here, have fun and enjoy this new funky feature for your app. If there are any ideas, questions, suggestions don’t be shy to let me know or ask. Also as a **warning** currently this do not do any fallback if no javascrip is present but I may write a tutorial on that up on a later date this text is long enough as it is.
Trackbacks
Use the following link to trackback from your own site:
http://blog.licenser.net/trackbacks?article_id=7
Comments
- Nice writeup! Thanks.
- Really great work! (In behalf of the PUMPE i thank you ;) ) But i noticed one liitle typo... isnt it "script/generate ajax_scaffold Todo" rather than "script/generate ajaxscaffold Todo" ?
- Internet Explorer produces an RJS error. Have you managed to solve this? Thanks, JD
- It has a time but, I think I found that problem some time back, so please don't take it as guaranted - i've quite some trouble to get my hands on that silly browser while developing ;P despite it makes me sick everytime I do get my hands on it. When I remember right it was some problem with tables and forms. That you couldn't put formfields within a table, or forms within a table that are monitored. Something along the line, sorry that I don't recall more detailed.
- Some errors & fixes: * don't use update with RJS `<%= observe_field(:search, :frequency => 0.5, :update => '',` should NOT SET UPDATE AT ALL or it will break. * make sure you clear flash errors `else page.replace_html scaffold_messages_id(@options), :partial => 'messages' end ` should be `end page.replace_html scaffold_messages_id(@options), :partial => 'messages'` ... because otherwise it won't clear up irrelevant errors. * don't use raw post `@search = params[:search] || request.raw_post || request.query_string` is bad - raw_post and query_string are only useful for debugging, as the results of them will not work in the rest of the code under any circumstances I've tested. * It's better to use a search form, so you can easily add multiple search elements (e.g. a checkbox AND text search). <%= form_tag nil, { :id => 'search_form' } %> <% search_params = params.merge(:controller => '/thingies', :action => 'search') %> <%= loading_indicator_tag search_params %> Search: <%= text_field_tag :search %> Include completed: <%= check_box_tag :search_completed %> <%= end_form_tag %> <%= observe_form(:search_form, :frequency => 0.5, :loading => "Element.show('#{loading_indicator_id(search_params)}')", :complete => "Element.hide('#{loading_indicator_id(search_params)}')", :url => search_params, :submit => 'search_form') %> * various improvements to search makes conditions easier esp. for multiple search fields, does downcasing, and searches ALL words against ALL fields - so long as EACH word matches at least ONE field def search @search = params[:search] @search_completed = params[:search_completed] @search_projectless = params[:search_projectless] completed_conditions = "status != 'Canceled' AND status != 'Completed'" if (!@search and !@search_completed) @search = request.raw_post || request.query_string end @options = { :scaffold_id => params[:scaffold_id], :action => "search", :search => @search } update_params :default_scaffold_id => "thing", :default_sort => nil, :default_sort_direction => "asc" @sort_sql = Thing.scaffold_columns_hash[current_sort(params)].sort_sql rescue nil @sort_by = @sort_sql.nil? ? "#{Thing.table_name}.status desc" : @sort_sql + " " + current_sort_direction(params) conditions = ["TRUE"] if !@search_completed conditions[0] = "(" + conditions[0] + ") AND (" + completed_conditions + ")" end if @search.nil? or @search == "" @paginator, @thingies = paginate(:thingies, :order_by => @sort_by, :include => [:relationship], :conditions => conditions, :per_page => default_per_page) else search_words = @search.split.collect { |name| "%#{name.downcase.strip}%" } search_fields = ['name'] conditions[0] = "(" + conditions[0] + ") AND (" + search_words.map{search_fields.map{|p| "LOWER(#{p})" }.join(' LIKE ? OR ') + ' LIKE ?'}.join(' AND ') + ")" search_words.each do |word| search_fields.each do || conditions << word.downcase end end @paginator, @thingies = paginate(:thingies, :conditions => conditions, :order => @sort_by, :include => [:relationship], :per_page => default_per_page) end @successful = !@thingies.empty? @flash[:error] = "Nothing found for search string #{@search}." if !@successful render :action => 'search.rjs' end
- ... and btw your comment markup is uglifying. :( I suggest switching to Textile.
- Hi Sai I have a problem with observe_form it doesnt work it