简体   繁体   English

Rails 4:使用Cocoon Gem将child_index添加到动态添加(嵌套)表单字段

[英]Rails 4: Adding child_index to dynamically added (nested) form fields with Cocoon Gem

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 . 我已经看过Ryan Bates的“动态表格”轨道广播,我已经使用Cocoon Gem参考了这篇文章 Following that article has made everything work perfectly except for the child_index. 在该文章之后,除了child_index之外,一切都完美无缺。 The child_index is present only on the first :kid input field ( :name ) and the first :pet input fields ( :name and :age ). child_index仅出现在第一个:kid输入字段( :name )和第一个:pet输入字段( :name: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. 我已经删除了所有的JS和帮助器方法,而是使用了一些内置JS的Cocoon方法。

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. 我修复了问题,单击“添加”将通过从application.html.erb文件中删除= javascript_include_tag :cocoon来添加两个字段而不是一个。

I have tried adding jQuery and form helpers but I'm not sure I entered the code correctly. 我试过添加jQuery和表单助手,但我不确定我是否正确输入了代码。

(I have changed the model objects to make the relationships more clear) (我更改了模型对象以使关系更清晰)

parent.rb file: parent.rb文件:

class Parent < ActiveRecord::Base

has_many :kids
has_many :pets, through: :kids # <<<<<< ADDED KIDS USING 'through:'

kid.rb file: kid.rb文件:

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: pet.rb文件:

 class Pet < ActiveRecord::Base

 belongs_to :kid

 validates :name, presence: true

 validates :age, presence: true

This is my _form.html.erb file: 这是我的_form.html.erb文件:

 <%= 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: 这是我的_kid_fields.rb文件:

    <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: 这是我的_pet_fields.rb文件:

    <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). 这是您所关注的特定RailsCast的一个众所周知的问题(它已过时)。 There's another here : 还有另一种在这里

在此输入图像描述

The problem comes down to the child_index of the fields_for references . 这个问题归结为child_index中的fields_for引用

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. 每次使用fields_for (这是您使用上述javascript功能复制的内容)时,它会为其创建的每组字段分配一个id These ids are used in the params to separate the different attributes; 这些idsparams用于分隔不同的属性; they're also assigned to each field as an HTML "id" property . 它们也被分配给每个字段作为HTML“id”属性

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. 因此,您遇到的问题是,由于每次添加新字段时都没有更新此child_index ,因此它们都是相同的。 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. 由于你的link_to_add_fields助手没有更新JS(IE允许你追加具有完全相同的child_index字段),这意味着每当你“删除”一个字段时,它将选择所有字段。


The fix for this is to set the child_index (I'll give you an explanation below). 解决这个问题的方法是设置child_index (我将在下面给你一个解释)。

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 我在这里写了这篇文章(虽然它可以稍微抛出一点): Rails的accept_nested_attributes_for与f.fields_for和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. 有宝石为你做这个 - 一个叫做Cocoon的非常受欢迎,虽然不是许多人认为的“即插即用”解决方案。

Nonetheless, it's best to know it all works, even if you do opt to use something like Cocoon ... 尽管如此,最好知道这一切都有效,即使你选择使用像Cocoon这样的东西......


fields_for fields_for

To understand the solution, you must remember that Rails creates HTML forms. 要理解解决方案,您必须记住Rails创建HTML表单。

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. 这很重要,因为当你意识到HTML表单必须遵守HTML强加的所有约束时,你就会明白Rails并不是很多人想象的魔术师。

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. 需要注意的一点是,您的accepts_nested_attributes_for应该在模型上。 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. 模型中的accepts_nested_attributes_for方法将嵌套属性传递给依赖模型。

-- -

The best thing to do with this is to take note of the id for the nested fields the above code creates. 最好的办法是记下上面代码创建的嵌套字段的id I don't have any examples on hand; 我手边没有任何例子; it should show you the nested fields have names like teachers_attributes[0][name] etc. 它应该显示嵌套字段的名称,如teachers_attributes[0][name]等。

The important thing to note is the [0] - this is the child_index which plays a crucial role in the functionality you need. 需要注意的重要事项是[0] - 这是child_index ,它在您需要的功能中起着至关重要的作用。


Dynamic 动态

Now for the dynamic form. 现在为动态表格。

The first part is relatively simple... removing a field is a case of deleting it from the DOM. 第一部分相对简单... 删除字段是从DOM中删除它的一种情况。 We can use the child_index for that, so we first need to know how to set the child index etc etc etc... 我们可以使用child_index ,所以我们首先需要知道如何设置子索引等等...

#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 . 应该为您呈现各种形式等,包括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. 注意child_index: Time.now.to_i - 这为每个fields_for设置了一个唯一的ID,允许我们根据需要区分每个字段。

Making this dynamic then comes down to JS: 让这个动态变成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): 使用此路由允许我们发送Ajax请求(以获取新字段):

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM