Pat Shaughnessy

Ribadesella, Spain

Auto complete for complex forms using nested attributes in Rails 2.3

June 14, 2009 · 20 comments

I just updated my fork of the auto_complete plugin to support Rails 2.3 nested attributes. Thanks to Anthony Frustagli for the code and ideas that I used as the basis for this fix.

Basic usage:

To use auto_complete on a complex form with nested attributes, just call “text_field_for_auto_complete” right on the form builder object yielded by form_for or fields_for, like in this example:

<% parent_form.fields_for :children do |child_form| %>
  <%= child_form.text_field_with_auto_complete :name, {},
        { :method => :get, :skip_style => true } %>
<% end %>

If you have Rails 2.3, this code will iterate over each child object and display a text field with auto complete support. My plugin will generate HTML and Javascript that works even when repeated in a loop like this. Also note that I’ve left off the object name parameter from text_field_with_auto_complete. It’s not needed now, since the object is indicated by the surrounding call to fields_for. The other parameters are optional and are taken unchanged from the original auto_complete plugin:

  • “:method => :get” indicates GET requests should be used by the AJAX calls to load the pick list values, avoiding problems with CSRF protection.
  • And “:skip => :true” indicates that the inline CSS stylesheet used by the auto complete drop down Prototype code should be skipped. Since we’re iterating over child objects we don’t want the same CSS code repeated once for each; instead include it once in a parent object’s call to text_field_for_auto_complete or else just include it manually somewhere.

That’s it – it should just work. If you’re interested in learning more about how to use nested attributes and what my plugin is actually doing, read on…

Details:

To learn more, let’s take a look at a simple nested attribute example, using the Projects/Tasks models from Ryan Bates' complex forms screen cast:

class Project < ActiveRecord::Base
  has_many :tasks
  accepts_nested_attributes_for :tasks, :allow_destroy => true
end
class Task < ActiveRecord::Base
  belongs_to :project
end

A project has many tasks, and each task belongs to a project. Here I’ve also declared that each project “accepts nested attributes for” tasks. This is a new method added to ActiveRecord in Rails 2.3… for lots of examples and explanation just take a look directly at the new nested_attributes.rb code file in Rails 2.3. In a nutshell, “accepts_nested_attributes_for” tells ActiveRecord that the project model should be able to save the attributes of the associated task model objects when a project is saved. This means that when I submit my project form, it can also contain a series of task fields as well. For example, my view code might look something like this:

<% form_for @project do |project_form| %>
  <p>
    <%= project_form.label :name, "Project:" %>
    <%= project_form.text_field :name %>
  </p>
  <% project_form.fields_for :tasks do |task_form| %>
    <%= task_form.label :name, "Task:" %>
    <%= task_form.text_field :name %>
  <% end %>
<% end %>

This displays a name text field for the project, and then calls “fields_for” again right on the form builder yielded by form_for. This is new for Rails 2.3. In earlier versions of Rails you had to explicitly iterate over the child objects and call fields_for for each one. Now in Rails 2.3, you can call fields_for as a method of the parent form and it will automatically iterate over all of the child objects and call fields_for. If we take a look at the HTML generated by this example form, we’ll find something like:

<input id="project_name" name="project[name]"
  size="30" type="text" value="Some project" />
<input id="project_tasks_attributes_0_id"
  name="project[tasks_attributes][0][id]" type="hidden" value="1" />
<input id="project_tasks_attributes_0_name"
  name="project[tasks_attributes][0][name]" type="text" value="Task one" />
<input id="project_tasks_attributes_1_id"
  name="project[tasks_attributes][1][id]" type="hidden" value="2" />
<input id="project_tasks_attributes_1_name"
  name="project[tasks_attributes][1][name]" type="text" value="Task two" />

I’ve simplified this to make it more readable. You can see the iteration by project_form.fields_for :tasks, and that for each task there’s an <input> tag for the “name” field, along with another hidden <input> tag containing the task’s “id” attribute. The most important detail here is the name given to each of these tags: “project[tasks_attributes][0][name]” for example. Since the tasks are nested attributes of the project, they are displayed using the PARENT_OBJECT[CHILD_OBJECTS_attributes][INDEX][FIELD] pattern, while for the project we get the simple OBJECT[FIELD] pattern. This is the key to making nested attributes work. In our project model, when we called “has_many :tasks”, Rails defined some new methods for us on the Project class to handle tasks: tasks, tasks=, task_ids, task_ids= and a couple of others as well. Now with Rails 2.3, when we call “accepts_nested_attributes_for :tasks” Rails defines another new method for Project called tasks_attributes= in order to process all of the new nested parameters for tasks when the complex project form is submitted. This is the reason for the “_attributes” in the naming pattern used in the form.

Now… how do we get auto complete to work for this form? The problem with auto complete on a complex form has always been that the Javascript and HMTL used by the Prototype library assumes that the <input> tag, <div> tag and related Javascript code would be unique on the HTML page. If you just call the text_field_with_auto_complete macro from the standard auto_complete plugin like this…

<% project_form.fields_for :tasks do |task_form| %>
  <%= text_field_with_auto_complete :task, :name, {},
        { :method => :get, :skip_style => true } %>
<% end %>

… it will not work. The first problem is that text_field_with_auto_complete does not know that fields_for is iterating over the child tasks, or which task is currently being processed in the iteration. But even if you were able to identify the current task object somehow, you would still get HTML like this:

<input id="task_name" name="task[name]" size="30" type="text" />
<div class="auto_complete" id="task_name_auto_complete"></div>
<script type="text/javascript">
//<![CDATA[
var task_name_auto_completer = new Ajax.Autocompleter('task_name',
  'task_name_auto_complete', '/projects/auto_complete_for_task_name',
  {method:'get'})
//]]>
</script>

…

<input id="task_name" name="task[name]" size="30" type="text" />
<div class="auto_complete" id="task_name_auto_complete"></div>
<script type="text/javascript">
//<![CDATA[
var task_name_auto_completer = new Ajax.Autocompleter('task_name',
  'task_name_auto_complete', '/projects/auto_complete_for_task_name',
  {method:'get'})
//]]>
</script>

Now the <input id=“task_name”> tag is repeated on the same page, and the Javascript call to Ajax.Autocompleter('task_name', … ) will not work since the browser will not be able to identify which <input> tag to use.

If you use my plugin instead of the original auto_complete plugin…

$ rm -rf vendor/plugins/auto_complete
$ ./script/plugin install git://github.com/patshaughnessy/auto_complete.git
Initialized empty Git repository in /Users/pat/rails-app/vendor/plugins/auto_complete/.git/
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 20 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (20/20), done.
From git://github.com/patshaughnessy/auto_complete
 * branch            HEAD       -> FETCH_HEAD

… and restart your Rails app, then you can change your view to call text_field_with_auto_complete as a method of the form builder, like this:

<% project_form.fields_for :tasks do |task_form| %>
  <%= task_form.text_field_with_auto_complete :name, {},
  { :method => :get, :skip_style => true } %>
<% end %>

Note that I’ve also dropped :task as a parameter since that’s implicit in the call to fields_for. In fact, since text_field_with_auto_complete is now a method of the FormBuilder object (“task_form”), it has access to the task object currently being processed in the iteration. Now if you refresh the same form you’ll instead get this HTML instead:

<input id="project_tasks_attributes_0_name"
  name="project[tasks_attributes][0][name]"
  size="30" type="text" value="Task one" />
<div class="auto_complete" id="project_tasks_attributes_0_name_auto_complete">
</div><script type="text/javascript">
//<![CDATA[
var project_tasks_attributes_0_name_auto_completer =
  new Ajax.Autocompleter('project_tasks_attributes_0_name',
  'project_tasks_attributes_0_name_auto_complete',
  '/projects/auto_complete_for_task_name',
  {method:'get', paramName:'task[name]'})
//]]>
</script>

…

<input id="project_tasks_attributes_1_name"
  name="project[tasks_attributes][1][name]"
  size="30" type="text" value="Task two" />
<div class="auto_complete" id="project_tasks_attributes_1_name_auto_complete">
</div><script type="text/javascript">
//<![CDATA[
var project_tasks_attributes_1_name_auto_completer =
  new Ajax.Autocompleter('project_tasks_attributes_1_name',
  'project_tasks_attributes_1_name_auto_complete',
  '/projects/auto_complete_for_task_name',
  {method:'get', paramName:'task[name]'})
//]]>
</script>

This looks much better, and will actually work for the following reasons:

  • The <input> tags have the correct names, using the PARENT_OBJECT[CHILD_OBJECTS_attributes][INDEX][FIELD] pattern from fields_for. This means that the field values will be processed properly by ActiveRecord when the form is submitted.
  • My changes to the auto_complete plugin have picked up the child object index, 0 and 1 in this example, and included it in the <input> tag’s id, the <div> tag id and well as the associated Javascript code that calls Ajax.Autocompleter. Since all of the tag id’s are unique, the auto complete behavior works properly again for each text field.
  • The original “task” class name and “name” field name are passed unchanged into the Ajax calls to the server. This means that in your controller you can continue to use “auto_complete_for :task, :name” as usual, without worrying about the complex form and the fact that the task fields are repeated multiple times, etc.:
    Ajax.Autocompleter('project_tasks_attributes_1_name',     
      'project_tasks_attributes_1_name_auto_complete',
      '/projects/auto_complete_for_task_name',
      {method:'get', paramName:'task[name]'})
    Here the third parameter to Ajax.Autocompleter, "/projects/auto_complete_for_task_name", is the AJAX URL which you need to account for in routes.rb, and paramName:'task[name]' tells the auto_complete_for handler in your controller to get the task names as usual, and protects the server side code from all of the complexity around the tag id, names, child object index, etc.

Tags:

20 responses so far ↓

  • 1 Fahri // Sep 06, 2009 at 06:35 AM

    Hi Pat; Thanks again for the great work. I just wanted to share that I used your auto_complete plugin fork with Eloy Duran's complex form examples and figured out that Duran's example uses javascript to replace NEW_RECORD strings with unique ids while dynamically adding fields whereas your auto_complete generates its own unique_object_names. In order to get the auto_complete to work with dynamically added fields, I changed your unique_object_name = "#{class_name}_#{Object.new.object_id.abs} code to unique_object_name = "#{class_name}_NEW_RECORD and now it works great with 1 level nesting (haven't tried 2 level nesting).
  • 2 pat // Sep 06, 2009 at 06:55 PM

    What a great solution! Thanks for sharing… I’ll definitely take a closer look at Eloy’s sample app. I like his use of search/replace with javascript on the client side when you add a new field dynamically since this avoids unnecessary calls back to the server. And your change to my plugin is a clever way to take advantage of that.

    Two things that I’m thinking about:

    • It would be nice to avoid putting the “NEW_RECORD” string in the plugin code, since this is specific to the sample app. Instead it would be a bit cleaner to specify :index => “NEW_RECORD” as a tag option to text_field_with_auto_complete. But right now the :index option is broken in my fork of auto_complete. I’ll try to post a patch soon.
    • Do you have problems when the “NEW_RECORD” string is not put through the javascript search/replace, for example when there are two or more existing records? I was trying your idea today and ran into an error: “unknown attribute: NEW_RECORD”

  • 3 Fahri // Sep 09, 2009 at 10:15 AM

    Hi Pat,
    Thanks for your kind comments.

    Regarding your two notes:
    • You are right about the downside of manipulating the plugin for a specific sample app and adding a specify :index as a tag option would be a cleaner solution.
    • I have no such problem as the "NEW_RECORD" string is not put through the javascript-replace for existing records. Existing records are generated with rendering partial as a collection, and they get their unique ids appended to their name starting from 0.
      The "NEW_RECORD" string is only added to the form template and replaced with a unique number each time the template is appended to the form.
  • 4 pat // Sep 15, 2009 at 05:32 PM

    Fahri, in case you check in again on this issue, during the past week I did push a patch up to github for this. Now my version of auto_complete should work unchanged in Eloy Duran’s sample app. (http://github.com/alloy/complex-form-examples). You just need to get the latest version of my auto_complete plugin from github again and just try it in Eloy’s app unchanged… The change I made today was to allow you to specify the index/random number used in the auto complete javascript using “:child_index” like this:

    form.text_field_with_auto_complete :name, {}, { :method => :get, :skip_style => true, :child_index => 'NEW_RECORD' }

    Text_field_with_auto_complete will also look for the :child_index option in the surrounding fields_for… so for Eloy’s sample app you don’t even need to specify it at all.

  • 5 SaveTimE // Jan 07, 2010 at 03:54 PM

    Hello Pat, thanks for your nice tutorials and scripts. Brought it easily to work. Only problem is: The example doesn't fit to my models and I still have problems to make it fit. I have 3 Models: recipe, ingredient and ingredientstorecipe. In ingredientstorecipe I have the recipe_id, ingredients_id, amount_of_ingredient, sequenz My main controller is recipe_controller In the edit_view for recipes I want to handel all actions need. For example: If I want to change the amount_of_ingredient or the sequenz all works fine, but if I want to add a new ingredient to over the recipe_form I get stucked. Even if I link to ingredienttorecipe_controller :action=>edit, :query =>@recipe and read it out with
    @ingredienttorecipe = Ingredienttorecipe.find(:all,
                   :conditions => ["ingredient_id LIKE ?",
                     "#{params[:query]}"])
    - even if I get the right amount of records - the
    <% form_for @ingredienttorecipe do |f| %>
    respond with:
    "Unkown Methode : 'ingredienttorecipe_ingredienttorecipe_ingredienttorecipe_ingredienttorecipe_ingredienttorecipe_ingredienttorecipe_ingredienttorecipe_path'"
    . The read out of the array @ingredienttorecipe works with: @ingredienttorecipe[1][:name]. If I use :
    <% form_for :ingredienttorecipe do |f| %>
    I get the partial rendered, but the auto_complete looks like this:
      //<![CDATA[
      var nil_class_36651630_recipe_auto_completer =
        new Ajax.Autocompleter(
          'nil_class_36651630_recipe',
          'nil_class_36651630_recipe_auto_complete',
          '/ingredienttorecipe/auto_complete_for_nil_class_recipe',
          {paramName:'nil_class[recipe]'})
    the models are set up as followed:
      recipe.rb
      has_many :ingredientstorecipes
      has_many :ingrdients, :through => ingredientstorecipes
    
      ingredient.rb
      has_many :ingredientstorecipes
      has_many :recipes, :through => ingredientstorecipes
    
      ingredientstorecipe.rb
      has_and_belongs_to_many :recipes
      has_and_belongs_to_many :ingredients
    I think the problem is somewhere in the relations of the models or in the struktur of the array @ingredienttorecipe It would be nice to find some help. Thanks a lot SaveTimE
  • 6 pat // Jan 07, 2010 at 10:55 PM

    Sorry about the poor code formatting; I need to reconfigure Mephisto to handle code in comments better...:(

    I would try using the Rails nested attributes feature in your app with recipes and ingredientstorecipe and create a single complex form that uses auto_complete. Did you try view mapper to create a nested form in your app with your models, like I describe here? I don't think it's a good idea to use form_for on the child model directly like that. Also the 'nil' values in the javascript might be caused by using a symbol :ingredienttorecipe in form_for... again try using nested attributes and fields_for instead.

    A couple of other people have run into some trouble using the complex form sample app on models in a has_many => through association. Next week I'm hoping to post an article about how to make that work properly, if you can wait that long. I hope this helps; let me know if I can help answer other questions.

  • 7 Tester // Feb 11, 2010 at 05:06 AM

    Hi Pat, thanks for the contribution. Great work! We are using your autocomplete successfully in nested forms. There is one very smal thing which does not work so. It's a problem which only exists in the IE 6 and IE 7. It looks like it's not such a big deal, but if you give it a close thought it actually is. When a user types something into the autocomplete for the very first time, then it does not open under IE6 and IE7. So if you enter for the first time "Will not" and then you stop tipping, nothing will happen, even if there are suggestions to make. If you then continue typing " work the", the autosuggest opens. You could say, well that really does not matter, but there are use cases where it can be really problematic, because the use will not notice and understand this. The first time the user types something in, it actually gets send to the server, but then the result is not displayed. So it's something probably with the client code and the event handling. If this could be fixed it would be great.
  • 8 pat // Feb 11, 2010 at 11:05 PM

    Thanks! Good to hear you were able to get a nested form to work with auto_complete. It looks like your issue with the first request using IE6 or IE7 is a well known problem with the Prototype javascript library, which is the code that actually implements the completion behavior in the browser. See this discussion and fix on stackoverflow: Script.aculo.us Autocompleter problem in IE. Also there were links to other solutions in this lighthouse bug report for Prototype and this Rails bug report.
  • 9 Albert // Apr 06, 2010 at 11:51 PM

    Hi, Pat. Please help me... ^_^ I have tried to render partially the child attributes. I move the section of <% f.fields_for .... %>.... <% end %> to separate file, and i will used this from my javascript to update the child object. It all run smooth, until i got error when re-rendering the child, and it's stated that "undefined... f" . I know that i need to pass the "f" variabel form the "<% form_for .... do |f| %>", but how i do this from my controller? Thanks.
  • 10 pat // Apr 07, 2010 at 10:17 AM

    Hey Albert, can you give me a bit more context about what you’re trying to do? Then I might be able to post some sample code to help you out… is it that you have an “update” button for each child, and you’re trying to update and redisplay them separately with ajax, rather than updating the entire page? Or something else?

    Sorry I don’t want to give you a quick answer without understanding where you’re trying to go with this…

  • 11 Albert // Apr 08, 2010 at 03:46 PM

    Hey Pat, Let say i have nested attribute, which are ClassRoom and Student. A class_room have attribute 'subject'. Class room can have many student, dependent on the 'subject' of that class_room. When the attribute 'subject' on class_room changed, then i want to update the student collection based on the 'subject' value. Rather then updating the whole page, i think better we only render the html element of "students_children" with javascript, that doing xhr to an action inside ClassRoomController, and given respond using ...js.rjs file, and do partial render for 'students'. In order to do so, I need to move the <%= f.fields_for :students do |student|%> ... <%%> to partial file, and when i rendering this partial file, i will get an error of "...undefined... f variabel". This happening because the 'f' variabel which is the class_room form not defined yet. So far i know we can not pass FormBuilder variabel to controller, in order to pass it to partial file. Do have suggestion in order to solved this? Thanks.
  • 12 pat // Apr 08, 2010 at 11:04 PM

    Hi… so I did something similar to this at my day job recently using JQuery, and avoided this whole issue by returning the entire form in Ajax, and not just the partial. Then on the client JQuery parses the response and replaces just the portion of the form you want to reload.

    Here’s a simple example to show you what I did: http://pastie.org/910832

    • This is a simple form for a classroom object, with a single name field and no nested attributes. Your form is probably a lot more complex than this, but the same idea will work for you.
    • Note the form that we want to reload is inside a classroom_form div (line 19)
    • When the user clicks on the “Reload form…” link, the JQuery click function is called for it (line 6)
    • This makes an Ajax call, using the JQuery Ajax API. It sends the Ajax request to the url set on the reload link: edit_classroom_path(classroom) in my simple example. (line 17)
    • The server generates the ENTIRE classroom form again, including the entire page: the layout, the form tag, the reload link, etc. There’s no special code on the server to only return just the _form partial or whatever. This avoids your problem about getting the form builder in the controller… you don’t need to do that at all. Just render the entire page as usual.
    • When JQuery gets the Ajax response, it calls afterReload (line 2)
    • Now on line 3 afterReload replaces the contents of the classroom_form div with the new contents of the same div present in the ajax response. This is the trick: we don’t need to generate only the partial form on the server… it just returns the entire form as usual and afterReload gets the portion we want.
    • Don’t forget to include jquery.js in your layout: <%= javascript_include_tag "jquery" %> … and put jquery.js in your public/javascripts folder.
    I hope this makes some sense… I’m not sure if you want to use JQuery or not, or if you want to stick with RJS but maybe this idea might work for you in your app in some way: return the entire form but then parse out and replace just the piece you need.

  • 13 Albert // Apr 08, 2010 at 11:28 PM

    Hi Pat, this look great idea. Let me try this and i will let you know the result. Thank you so much.
  • 14 Albert // Apr 09, 2010 at 01:17 PM

    Hi Pat, I have try to mimic what you done.. but i can not get it work... If you don't mind can i send you my code, so you can help me figured out what went wrong. If it's ok, please send me your email address.
  • 15 pat // Apr 12, 2010 at 08:41 PM

    Hi Albert, did you ever figure this out? I sent a note to your gmail account the other day, but never heard back. My address is just pat@patshaughnessy.net.
  • 16 Albert // Apr 14, 2010 at 05:24 AM

    Hi Pat, sory not to reply your email. I just don't want to bother your time too much ^_^. After reading all the document about nested attributes, prototype and jquery, i have figured out how to change this partial. Finally i know that jquery-1.4.2 and prototype-1.6.js did not work along nicely. Still using your idea, i changed my code to : 1. Change the action to just render the form without layout. 2. change a little code in your sample to, $(".classroom_form").html($(".classroom_form", response).html()) Btw, i am using remote_function at my onchange event, so i have to change it to jquery Ajax.post{} call. Here the source, http://www.mail-archive.com/rubyonrails-talk@googlegroups.com/msg54944.html Pat, if you want i can send you my working code to your email. Thanks.
  • 17 Albert // Apr 14, 2010 at 05:27 AM

    Pat, just curios, how to add new line to this comment? thanks.
  • 18 pat // Apr 14, 2010 at 10:15 AM

    Good to hear you got it working… I took a look at the code you linked to; it looks great. No need to email it to me. I actually used a similar solution at my day job just last week. All of this makes me think I should re-implement some of the View Mapper scaffolding using JQuery instead.

    Sorry about the comment formatting; I just reconfigured my copy of Mephisto to use Markdown/SmartyPants for comments so now you would just add extra blank lines between your paragraphs.

  • 19 Albert // Apr 19, 2010 at 06:26 AM

    Thank you so much…

    ^_^

  • 20 Jeff Shurts // Aug 31, 2010 at 03:42 PM

    Thank you, thank you, thank you! Saved me a bunch of time! I have an application that is full of forms with nested models, and equally full of autocomplete text fields. Your fork of the auto_complete plugin worked flawlessly upon installation. You’re a credit to your profession…

Leave a Comment