Autocomplete plugin doesn’t work for repeated fields
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.