I have a pretty standard use-case. I have a parent object and a list of child objects. I want to have a tabular form where I can edit all the children at once, as rows in th
There is a gem called cocoon for doing this, I would go for a leaner mor DIY aproach but it was specifically built for this cases.
So, I was not happy with the solution I saw most often, which was to generate a pseudo-index for new elements, either on the server or in client-side JS. This feels like a kludge, especially in light of the fact that Rails/Rack is perfectly capable of parsing lists of items so long as they all use empty brackets ([]
) as the index. Here's an approximation of the code I wound up with:
# note that this is NOT f.fields_for.
fields_for 'parent[children_attributes][]', child, index: nil do |f|
f.label :name
f.text_field :name
# ...
end
Ending the field name prefix with []
, coupled with the index: nil
option, disables the index generation Rails so helpfully tries to provide for persisted objects. This snippet works for both new and saved objects. The resulting form parameters, since they consistently use []
, are parsed into an array in the params
:
params[:parent][:children_attributes] # => [{"name" => "..."}, {...}]
The Parent#children_attributes=
method generated by accepts_nested_attributes_for :children
deals with this array just fine, updating changed records, adding new ones (ones lacking an "id"
key), and removing the ones with the "_destroy"
key set.
I'm still bothered that Rails makes this so difficult, and that I had to revert to a hardcoded field name prefix string instead of using e.g. f.fields_for :children, index: nil
. For the record, even doing the following:
f.fields_for :children, index: nil, child_index: nil do |f| ...
...fails to disable field index generation.
I'm considering writing a Rails patch to make this easier, but I don't know if enough people care or if it would even be accepted.
EDIT: User @Macario has clued me in to why Rails prefers explicit indices in field names: once you get into three layers of nested models, there needs to be a way to discriminate which second-level model a third-level attribute belongs to.
I've came across this user case in all my last proyects, and I expect this to continue, as julian7 pointed, it is necesary to provide a unique id inside the []. In my opinion this is better done via js. I've been dragging and improving a jquery plugin for dealing with this situations. It works with existing records and for adding new records but expects a certain markup and it degrades gracefully, heres the code and an example:
https://gist.github.com/3096634
Caveats for using the plugin:
The fields_for call should be wrapped in a <fieldset>
with data-association attribute equal to the pluralized name of the model, and a class 'nested_models'.
an object should be built in the view just before calling fields_for.
the object fields perse should be wrapped in a <fieldset>
with class "new" but only if the record is new (cant remember if I removed this requirement).
A checkbox for the '_destroy' attribute inside a label must exist, the plugin will use the label text to create a destroy link.
A link with class 'add_record' should exist within the fieldset.nested_models but outside the fieldset enclosing the model fields.
Appart from this nuisances its been working wonders for me.
After checking the gist this requirements must be clearer.
Please let me know if you improve on the code or if you use it :).
BTW, I was inspired by Ryan Bates first nested models screencast.
As others have mentioned, the []
should contain a key for new records because otherwise it is mixing a hash with an array type. You can set this with the child_index
option on fields_for.
f.fields_for :items, Item.new, child_index: "NEW_ITEM" # ...
I usually do this using the object_id
instead to ensure it is unique in case there are multiple new items.
item = Item.new
f.fields_for :items, item, child_index: item.object_id # ...
Here's an abstract helper method that does this. This assumes there is a partial with the name of item_fields
which it will render.
def link_to_add_fields(name, f, association)
new_object = f.object.send(association).klass.new
id = new_object.object_id
fields = f.fields_for(association, new_object, child_index: id) do |builder|
render(association.to_s.singularize + "_fields", f: builder)
end
link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end
You can use it like this. The arguments are: the name of the link, the parent's form builder, and the name of the association on the parent model.
<%= link_to_add_fields "Add Item", f, :items %>
And here is some CoffeeScript to listen to the click event of that link, insert the fields, and update the object id with the current time to give it a unique key.
jQuery ->
$('form').on 'click', '.add_fields', (event) ->
time = new Date().getTime()
regexp = new RegExp($(this).data('id'), 'g')
$(this).before($(this).data('fields').replace(regexp, time))
event.preventDefault()
That code is taken from this RailsCasts Pro episode which requires a paid subscription. However, there is a full working example freely available on GitHub.
Update: I want to point out that inserting a child_index
placeholder is not always necessary. If you do not want to use JavaScript to insert new records dynamically, you can build them up ahead of time:
def new
@project = Project.new
3.times { @project.items.build }
end
<%= f.fields_for :items do |builder| %>
Rails will automatically insert an index for the new records so it should just work.
I think you can make it work by including the id of the record as a hidden field
The common solution is to add a placeholder into [], and replace it with a unique number on inserting the snippet to the form. Timestamp works most of the time.