Rails 4: Adding child_index to dynamically added (

2020-06-16 05:36发布

问题:

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>

回答1:

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()


回答2:

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))