Introduced in Rails 2.3 nested attributes help to shorten code if you want to
edit multiple nested models within a single form.
Here the corresponding article from the rails weblog:
http://weblog.rubyonrails.org/2009/1/26/nested-model-forms
There is also a very good tutorial on nested forms from Ryan Daigle:
http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes
Right now it seems as if there is no clean way to have ajax enabled nested forms.
Let’s have a look at it in more detail using a simple invoice example.

Each Invoice has one or more InvoiceLineItems.
Actually this 1:n situation occurs in nearly every Rails application but there are two things turning this into a special case. First, we will use the nested attributes feature which has been added in Rails 2.3. Seccond, we want to have an ajax enabled form where invoice items can be created, edited and deleted without reloading the whole page.
As mentioned before a very good guide on how to create a nested form without ajax capabilities is can be found under
http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes
This guide will be the foundation for this application so you might want to read it before reading further this article.
Note:
This is not meant to be a best practice suggestion. This article’s main intention is to show and discuss some of the problems you run into when creating ajax forms using nested attributes.
I tried to create a clean solution but I think things should be easier than this. So either I am somehow unable to see what I am missing or there is still some work to be done within the Rails framework itself.
Take the challenge!
If you see an easier way to overcome problems mentioned here feel free to drop me an email or post a comment.
Prerequisites
Before we start let’s create a non ajax enabled application as described in Rayan’s guide.
You can also find this non-ajax version of this app at github (initial checkin):
http://github.com/enterprise-rails/invoice_nested_attributes_ajax/tree/f7f676ad344cf1e364fcb7af86478332d1c18146
No we have our basic application which is able to create and update invoices including invoice positions (non-ajax). The application has still a limition. You can only create a single InvoiceLineItem since there is no way to tell the controller to add another one.
You could enhance the form by rendering it again passing an argument to add another empty InvoiceLineItem. But this is not what we want. We want to add an empty form to create another InvoiceLineItem by performing an ajax call. So what’s the deal about this?
If you have a closer look at new.html.erb form you will notice that the fields_for helper is invoked on the local variable supplied by form_for(@invoice). This is necessary because Rails internally checks the Invoice model while performing fields_for to determine whether invoice_line_items is declared as a nested attribute.
Rails generates special html ids and form names when detecting nested attributes such as:
<input id=”invoice_invoice_line_items_attributes_1_amount” type=”text” value=”100″ size=”30″ name=”invoice[invoice_line_items_attributes][1][amount]”/>
As you can see the name of the input element ends with _attributes and in addition to that is numbered ( …][1][… ).
More than that if you are editing an invoice and having a look at the html source you will see an additional hidden field added by rails helpers:
<input id=”invoice_invoice_line_items_attributes_1_id” type=”hidden” value=”2″ name=”invoice[invoice_line_items_attributes][1][id]”/>
Now that we know how the form should look like we can have a look at problems when trying to generate it within an ajax call.
Problem when creating the AJAX Call
The main problem when creating the ajax call is the missing form_for-context for our Invoice object.
At this point my expectation was the following. It should somehow be possible to use the field_for helper without creating a form context. We don’t want a form so why should I generate one? Ok, we need to provide the invoice object to the fields_for helper. This is necessary so that the helper can create all those nice html ids and form item names. But in an ajax call to enhance the existing form I definitely don’t need another form tag.
However, I am not an expert for rails internals so I had a look at the helper implementation to get a better understanding of the rails helpers. I found the implementation in:
lib/action_view/helpers/form_helper.rb
As you might know there are two ways to invoke a form helper; by calling a helper method such as fields_for withouth or by calling form.fields_for with a form context.
The implementation in form_helper.rb reflects this by having one implemention within the ActionView::Helper module and one in the ActionView::Helper::FormBuilder class.
Only the form builder implementation seems to be aware of generating nested attributes form elements.
I think this might be the main reason for the laking ajax support.
If you invoke fields_for like this
<% fields_for :invoice_line_items, @invoice.invoice_line_items.first do |ili_form| %>
<%= render :partial => ‘invoice_line_item’, :locals => {:ili_form => ili_form} %>
<% end %>
then you’ll receive a form but it’s not meant to be used within a nested form:
<input id=”invoice_line_items_amount” type=”text” size=”30″ name=”invoice_line_items[amount]”/>
As you can see form elements do not end with _attributes.
Workaround to get a form_for context
As an ugly workaround you could do something like this:
<% ActionView::Helpers::FormBuilder.new(:invoice, @invoice, @template, {}, proc {} ).fields_for :invoice_line_items, {:child_index => @next_child_index} do |ili_form| %>
<%= render :partial => ‘invoice_line_item’, :locals => {:ili_form => ili_form} %>
<% end %>
The trick here is to create a FormBuilder object and passing the Invoice object to it but throwing away all output from the form builder. We just use it to create the ili_form object which is another form builder instance. But this form builder has the needed context to generate correct elements for our nested form.
So how did we create the silent invoice form builder? Normally we would use the form_for method to create it. In this case we used the constructor method new. Since output within the form builder block would mess up our response we pass an empty proc instead of a block.
Again, I don’t like this very much and I would be glad if someone knew a better way to do this.
With this workaround we are able to add empty forms to add new invoice line items per ajax call. But since we have created an empty record on a new Invoice object the generated html ID and form names are not indexed properly. Index is always 0.
A closer look at the form_helper.rb source code shows that there is an option for fields_for called child_index.
Using this option we can generate an empty record with a certain index. But wait, how can we determine the correct index? The client could have added dozens of empty records on the client side.
I think the best way would be to have a look at existing records and determine the next index on the client side. If my last record has index 0 (can be taken from html ids or form names) the next index is 0+1 = 1.
We implement two javascript methods like this:
function max_html_seq_id(object_name, collection_name, attribute_name, field_to_count) {
if ( field_to_count === undefined ) {
field_to_count = ‘input[type=text]’
}
search_string = field_to_count + ‘[id^=’ + object_name + ‘_’ + collection_name + ‘_attributes]’;
bp_fields = $$(search_string);
max_seq_id = -1;
// ==> <input id=”invoice_invoice_line_items_attributes_0_amount” type=”hidden” value=”1″ name=”invoice[invoice_line_items_attributes][0][id]”>
bp_fields.each(function(bp_field) {
// ==> “invoice_invoice_line_items_attributes_0_amount”
html_id = bp_field.id;
// ==> ‘^invoice_invoice_line_items_attributes_(\d+)_amount’
reg_exp_str = ‘^’ + object_name + ‘_’ + collection_name + ‘_attributes_(\\d+)_’ + attribute_name;
// ==> /^invoice_invoice_line_items_attributes_(\d+)_amount/
reg_exp = new RegExp(reg_exp_str);
// ==> [”invoice_invoice_line_items_attributes_0_amount”, “0″]
match_ret = reg_exp.exec(html_id);
// ==> “0″
seq_id_str = match_ret[1];
// ==> 0
seq_id = parseInt(seq_id_str);
// Maximumsuche
if (seq_id > max_seq_id) {
max_seq_id = seq_id;
}
});
return max_seq_id;
}
function next_html_seq_id(object_name, collection_name, attribute_name) {
return (max_html_seq_id(object_name, collection_name, attribute_name) + 1);
}
Basically we perform a maximum search on indexes taken from text fields.
So we need to generate a button performing an ajax call which submits the calculated index as a parameter. This could look like this:
In your javascript file:
function params_to_new_invoice_line_item() {
return ‘next_child_index=’ + next_html_seq_id(’invoice’, ‘invoice_line_items’, ‘amount’);
}
In your view:
<%= button_to_remote “New line item”,
:url => {:action => ‘ajax_new_line_item’},
:with => ‘params_to_new_invoice_line_item()’ %>
Since there is no need to modify the controller this should be enough to create and edit new line items.
Summary
The main question is whether there is a more elegant way to create a new invoice line item field set with a correct index prepared for the use in a nested form.
Maybe a modified FormBuilder or FormHelper?
Download the example
You can find the final app on github:
http://github.com/enterprise-rails/invoice_nested_attributes_ajax/tree/master
Git clone url:
git://github.com/enterprise-rails/invoice_nested_attributes_ajax.git