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.
20 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
9 Albert // Apr 06, 2010 at 11:51 PM
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
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
14 Albert // Apr 09, 2010 at 01:17 PM
15 pat // Apr 12, 2010 at 08:41 PM
16 Albert // Apr 14, 2010 at 05:24 AM
17 Albert // Apr 14, 2010 at 05:27 AM
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