Auto complete for complex forms using nested attributes in Rails 2.3

I just updated my fork of the auto_complete plugin to support Rails 2.3 nested attributes. Thanks to Anthony Frustagli for the code and ideas that I used as the basis for this fix.

Basic usage:

To use auto_complete on a complex form with nested attributes, just call “text_field_for_auto_complete” right on the form builder object yielded by form_for or fields_for, like in this example:

<% parent_form.fields_for :children do |child_form| %>
  <%= child_form.text_field_with_auto_complete :name, {},
        { :method => :get, :skip_style => true } %>
<% end %>

If you have Rails 2.3, this code will iterate over each child object and display a text field with auto complete support. My plugin will generate HTML and Javascript that works even when repeated in a loop like this. Also note that I’ve left off the object name parameter from text_field_with_auto_complete. It’s not needed now, since the object is indicated by the surrounding call to fields_for. The other parameters are optional and are taken unchanged from the original auto_complete plugin:

  • “:method => :get” indicates GET requests should be used by the AJAX calls to load the pick list values, avoiding problems with CSRF protection.
  • And “:skip => :true” indicates that the inline CSS stylesheet used by the auto complete drop down Prototype code should be skipped. Since we’re iterating over child objects we don’t want the same CSS code repeated once for each; instead include it once in a parent object’s call to text_field_for_auto_complete or else just include it manually somewhere.

That’s it – it should just work. If you’re interested in learning more about how to use nested attributes and what my plugin is actually doing, read on…

Details:

To learn more, let’s take a look at a simple nested attribute example, using the Projects/Tasks models from Ryan Bates' complex forms screen cast:

class Project < ActiveRecord::Base
  has_many :tasks
  accepts_nested_attributes_for :tasks, :allow_destroy => true
end
class Task < ActiveRecord::Base
  belongs_to :project
end

A project has many tasks, and each task belongs to a project. Here I’ve also declared that each project “accepts nested attributes for” tasks. This is a new method added to ActiveRecord in Rails 2.3… for lots of examples and explanation just take a look directly at the new nested_attributes.rb code file in Rails 2.3. In a nutshell, “accepts_nested_attributes_for” tells ActiveRecord that the project model should be able to save the attributes of the associated task model objects when a project is saved. This means that when I submit my project form, it can also contain a series of task fields as well. For example, my view code might look something like this:

<% form_for @project do |project_form| %>
  <p>
    <%= project_form.label :name, "Project:" %>
    <%= project_form.text_field :name %>
  </p>
  <% project_form.fields_for :tasks do |task_form| %>
    <%= task_form.label :name, "Task:" %>
    <%= task_form.text_field :name %>
  <% end %>
<% end %>

This displays a name text field for the project, and then calls “fields_for” again right on the form builder yielded by form_for. This is new for Rails 2.3. In earlier versions of Rails you had to explicitly iterate over the child objects and call fields_for for each one. Now in Rails 2.3, you can call fields_for as a method of the parent form and it will automatically iterate over all of the child objects and call fields_for. If we take a look at the HTML generated by this example form, we’ll find something like:

<input id="project_name" name="project[name]"
  size="30" type="text" value="Some project" />
<input id="project_tasks_attributes_0_id"
  name="project[tasks_attributes][0][id]" type="hidden" value="1" />
<input id="project_tasks_attributes_0_name"
  name="project[tasks_attributes][0][name]" type="text" value="Task one" />
<input id="project_tasks_attributes_1_id"
  name="project[tasks_attributes][1][id]" type="hidden" value="2" />
<input id="project_tasks_attributes_1_name"
  name="project[tasks_attributes][1][name]" type="text" value="Task two" />

I’ve simplified this to make it more readable. You can see the iteration by project_form.fields_for :tasks, and that for each task there’s an <input> tag for the “name” field, along with another hidden <input> tag containing the task’s “id” attribute. The most important detail here is the name given to each of these tags: “project[tasks_attributes][0][name]” for example. Since the tasks are nested attributes of the project, they are displayed using the PARENT_OBJECT[CHILD_OBJECTS_attributes][INDEX][FIELD] pattern, while for the project we get the simple OBJECT[FIELD] pattern. This is the key to making nested attributes work. In our project model, when we called “has_many :tasks”, Rails defined some new methods for us on the Project class to handle tasks: tasks, tasks=, task_ids, task_ids= and a couple of others as well. Now with Rails 2.3, when we call “accepts_nested_attributes_for :tasks” Rails defines another new method for Project called tasks_attributes= in order to process all of the new nested parameters for tasks when the complex project form is submitted. This is the reason for the “_attributes” in the naming pattern used in the form.

Now… how do we get auto complete to work for this form? The problem with auto complete on a complex form has always been that the Javascript and HMTL used by the Prototype library assumes that the <input> tag, <div> tag and related Javascript code would be unique on the HTML page. If you just call the text_field_with_auto_complete macro from the standard auto_complete plugin like this…

<% project_form.fields_for :tasks do |task_form| %>
  <%= text_field_with_auto_complete :task, :name, {},
        { :method => :get, :skip_style => true } %>
<% end %>

… it will not work. The first problem is that text_field_with_auto_complete does not know that fields_for is iterating over the child tasks, or which task is currently being processed in the iteration. But even if you were able to identify the current task object somehow, you would still get HTML like this:

<input id="task_name" name="task[name]" size="30" type="text" />
<div class="auto_complete" id="task_name_auto_complete"></div>
<script type="text/javascript">
//<![CDATA[
var task_name_auto_completer = new Ajax.Autocompleter('task_name',
  'task_name_auto_complete', '/projects/auto_complete_for_task_name',
  {method:'get'})
//]]>
</script>

<input id="task_name" name="task[name]" size="30" type="text" /> <div class="auto_complete" id="task_name_auto_complete"></div> <script type="text/javascript"> //<![CDATA[ var task_name_auto_completer = new Ajax.Autocompleter('task_name', 'task_name_auto_complete', '/projects/auto_complete_for_task_name', {method:'get'}) //]]> </script>

Now the <input id=“task_name”> tag is repeated on the same page, and the Javascript call to Ajax.Autocompleter('task_name', … ) will not work since the browser will not be able to identify which <input> tag to use.

If you use my plugin instead of the original auto_complete plugin…

$ rm -rf vendor/plugins/auto_complete
$ ./script/plugin install git://github.com/patshaughnessy/auto_complete.git
Initialized empty Git repository in /Users/pat/rails-app/vendor/plugins/auto_complete/.git/
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 20 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (20/20), done.
From git://github.com/patshaughnessy/auto_complete
 * branch            HEAD       -> FETCH_HEAD

… and restart your Rails app, then you can change your view to call text_field_with_auto_complete as a method of the form builder, like this:

<% project_form.fields_for :tasks do |task_form| %>
  <%= task_form.text_field_with_auto_complete :name, {},
  { :method => :get, :skip_style => true } %>
<% end %>

Note that I’ve also dropped :task as a parameter since that’s implicit in the call to fields_for. In fact, since text_field_with_auto_complete is now a method of the FormBuilder object (“task_form”), it has access to the task object currently being processed in the iteration. Now if you refresh the same form you’ll instead get this HTML instead:

<input id="project_tasks_attributes_0_name"
  name="project[tasks_attributes][0][name]"
  size="30" type="text" value="Task one" />
<div class="auto_complete" id="project_tasks_attributes_0_name_auto_complete">
</div><script type="text/javascript">
//<![CDATA[
var project_tasks_attributes_0_name_auto_completer =
  new Ajax.Autocompleter('project_tasks_attributes_0_name',
  'project_tasks_attributes_0_name_auto_complete',
  '/projects/auto_complete_for_task_name',
  {method:'get', paramName:'task[name]'})
//]]>
</script>

<input id="project_tasks_attributes_1_name" name="project[tasks_attributes][1][name]" size="30" type="text" value="Task two" /> <div class="auto_complete" id="project_tasks_attributes_1_name_auto_complete"> </div><script type="text/javascript"> //<![CDATA[ var project_tasks_attributes_1_name_auto_completer = new Ajax.Autocompleter('project_tasks_attributes_1_name', 'project_tasks_attributes_1_name_auto_complete', '/projects/auto_complete_for_task_name', {method:'get', paramName:'task[name]'}) //]]> </script>

This looks much better, and will actually work for the following reasons:

  • The <input> tags have the correct names, using the PARENT_OBJECT[CHILD_OBJECTS_attributes][INDEX][FIELD] pattern from fields_for. This means that the field values will be processed properly by ActiveRecord when the form is submitted.
  • My changes to the auto_complete plugin have picked up the child object index, 0 and 1 in this example, and included it in the <input> tag’s id, the <div> tag id and well as the associated Javascript code that calls Ajax.Autocompleter. Since all of the tag id’s are unique, the auto complete behavior works properly again for each text field.
  • The original “task” class name and “name” field name are passed unchanged into the Ajax calls to the server. This means that in your controller you can continue to use “auto_complete_for :task, :name” as usual, without worrying about the complex form and the fact that the task fields are repeated multiple times, etc.:
    Ajax.Autocompleter('project_tasks_attributes_1_name',     
      'project_tasks_attributes_1_name_auto_complete',
      '/projects/auto_complete_for_task_name',
      {method:'get', paramName:'task[name]'})
    Here the third parameter to Ajax.Autocompleter, "/projects/auto_complete_for_task_name", is the AJAX URL which you need to account for in routes.rb, and paramName:'task[name]' tells the auto_complete_for handler in your controller to get the task names as usual, and protects the server side code from all of the complexity around the tag id, names, child object index, etc.