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.
8 responses so far ↓
1 Fahri // Sep 06, 2009 at 06:35 AM
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:
3 Fahri // Sep 09, 2009 at 10:15 AM
Thanks for your kind comments.
Regarding your two notes:
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
@ingredienttorecipe = Ingredienttorecipe.find(:all, :conditions => ["ingredient_id LIKE ?", "#{params[:query]}"])- even if I get the right amount of records - the respond with: . The read out of the array @ingredienttorecipe works with: @ingredienttorecipe[1][:name]. If I use : 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: 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 SaveTimE6 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
8 pat // Feb 11, 2010 at 11:05 PM
Leave a Comment