How to send multiple new items using Rails 3.2 bulk

I have a pretty standard use case. I have a parent and a list of child objects. I want to have a tabular form where I can edit all the children at once, like rows in a table. I also want to be able to insert one or more new lines, and in submit, create them as new entries.

When I use fields_for to render a series of subforms for nested entries related to has-many, rails generates field names, for example. parent[children_attributes][0][fieldname] , parent[children_attributes][1][fieldname] , etc.

This causes Rack to parse the params hash, which looks like this:

 { "parent" => { "children" => { "0" => { ... }, "1" => { ... } } } 

When transferring a new (uninstalled) object, the same fields_for will generate a field name that looks like this:

 parent[children_attributes][][fieldname] 

Note [] without an index in it.

It cannot be sent in the same form with fields containing [0] , [1] etc., because the Rack gets tangled and raises

 TypeError: expected Array (got Rack::Utils::KeySpaceConstrainedParams) 

OK, I think. β€œI’ll just make all the fields use the form [] instead of the form [index] . But I can’t figure out how to convince fields_for to do this sequentially. Even if I give it an explicit field name prefix and an object:

 fields_for 'parent[children_attributes][]', child do |f| ... 

While the child saved, it automatically changes the field names to become, for example, parent[children_attributes][0][fieldname] , leaving the field names for new entries as parent[children_attributes][][fieldname] . Once again, Rack barfs.

I'm at a loss. How do I use standard Rails helpers, such as fields_for to send several new records along with existing records, so that they are parsed as an array in the parameters, and so that all records without identifiers are created as new records in the database? Am I lucky and just need to generate all the field names manually?

+56
ruby-on-rails ruby-on-rails-3
Jul 12 2018-12-12T00:
source share
8 answers

As others have noted, [] must contain the key for new entries, because otherwise it mixes the hash with the array type. You can set this using the child_index option in the_for fields.

 f.fields_for :items, Item.new, child_index: "NEW_ITEM" # ... 

I usually do this with object_id to make sure it is unique in case there are several new elements.

 item = Item.new f.fields_for :items, item, child_index: item.object_id # ... 

An abstract helper method is used here. This assumes that there is a partial name item_fields that it will display.

 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 that. The arguments are: link name, parent form template, and association name in the parent model.

 <%= link_to_add_fields "Add Item", f, :items %> 

And here are a few CoffeeScript for listening to the click event on this link, inserting fields and updating the identifier of the object 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() 

This code is taken from this episode of RailsCasts Pro , which requires a paid subscription. However, there is a complete working example, free on GitHub .

Update: I want to indicate that adding a child_index placeholder child_index not always necessary. If you do not want to use JavaScript to dynamically insert new records, you can create them in advance:

 def new @project = Project.new 3.times { @project.items.build } end <%= f.fields_for :items do |builder| %> 

Rails will automatically add an index for new entries so that it just works.

+45
Jul 12 2018-12-12T00:
source share

Thus, I was unhappy with the decision that I saw most often that it was supposed to generate a pseudo-index for new elements either on the server or on the client side of JS. This is similar to kludge, especially in light of the fact that Rails / Rack is very good at parsing lists of items, as long as they all use empty brackets ( [] ) as indexes. Here's the approximation of the code I came across:

 # 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 

Completing the field name prefix with [] , combined with the index: nil option, disables the generation of the Rails index, so it is useful to try to ensure that objects are saved. This snippet works for both new and saved objects. The resulting form parameters, as they consistently use [] , are parsed into an array in params :

 params[:parent][:children_attributes] # => [{"name" => "..."}, {...}] 

The Parent#children_attributes= method generated by accepts_nested_attributes_for :children handles this array perfectly, updates the changed records, adds new ones (those that do not have the "id" key), and deletes them using the "_destroy" set.

I'm still worried that Rails makes this so complicated, and I had to go back to the string string with hard code, instead of using for example. f.fields_for :children, index: nil . For the record, even by doing the following:

 f.fields_for :children, index: nil, child_index: nil do |f| ... 

... cannot disable field index generation.

I am considering writing a Rails patch to make this easier, but I don’t know if a lot of people care about it, or even if it is accepted.

EDIT: User @Macario made me understand why Rails prefers explicit indexes in field names: once you get into three layers of nested models, there should be a way to recognize which second-level model has a third-level attribute.

+23
Jul 13 2018-12-12T00:
source share

A common solution is to add a placeholder in [] and replace it with a unique number when inserting a fragment into the form. Timestamps work most of the time.

+14
Jul 12 '12 at 7:20
source share

Maybe you should just cheat. Put the new entries in another faux attribute, which is the decorator for the actual one.

 parent[children_attributes][0][fieldname] parent[new_children_attributes][][fieldname] 

This is not very, but it should work. This may require additional efforts to support rounding in the form for validation errors.

+3
Jul 12 '12 at 6:12
source share

long post deleted

Ryan has an episode about this: http://railscasts.com/episodes/196-nested-model-form-revised

It looks like you need to create a unique index manually. Ryan uses object_id for this.

+3
Jul 12 2018-12-12T00:
source share

I came across this user case in all my recent projects, and I expect this to continue, as julian7 pointed out, it is necessary to specify a unique identifier inside []. In my opinion, this is best done through js. I dragged and improved the jquery plugin to deal with these situations. It works with existing records and to add new records, but expects some markup and degrades competently, the code and example look:

https://gist.github.com/3096634

Caveats for using the plugin:

  • The field_for fields must be wrapped in a <fieldset> with a data association attribute equal to the pluralized model name and the class 'nested_models'.

  • an object must be created in the view immediately before calling fields_for.

  • perse object fields must be wrapped in <fieldset> with class "new", but only if the record is new (do not remember if I deleted this requirement).

  • A flag for the attribute '_destroy' inside the label must exist, the plugin will use the label text to create the destroy link.

  • A link with the class "add_record" must exist within fieldset.nested_models, but outside the field that spans the model fields.

The applause from these troubles works wonders for me.
After verifying the essence of these requirements should be more clear. Please let me know if you improve the code or use it :).
BTW, I was inspired by the first nested Ryan Bates models.

+3
Jul 12 '12 at 8:23
source share

I think you can make it work by including the record id in the hidden field

+1
Jul 12 '12 at 7:50
source share

To do this, there is a gem called a cocoon, I would go for a softer approach, but it was specially built for these cases.

+1
Jul 13 '12 at 18:01
source share



All Articles