Pat Shaughnessy

Ribadesella, Spain

Repeated_auto_complete changes merged into auto_complete

January 29, 2009 · 4 comments

Update June 2009: I just added support to my version of auto_complete to support Rails 2.3 nested attributes; for more details see: http://patshaughnessy.net/repeated_auto_complete. The basic ideas below still apply, but my implementation of auto_complete has changed, and I’ve also simplified the usage.

 

In October I described how the auto_complete plugin doesn’t work when text fields are repeated more than once on a complex form. I went on to write a plugin called “repeated_auto_complete&rdquo; which modified the way the standard auto_complete plugin works and fixed this problem by adding random numbers to <input id=""> attributes among other changes.

Since it’s much cleaner to have a single auto_complete plugin rather than two separate plugins, I’ve merged my changes to auto_complete into the original version, and pushed them to github as a new fork: http://github.com/patshaughnessy/auto_complete

To install and use my modified version of auto_complete first remove the standard auto_complete plugin from your app if necessary, and install with:

script/plugin install git://github.com/patshaughnessy/auto_complete.git

To use auto complete in a complex form, you write “auto_complete_fields_for” or “auto_complete_form_for” in your view, and then call text_field_with_auto_complete on the form builder object, as follows:

<% for person in @group.people %>
  <% auto_complete_fields_for "group[person_attributes][]", person do |form| %>
    Person <%= person_form.label :name %><br />
    <%= form.text_field_with_auto_complete :person, :name, {},
                                           {:method => :get}  %>
  <% end %>
<% end %>

To understand my changes to the plugin, let’s first look at how the original auto_complete works. If you add this line to your view:

<%= text_field_with_auto_complete :project, :name, {}, {:method => :get } %>

…then you get HTML and script that looks like this (style sheet omitted):

<input id="project_name" name="project[name]" size="30" type="text" />
<div class="auto_complete" id="project_name_auto_complete"></div>
<script type="text/javascript">
//<![CDATA[
var project_name_auto_completer = new Ajax.Autocompleter('project_name',
'project_name_auto_complete', '/projects/auto_complete_for_project_name',
{method:'get'})
//]]>
</script>

The original text_field_with_auto_complete method looked like this:

def text_field_with_auto_complete(object, method, tag_options = {},
                                  completion_options = {})
    (completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
    text_field(object, method, tag_options) +
    content_tag("div", "", :id => "#{object}_#{method}_auto_complete",
                :class => "auto_complete") +
    auto_complete_field(
        "#{object}_#{method}",
        { 
          :url => { :action => "auto_complete_for_#{object}_#{method}" }
        }.update(completion_options))
  end

You can see that it calls “text_field” in ActionView::Helpers::FormHelper to generate the actual <input> tag for the form, in addition to generating the HTML and script needed for the auto completion behavior.

What I wanted to achieve in the modified plugin was to allow the view to contain code like this:

<% auto_complete_fields_for task do |f| %>
  <%= f.label :name, "Task:" %>
  <%= f.text_field_with_auto_complete :task, :name, {}, {:method => :get } %>
<% end %>

To make this work, we need a new version of text_field_with_auto_complete that calls text_field from ActionView::Helpers::FormBuilder, and not ActionView::Helpers::FormHelper, generating an <input> tag similar to what this call would generate:

<% fields_for task do |f| %>
  <%= f.text_field :name %>
<% end %>

To do this, I first refactored the original text_field_with_auto_complete in auto_complete_macros_helper.rb:

def text_field_with_auto_complete(object, method, tag_options = {},
                                  completion_options = {})
  auto_complete_field_with_style_and_script(object, method, tag_options,
                                            completion_options) do
    text_field(object, method, tag_options)
  end
end

def auto_complete_field_with_style_and_script(object, method,
                                              tag_options = {},
                                              completion_options = {})
  (completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
  yield +
  content_tag("div", "", :id => "#{object}_#{method}_auto_complete",
              :class => "auto_complete") +
  auto_complete_field(
    "#{object}_#{method}",
    {
      :url => { :action => "auto_complete_for_#{object}_#{method}" } 
    }.update(completion_options))
end

Here I’ve introduced a new utility function called “auto_complete_field_with_style_and_script” that generates the same Javascript and style sheet for the view as before, but instead calls a block to generate the actual text field. Then I changed text_field_with_auto_complete to call this, providing a block to make the call to “text_field” in ActionView::Helpers::FormHelper with the proper names and options.

Now my new form builder class in auto_complete_form_helper.rb contains a version of text_field_with_auto_complete that looks like this:

def text_field_with_auto_complete(object,
                                  method,
                                  tag_options = {},
                                  completion_options = {})
  unique_object_name = "#{object}_#{Object.new.object_id.abs}"
  completion_options_for_original_name =
    {
      :url => { :action => "auto_complete_for_#{object}_#{method}"},
      :param_name => "#{object}[#{method}]"
    }.update(completion_options)
  @template.auto_complete_field_with_style_and_script(
        unique_object_name,
        method,
        tag_options,
        completion_options_for_original_name
  ) do
    text_field(method,
               {
                 :id => "#{unique_object_name}_#{method}"
               }.update(tag_options))
  end
end

Here the call to auto_complete_field_with_style_and_script passes a block that calls the other text_field from ActionView::Helpers::FormBuilder (note the “object” parameter is not present as above).

To allow the text field to be repeated on a complex form, I insure the object’s name is unique by adding a random number to it (“unique_object_name”). This unique name is then passed into both auto_complete_field_with_style_and_script and text_field, insuring that the <input> and related Javascript all work without problems, even if the text field is repeated more than once on the same form.

The last important detail here is that the completion options passed into auto_complete_field_with_style_and_script are generated using the original, unchanged (non-unque) object name, so that the Ajax calls to the server are made using the original name. This means no changes are required on the server side, and the same single line of code in your controller still works as usual:

auto_complete_for :task, :name

Next time I’ll post a sample application that uses this new plugin, and explain what changes you will need to make to your own application for auto_complete in a complex form.

Tags:·

4 responses so far ↓

  • 1 Matt // Feb 22, 2009 at 11:55 AM

    This is great! Thanks! I just have one issue I'm wondering if you can help me with... I have a form that I've got working with your forked version of the plugin doing two things - 1) using repeated inputs with auto_complete for the same field. The other thing I'm using it for, on a couple of other fields, is to get my fields working with your 'prefix' option. So, for example, imagine this form: Music information ----------- Artist: Some Guy Album: An Album Available Media Type: Vinyl Available Media Type: CD I'm using the multiple auto_complete inputs for Available Media Type. Then, because artists and albums are their own models, I'm using something like: <% prefix = "music[artist_name]" %> <% auto_complete_fields_for prefix, @music.artist do |artist_form| -%> <%= artist_form.text_field_with_auto_complete :artist, :name, { :size => '20'}, {:method => :get, :skip_style => true} %> <% end %> This way the artist gets submitted as part of the 'music' param, which is perfect. So, here's my problem... before I started down this path, I was using the regular auto_complete plugin, and was using a small piece of java script to pass the artist name along to the auto_complete for the album. That way, auto_complete for the album is filtered to only show albums known to belong to the artist that's currently entered in the artist box. I therefore had something like: ... { callback: function(e, qs) { return qs + '&artist=' + $F(artist[name]); } ... Now, however, I can't figure out how to get that last piece of code working - as I can't figure out how to get it the dynamically generated ID for the artist name... because it contains that random number. Any ideas? I hope I haven't made this too confusing :) Thanks!
  • 2 pat // Feb 22, 2009 at 07:51 PM

    Thanks so much for trying my plugin Matt… it seems to me that the key to your problem is the random number my code is inserting into the HTML/Javascript object names generated by text_field_with_auto_complete. If you had the ability to specify this value (you would have to insure it was unique) then you could write Javascript in your form as desired to set different values. For example, you could set the “random number” instead to be the index/counter value from your loop generating the repeated fields…

    I’m imagining a new tag option called “:unique_object_id => 1234”. Then in my code instead of unique_object_name = "#{object}_#{Object.new.object_id.abs}" it would use “#{object}_#{tag_options[:unique_object_id]” if this value were present. Just thinking aloud here… would this work for you? If so, I might have time later this week to code it up and post it on github. Or feel free to try it yourself and let me know.

  • 3 Matt // Feb 22, 2009 at 08:54 PM

    Thanks for the fast reply :) You proposed solution looks like it'd work for what I was doing. Don't worry about spending the time coding it just for this though - I've actually realized that I would benefit from moving those non-repeating columns back to the old method (without the random numbers - and not as part of the 'music' param). I'll still be using your plugin to do the repeated 'Media Type' fields and such - but I've realized that there's a dependency for me to control the order in which the other params are processed - and because of the fact that I'm going to have to manually handle that anyway - I don't need the album and artist 'prefixed.' Anyway - thanks again for sharing your work! Much appreciated!
  • 4 Nico // Jan 06, 2010 at 05:24 AM

    Awesome. I was going to resort to a monkey patch when at last I came upon your site. Thank you very much :)

Leave a Comment