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.

3 comments Tags:·

Autocomplete plugin doesn’t work for repeated fields

October 20, 2008 · 1 comment

Recently I tried using the autocomplete plugin from Rails for the first time. It was a great way to quickly implement type-ahead Ajax behavior by writing little or no code. By simply adding two lines – one to my controller:
auto_complete_for :group, :name
and another to my form:
<%= text_field_with_auto_complete :group, :name, {}, {:method => :get} %>
users of my form were able to easily pick from a long list of group names simply by typing the first few letters. What I love about Rails is not only was I able to implement this complex feature with only 2 lines, but those 2 lines were very easy to read: my controller has to handle “autocompletion for group names” and my form should contain a “text field with auto complete behavior for group names”. It’s very natural and intuitive.

The only ugly detail here is that the “{:method => :get}” hash is required to avoid errors related to form security… without it I get an error like this:

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
    /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.1/lib/action_controller/request_forgery_protection.rb:86:in 'verify_authenticity_token'
This discussion explains that we can avoid the security exception by forcing the autocomplete request to use GET instead of POST. Note that you also need to change config/routes.rb to map the GET request for the completion values to the autocomplete action in your controller. This is a bit ugly, but not a big deal.

However, once I tried adding additional fields to the form I ran into some real trouble. I enhanced my form by adding multiple child model objects; in this case I associated a series of people for each group. That is:

class Person < ActiveRecord::Base`
 belongs_to :group
and:
class Group < ActiveRecord::Base
 has_many :people
Following Ryan Bates’ great explanation on railcasts about how to implement a form for both parent and child records at the same time, I ended up with a form that looked like this:
<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 %>
This looks a lot more complicated, but except for the “index => nil” hack (see part 3 of Ryan’s screen cast for an explanation) the code is very clean and straightforward to understand. We display the group name autocomplete text field as before, and then iterate over the people contained in this group, displaying a new autocomplete text field for each one.

Too bad it doesn’t work! Even worse, this code actually has 3 separate problems: First, the <input> tag generated by text_field_with_auto_complete is not what we want for the child objects. Without autocomplete, we would use:

<%= person_form.text_field :name %>
referring to the “fields_for” method just above and get this HTML:
<input id="group_person_attributes__name" name="group[person_attributes][][name]" size="30" type="text" />
But text_field_with_auto_complete :person, :name, … yields this instead:
<input id="person__name" name="person[][name]" size="30" type="text" />
along with CSS and Javascript for the auto complete behavior.

Second, the server side code added to my controller also doesn’t know where to look for the person name value in the actual parameter hash; instead it assumes it will receive the parameter as specified in the <input> above: params[“person”][“name”]. See vendor/plugins/autocomplete/lib/auto_complete.rb for details.

And the last and worst problem of all is that the DHTML/Javascript code inside of controls.js from script.aculo.us assumes that the <input> tag id passed in will be unique on the page. But since we will have many person fields for each group field, the script.aculo.us code will fail trying to location the <input> and <div> tags specified by text_field_with_auto_complete.

So what to do? The first time I solved this problem, I simply copied and hard coded the HTML and Javascript produced by the autocomplete plugin into my form erb file. Then I changed it to make it work. I also manually added a function to my controller on the server side to get it to find and process the parameter hash properly. If I were still writing PHP code, then I would probably assume this was sufficient and call it a day. If I happened to be using J2EE for this project, I’d probably look at creating a subclass of the autocomplete plugin somehow and extending/modifying the behavior as necessary. Then I would have yet another class and another JAR file to maintain and worry about – not quite as messy, but still ugly or at best complicated.

There must be a better way! Since I’m using Ruby I don’t need to settle for overly complicated code or ugly code. Instead I’ll try to develop a solution using what seems to me the Ruby philosophy: write the cleanest, tightest code you can that makes sense and then find a way to make it work. In my next post I’ll tell you what happened.

1 comment Tags:·