UPDATED: I am trying to add/remove form fields to a nested form involving multiple models. I have seen the "Dynamic Forms" railscast by Ryan Bates and I have referred to this article using the Cocoon Gem. Following that article has made everything work perfectly except for the child_index. The child_index is present only on the first :kid
input field (:name
) and the first :pet
input fields (:name
and :age
). Then it goes back to an authenticity token for the fields being added.
I've removed all of the JS and helper methods and instead I'm using some of the Cocoon methods that has built in JS.
I fixed the problem where clicking "Add" would add two fields instead of one by removing the = javascript_include_tag :cocoon
from the application.html.erb
file.
I have tried adding jQuery and form helpers but I'm not sure I entered the code correctly.
(I have changed the model objects to make the relationships more clear)
parent.rb file:
class Parent < ActiveRecord::Base
has_many :kids
has_many :pets, through: :kids # <<<<<< ADDED KIDS USING 'through:'
kid.rb file:
class Kid < ActiveRecord::Base
belongs_to :parent
has_many :pets
accepts_nested_attributes_for :pets, reject_if: :all_blank, allow_destroy: true
validates :name, presence: true
pet.rb file:
class Pet < ActiveRecord::Base
belongs_to :kid
validates :name, presence: true
validates :age, presence: true
This is my _form.html.erb file:
<%= form_for @parent do |f| %>
<% if @parent.errors.any? %>
<div class="alert alert-danger">
<h3><%= pluralize(@student.errors.count, 'Error') %>: </h3>
<ul>
<% @student.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="inline">
<div>
<%= f.fields_for :kids do |kid| %>
<%= render 'kid_fields', f: kid %>
<% end %>
<div>
<%= link_to_add_association "Add Kid", f, :kids, id: 'add_kid',
'data-association-insertion-method' => 'before',
'data-association-insertion-traversal' => 'closest' %>
</div>
<% end %>
</div>
</div>
<div class="form-actions">
<%= f.submit 'Create Parent', class: 'btn btn-primary' %>
</div>
<% end %>
This is my _kid_fields.rb file:
<div class="nested-fields">
<div class="kid-fields inline">
<%= f.hidden_field :_destroy, class: 'removable' %>
<%= f.text_field :name, class: 'form-control', placeholder: 'Kid's Name', id: 'kid-input' %>
<div>
<%= link_to_remove_association 'Remove Kid', f %>
</div>
<%= f.fields_for :pets do |pet| %>
<%= render 'pet_fields', f: pet %>
<% end %>
</div>
<div>
<%= link_to_add_association "Add Pet", f, :pets, id: 'add_pet',
'data-association-insertion-method' => 'before' %>
</div>
</div>
This is my _pet_fields.rb file:
<div class="nested-fields">
<div class="pet-fields">
<%= f.hidden_field :_destroy, class: 'removable' %>
<%= f.text_field :name, placeholder: 'Pet Name', id: 'pet-name-input' %>
<%= f.text_field :age, placeholder: 'Pet Age', id: 'pet-age-input' %>
<%= link_to_remove_association 'Remove Pet', f, id: 'remove_pet' %>
</div>
</div>
This is a well known issue with the particular RailsCast you're following (it's outdated). There's another here:
The problem comes down to the
child_index
of thefields_for
references.Each time you use
fields_for
(which is what you're replicating with the above javascript functionality), it assigns anid
to each set of fields it creates. Theseids
are used in theparams
to separate the different attributes; they're also assigned to each field as an HTML "id" property.Thus, the problem you have is that since you're not updating this
child_index
each time you add a new field, they're all the same. And since yourlink_to_add_fields
helper does not update the JS (IE allows you to append fields with exactly the samechild_index
), this means that whenever you "remove" a field, it will select all of them.The fix for this is to set the
child_index
(I'll give you an explanation below).I'd prefer to give you new code than to pick through your outdated stuff to be honest.
I wrote about this here (although it could be polished a little): Rails accepts_nested_attributes_for with f.fields_for and AJAX
There are gems which do this for you - one called Cocoon is very popular, although not a "plug and play" solution many think it is.
Nonetheless, it's best to know it all works, even if you do opt to use something like
Cocoon
...fields_for
To understand the solution, you must remember that Rails creates HTML forms.
You know this probably; many don't.
It's important because when you realize that HTML forms have to adhere to all the constraints imposed by HTML, you'll understand that Rails is not the magician a lot of folks seem to think.
The way to create a "nested" form (without add/remove) functionality is as follows:
Something important to note is that your
accepts_nested_attributes_for
should be on the parent model. That is, the model you're passing data to (not the one receiving data):With these objects built, you're able to use them in your form:
This is a standard form which will send the data to your controller, and then to your model. The
accepts_nested_attributes_for
method in the model will pass the nested attributes to the dependent model.--
The best thing to do with this is to take note of the
id
for the nested fields the above code creates. I don't have any examples on hand; it should show you the nested fields have names liketeachers_attributes[0][name]
etc.The important thing to note is the
[0]
- this is the child_index which plays a crucial role in the functionality you need.Dynamic
Now for the dynamic form.
The first part is relatively simple... removing a field is a case of deleting it from the DOM. We can use the
child_index
for that, so we first need to know how to set the child index etc etc etc...Now for the views (note you have to split your form into partials):
This should render the various forms etc for you, including the
fields_for
. Notice thechild_index: Time.now.to_i
-- this sets a unique ID for eachfields_for
, allowing us to differentiate between each field as you need.Making this dynamic then comes down to JS:
Using this route allows us to send an Ajax request (to get a new field):