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>
when I click the "Remove Student" it removes every field above that link
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 the fields_for
references.
Each time you use fields_for
(which is what you're replicating with the above javascript functionality), it assigns an id
to each set of fields it creates. These ids
are used in the params
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 your link_to_add_fields
helper does not update the JS (IE allows you to append fields with exactly the same child_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:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :teachers
accepts_nested_attributes_for :teachers #-> this is to PASS data, not receive
end
#app/models/teacher.rb
class Teacher < ActiveRecord::Base
belongs_to :student
end
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):
Nested attributes allow you to save attributes on associated records
through the parent
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def new
@student = Student.new
@student.teachers.build #-> you have to build the associative object
end
def create
@student = Student.new student_params
@student.save
end
private
def student_params
params.require(:student).permit(:x, :y, teachers_attributes: [:z])
end
end
With these objects built, you're able to use them in your form:
#app/views/students/new.html.erb
<%= form_for @student do |f| %>
<%= f.fields_for :teachers |teacher| %>
<% # this will replicate for as many times as you've "built" a new teacher object %>
<%= teacher.text_field ... %>
<% end %>
<%= f.submit %>
<% end %>
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 like teachers_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...
#app/models/Student.rb
class Student < ActiveRecord::Base
def self.build #-> non essential; only used to free up controller code
student = self.new
student.teachers.build
student
end
end
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def new
@student = Student.build
end
def add_teacher
@student = Student.build
render "add_teacher", layout: false
end
def create
@student = Student.new student_params
@student.save
end
private
def student_params
params.require(:student).permit(:x, :y, teachers_attributes: [:z])
end
end
Now for the views (note you have to split your form into partials):
#app/views/students/new.html.erb
<%= form_for @student do |f| %>
<%= f.text_field :name %>
<%= render "teacher_fields", locals: {f: f} %>
<%= link_to "Add", "#", id: :add_teacher %>
<%= f.submit %>
<% end %>
#app/views/_teacher_fields.html.erb
<%= f.fields_for :teachers, child_index: Time.now.to_i do |teacher| %>
<%= teacher.text_field ....... %>
<%= link_to "Remove", "#", id: :remove_teacher, data: {i: child_index} %>
<% end %>
#app/views/add_teacher.html.erb
<%= form_for @student, authenticity_token: false do |f| %>
<%= render partial "teacher_fields", locals: {f:f}
<% end %>
This should render the various forms etc for you, including the fields_for
. Notice the child_index: Time.now.to_i
-- this sets a unique ID for each fields_for
, allowing us to differentiate between each field as you need.
Making this dynamic then comes down to JS:
#config/routes.rb
resources :students do
get :add_teacher, on: :collection #-> url.com/students/get_teacher
end
Using this route allows us to send an Ajax request (to get a new field):
#app/assets/javascripts/.....coffee
$ ->
#Add Teacher
$(document).on "click", "#add_teacher", (e) ->
e.preventDefault();
#Ajax
$.ajax
url: '/students/add_teacher'
success: (data) ->
el_to_add = $(data).html()
$('#subscribers').append(el_to_add)
error: (data) ->
alert "Sorry, There Was An Error!"
#Remove Teacher
$(document).on "click", "#remove_teacher", (e) ->
e.preventDefault();
id = $(this).data("i")
$("input#" + i).remove()
add this in your js.coffe file
$(document).on 'click', 'form .remove_', (event) ->
$(this).prev('input[type=hidden]').val('1')
$(this).closest('fieldset').hide()
event.preventDefault()
$(document).on 'click', 'form .add_teacher', (event) ->
event.preventDefault()
time = new Date().getTime()
regexp = new RegExp($(this).data('id'), 'g')
$(this).before($(this).data('fields').replace(regexp, time))