【问题标题】:Rails 4: Adding child_index to dynamically added (nested) form fields with Cocoon GemRails 4:使用 Cocoon Gem 将 child_index 添加到动态添加(嵌套)的表单字段
【发布时间】:2015-10-07 05:33:28
【问题描述】:

更新:我正在尝试将表单字段添加/删除到涉及多个模型的嵌套表单。我看过 Ryan Bates 的“Dynamic Forms”railscast,我使用Cocoon Gem 提到了this article。在那篇文章之后,除了 child_index 之外,一切都完美无缺。 child_index 仅出现在第一个 :kid 输入字段 (:name) 和第一个 :pet 输入字段(:name:age)上。然后它会返回到正在添加的字段的真实性令牌。

我已经删除了所有的 JS 和辅助方法,而是使用了一些内置在 JS 中的 Cocoon 方法。

我通过从application.html.erb 文件中删除= javascript_include_tag :cocoon 解决了单击“添加”会添加两个字段而不是一个字段的问题。

我已尝试添加 jQuery 和表单助手,但我不确定我是否正确输入了代码。

(我已更改模型对象以使关系更清晰)

parent.rb 文件:

class Parent < ActiveRecord::Base

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

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 文件:

 class Pet < ActiveRecord::Base

 belongs_to :kid

 validates :name, presence: true

 validates :age, presence: true

这是我的 _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 %>

这是我的 _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>

这是我的 _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>

【问题讨论】:

    标签: jquery ruby-on-rails forms nested-forms


    【解决方案1】:

    当我点击“删除学生”时,它会删除该链接上方的每个字段

    这是您关注的特定 RailsCast 的一个众所周知的问题(它已过时)。还有一个here

    问题归结为fields_for referenceschild_index

    每次您使用fields_for(这是您使用上述javascript 功能复制的内容)时,它都会为它创建的每组字段分配一个id。这些idsparams中用来分隔不同的属性;它们还以HTML "id" property 的形式分配给每个字段。

    因此,您遇到的问题是,由于您没有在每次添加新字段时都更新此 child_index,所以它们都是相同的。而且由于您的 link_to_add_fields 助手不会更新 JS(IE 允许您附加具有完全相同的 child_index 的字段),这意味着每当您“删除”一个字段时,它都会选择所有字段。


    解决这个问题的方法是设置child_index(我会在下面给你解释)。

    老实说,我宁愿给你新的代码,也不愿挑出你过时的东西。

    我在这里写过这个(虽然它可以稍微完善一下): Rails accepts_nested_attributes_for with f.fields_for and AJAX

    有一些宝石可以为您做到这一点 - 一个名为 Cocoon 的宝石非常受欢迎,尽管不是许多人认为的“即插即用”解决方案。

    尽管如此,最好知道这一切都有效,即使您选择使用类似 Cocoon...


    fields_for

    要了解解决方案,您必须记住 Rails 创建 HTML 表单。

    您可能知道这一点;许多人没有。

    这很重要,因为当您意识到 HTML 表单必须遵守 HTML 施加的所有约束时,您就会明白 Rails 并不是魔术师。人们似乎在思考。

    创建“嵌套”表单(不带添加/删除)功能的方法如下:

    #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
    

    需要注意的重要一点是,您的 accepts_nested_attributes_for 应该在 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
    

    通过构建这些对象,您可以在表单中使用它们:

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

    这是一个标准表单,它将数据发送到您的控制器,然后发送到您的模型。模型中的accepts_nested_attributes_for 方法会将嵌套属性传递给依赖模型。

    --

    最好的办法是记下上面代码创建的嵌套字段的id。我手头没有任何例子。它应该向您显示嵌套字段的名称如 teachers_attributes[0][name] 等。

    要注意的重要一点是[0] - 这是child_index,它在您需要的功能中起着至关重要的作用。


    动态

    现在是动态表单。

    第一部分比较简单……删除一个字段就是从DOM中删除它的情况。我们可以使用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
    

    现在查看视图(注意您必须将表单拆分为部分):

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

    应该为您呈现各种形式等,包括fields_for。请注意child_index: Time.now.to_i——它为每个fields_for 设置了一个唯一的ID,让我们可以根据需要区分每个字段。

    使这个动态然后归结为JS:

    #config/routes.rb
    resources :students do 
       get :add_teacher, on: :collection #-> url.com/students/get_teacher
    end
    

    使用此路由允许我们发送 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()
    

    【讨论】:

    • 我会为 Class 做与为 Teacher 做同样的事情吗?虽然我确实需要在某处涉及 child_index,但我有一个版本有点工作,因为我认为它只是分配一个真实性令牌而不是一个 id。
    • 所以我使用Cocoon Gem 关注了this example,一切似乎都运行良好,除非我单击“添加”链接时它会添加两个字段,而不仅仅是一个。我认为这可能与我的控制器有关,但我不确定。我也没有使用任何 JS 或 Helper 方法。我主要担心的是这些字段没有给我 child_index。相反,它仍然给了我我认为是真实性令牌(一堆随机数)的东西。 @richpeck
    • 这可能是 turbolinks 问题。尝试刷新页面并再次执行 - 如果它继续发送两个请求,我们将不得不找出您的 JS 绑定
    • 我应该卸载 turbolinks gem 吗?我觉得我只需要编写一些 JS 来为要添加的每个字段分配一个 child_index。
    • 现在无法编辑我的评论,但是 - 例如,我刚刚在本地构建了这个,如果我在一秒钟内点击 #add_teacher 3x,我会得到 3 个具有相同索引的新行。对于任何用户来说,这都是一个现实的用例。在示例情况下,如果有人将 3 个Teacher 关联添加到他们的Student,然后单击添加 3x 怎么办?我想解决方案也是在时间上添加一些随机数。
    【解决方案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))
    

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-09
    • 1970-01-01
    • 2016-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多