Sample app for auto complete on a complex form
Update November 2009: My View Mapper gem now supports generating scaffolding code for a complex form with auto_complete behavior like the one I describe below right inside your application, using your models and attributes. For more info see: https://patshaughnessy.net/2009/11/25/scaffolding-for-auto-complete-on-a-complex-nested-form. You can read more about my fork of the auto_complete plugin here: https://patshaughnessy.net/repeated_auto_complete.
In his “Complex Forms” series (part 1, part 2 and part 3) Ryan Bates does a fantastic job explaining how to create a complex form containing a series of parent/child text fields while still using simple, clean code. Ryan also pushed the sample application from the screen cast onto github, here: http://github.com/ryanb/complex-form-examples
Here’s what Ryan’s sample complex form looks like:
One problem I ran into while using Ryan’s suggestions on a complex form I was writing was how to get auto complete behavior to work properly using the auto_complete plugin for fields that are repeated, like the “task” field here. As I explained in a previous blog post, this causes a lot of problems for the auto_complete plugin since the <input id=””> attributes are no longer unique, breaking the javascript used for auto complete. I was able to solve the problem by modifying the auto_complete plugin to generate unique <input id=””> attributes, among other things.
Here I want to take some time to show how to use my modified auto_complete plugin, using the same sample application from Ryan’s screencast. To get started, let’s clone the git repository for the sample app - this command refers to my fork of Ryan's complex-form-examples repository: http://github.com/patshaughnessy/complex-form-examples
$ git clone git://github.com/patshaughnessy/complex-form-examples.git Initialized empty Git repository in /Users/pat/rails-apps/complex-form-examples/.git/ remote: Counting objects: 192, done. remote: Compressing objects: 100% (122/122), done. remote: Total 192 (delta 71), reused 159 (delta 58) Receiving objects: 100% (192/192), 86.19 KiB | 68 KiB/s, done. Resolving deltas: 100% (71/71), done.
Ryan had saved various versions of the sample app in different git branches, so to avoid confusion I’ve saved my auto complete related changes in a branch called “auto_complete.” So next you should switch to that branch:
$ cd complex-form-examples $ git checkout origin/auto_complete Note: moving to "origin/auto_complete" which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b <new_branch_name> HEAD is now at 4f3e908... Sample app code changes for auto_complete
Now you will see my changes in Ryans’ code, except for one more detail: I saved my version of the auto_complete plugin in this git repository as a submodule. To get the plugin’s code for this sample app you need to run these commands:
$ git submodule init Submodule 'vendor/plugins/auto_complete' (git://github.com/patshaughnessy/auto_complete.git) registered for path 'vendor/plugins/auto_complete' $ git submodule update Initialized empty Git repository in /Users/pat/rails-apps/complex-form-examples/vendor/plugins/auto_complete/.git/ remote: Counting objects: 22, done. remote: Compressing objects: 100% (21/21), done. remote: Total 22 (delta 5), reused 0 (delta 0) Receiving objects: 100% (22/22), 7.65 KiB, done. Resolving deltas: 100% (5/5), done. Submodule path 'vendor/plugins/auto_complete': checked out '0814a25a754a235c5cf6f7a258fa405059a5ca6f'
(Note that normally to install the plugin in your app you would just run “script/plugin install git://github.com/patshaughnessy/auto_complete.git” – the submodule is only present in this sample app.) Now to setup and run the application you just need to:
- Enter your MySQL details in config/database.yml
- Run rake db:migrate
- Run script/server to launch the app
If you enter a few records you should be able to see the auto complete drop down, even for the repeated field:
Let’s review the changes I’ve made to Ryan’s code aside from adding my modified version of auto_complete to vendor/plugins. First, I added the standard auto_complete handlers to projects_controller.rb for both the project and task fields:
class ProjectsController < ApplicationController auto_complete_for :project, :name auto_complete_for :task, :name …
Next I modified the project text field to use auto complete (in views/projects/_form.html.erb):
<p> <%= f.label :name, "Project:" %> <%= text_field_with_auto_complete :project, :name, {}, {:method => :get } %> </p>
These two changes enable auto complete for the single project text field, just the same way you would with any text field and the standard auto_complete plugin. However, to get auto complete to work with the repeated tasks field, we need to use changes I’ve made to auto_complete. First, in helpers/projects_helper.rb change the “fields_for_task” method to use my new auto_complete_fields_for method, like this:
def fields_for_task(task, &block) new_or_existing = task.new_record? ? 'new' : 'existing' prefix = "project[#{new_or_existing}_task_attributes][]" auto_complete_fields_for(prefix, task, &block) end
This causes my code in auto_complete to provide a custom form builder object, which we can use in the view as follows (views/projects/_task.html.erb):
<% fields_for_task task do |f| -%> <%= error_messages_for :task, :object => task %> <%= f.label :name, "Task:" %> <%= f.text_field_with_auto_complete :task, :name, {}, {:method => :get } %> <%= link_to_function "remove", "$(this).up('.task').remove()" %> <% end -%>
Here I’ve called “text_field_with_auto_complete” as a method on the “f” form builder object yielded by fields_for_task. This will cause the auto complete script and HTML to be generated with unique <input id=””> attributes, allowing the auto complete behavior to work properly.
One other change I made was also to helpers/projects_helper.rb:
def add_task_link(name) link_to_remote "Add a task", :url => { :controller => "projects", :action => "add_task_script" } end
Here I’ve changed Ryan’s “link_to_function” call to “link_to_remote.” As Ryan explains in part 2 of his complex forms screen cast, link_to_function avoids an AJAX call to the server to obtain the HTML for each new task <input> tag, avoiding unnecessary load on the server since all of the task fields are the same. However, with my changes to auto_complete the HTML generated for the task field contains random numbers which are different for each copy of the field… meaning that we do need a separate call to the server to obtain the task field HTML and script. To handle the call from link_to_remote, I’ve added a new file, views/projects/add_task_script.rjs:
page.insert_html :bottom, :tasks, :partial => 'task', :object => Task.new
… which works essentially the same way as described by Ryan, but is called each time the user clicks “Add a task.”
The last change I made to the sample app is in routes.rb; these changes are required to allow the controller to map the Ajax requests, and to insure that these requests use GET, and not POST HTTP requests:
map.connect 'projects/auto_complete_for_project_name', :controller => 'projects', :action => 'auto_complete_for_project_name' map.connect 'projects/auto_complete_for_task_name', :controller => 'projects', :action => 'auto_complete_for_task_name' map.connect 'projects/add_task_script', :controller => 'projects', :action => 'add_task_script' map.resources :projects, :collection => { :auto_complete_for_project_name => :get, :auto_complete_for_task_name => :get }
This certainly seems very ugly, and probably could be simplified! But for now, we need this code to avoid problems with CRSF protection; see http://www.ruby-forum.com/topic/128970.