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

前端 未结 2 615
隐瞒了意图╮
隐瞒了意图╮ 2021-02-15 11:07

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

相关标签:
2条回答
  • 2021-02-15 11:36
    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))
    
    0 讨论(0)
  • 2021-02-15 11:47

    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()
    
    0 讨论(0)
提交回复
热议问题