Modifying the auto_complete Plugin to Allow Repeated Fields

Update March 2009: I reimplemented the code from this article in a better way and posted it in a fork of auto_complete on github; see: http://patshaughnessy.net/repeated_auto_complete.... however, the basic ideas below still apply.

Last week I ran into trouble trying to use the auto_complete plugin like this for a form containing a single group name field, but a series of repeated people name fields:

<p>
  Group <%= f.label :name %><br />
  <%= text_field_with_auto_complete :group, :name,
                                       {}, {:method => :get} %></p>
<% for person in @group.people %>
  <% fields_for "group[person_attributes][]", person do |person_form| %>
    <p>
      Person <%= person_form.label :name %><br />
     <%= text_field_with_auto_complete :person, :name, {:index => nil},
                                      {:method => :get}  %></p>
    <% unless person.new_record? %>
      <%= person_form.hidden_field :id, :index => nil %>
    <% end %>
  <% end %>
<% end %>

I want to just display a text field repeated once for each person record. The problem is that the text_field_with_auto_complete macro returns HTML and Javascript that doesn’t work when it’s repeated many times. How can I to get this to work?

Let’s start by writing the code we would like to use, or wished could work someday. The key detail in the form above is the name of the object in fields_for:

fields_for "group[person_attributes][]", person do |person_form|

This name is used on the server to mass-assign the attributes of all of the person records to the parent group record. The only way text_field_with_auto_complete could possibly work is if it generated an <input> tag with the desired name: “group[person_attributes][][name]”. I could just pass this value into text_field_with_auto_complete, but that not be very DRY since I would have to repeat the name more than once.

What if I could just call text_field_with_auto_complete directly on the person_form object yielded by fields_for? For example:

<% fields_for "group[person_attributes][]", person do |person_form| %>
  <%= person_form.text_field_with_auto_complete :person, :name %>

Then this call could generate an <input> tag with a name generated from the fields_for object name and my form code would remain very simple, readable and maintainable.

I found a way to do this following John Ford’s example (Writing a Custom FormBuilder in Rails) by adding a new version of form_for and fields_for to ActionView. First I modified the auto_complete plugin’s init.rb file and added a line to mixin a new form helper module into ActionView like this:

ActionView::Base.send :include, AutoCompleteFormHelper

Then our new AutoCompleteFormHelper class will look like this:

module AutoCompleteFormHelper
  [:form_for, :fields_for, :form_remote_for, :remote_form_for].each do |meth|
    src = <<-end_src
      def auto_complete_#{meth}(object_name, *args, &proc)
        options = args.last.is_a?(Hash) ? args.pop : {}
        options.update(:builder => AutoCompleteFormBuilder)
        #{meth}(object_name, *(args &lt;&lt; options), &proc)
      end
    end_src
    module_eval src, __FILE__, __LINE__
  end
end

What this does is create new ActionView methods called “auto_complete_form_for,” “auto_complete_fields_for,” etc. These methods simply call the original form_for and fields_for but pass in an additional option “:builder” that indicates ActionView should yield a different class to the form block… a new class I will write called “AutoCompleteFormBuilder,” instead of the original FormBuilder class. Now by writing AutoCompleteFormBuilder we have the ability to implement the behavior we need from text_field_with_auto_complete.

Here’s what I ended up with:

class AutoCompleteFormBuilder < ActionView::Helpers::FormBuilder
  def text_field_with_auto_complete(object, method, tag_options = {},
                                    completion_options = {})
    @template.text_field_with_auto_complete(
      "#{object}_#{Object.new.object_id}",
      method,
      { :name => "#{@object_name}[#{method}]" }.update(tag_options),
      { :param_name => "#{object}[#{method}]",
        :url => { :action => "auto_complete_for_#{object}_#{method}" }
      }.update(completion_options)
    )
  end  
end

This simply calls the original text_field_with_auto_complete with slightly different parameters:

  • We generate a unique number and add it as a suffix to the id attribute that will be generated for the <input> tag, and referenced in the Javascript:
    "#{object}_#{Object.new.object_id}"
    Now each <input> tag will have a unique id attribute, and the script.aculo.us autocomplete Javascript will work unmodified. Without this change, all of the repeated <input> tags would have the same id, and the autocomplete code would fail trying to find the ambiguous id on the page.
  • We pass in the object name from the fields_for or forms_for declaration as the name attribute for the <input> tag:
    { :name => "#{@object_name}[#{method}]" }.update(tag_options)
    Now our model code works as originally intended when the form is submitted, i.e. the "person_attributes" value will be mass-assigned to our child model objects.
  • We get the autocomplete behavior to work by telling it explicitly to use the simple “person[name]” object name in both the parameter name and URL for the AJAX request:
    { :param_name => "#{object}[#{method}]",
      :url => { :action => "auto_complete_for_#{object}_#{method}" }
    }.update(completion_options)
    This allow us to continue to use the same simple declaration in the controller that handles the auto complete requests:
    auto_complete_for :person, :name

Next step: Post this on GitHub.