Pat Shaughnessy

Ribadesella, Spain

Modifying the auto_complete Plugin to Allow Repeated Fields

October 31, 2008 · 3 comments

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.

Tags:·

3 responses so far ↓

  • 1 Val // Dec 30, 2008 at 06:10 PM

    Hi Pat, Thanks a lot for this article!
  • 2 Stephen Boisvert // Mar 13, 2009 at 02:12 PM

    Hi Pat, Me again. I've been playing around with this a bit more and come across a minor issue. The undocumented but useful option of :skip_style => true which lets you skip the default css styles doesn't seem to work with this plugin. I wanted to use this a for a specific purpose - simulating the related questions dropdown that stackoverflow has when you enter a new quesiton but other people might want it so they can override the default css.
  • 3 pat // Mar 13, 2009 at 10:35 PM

    Hi Stephen… Hmm I just tried it and it works for me. You just need to add it to the completion options parameter for text_field_with_auto_complete. For example, in my sample app (see this post) line 5 of _task.html.erb would be:
    f.text_field_with_auto_complete :task, :name, {}, { :method => :get, :skip_style => :true }

    Note the first empty hash passed as the 3rd parameter contains the tag options... :skip_style needs to go in the 4th parameter. This should work for you; if not send me the code you’re using and I’ll take a look.

Leave a Comment