ruby-on-railsnested-forms

Rails 2 level nested forms


I slightly modified and extended the example for building nested forms to implement the second level of nesting. Everything is displayed perfectly on the form. Data for the person is displayed at both nesting levels correctly. The corresponding JS scripts work to add and remove nested forms. All 3 are generated using scaffold. Form

But when I click on update, only the main form and the first nesting level (addresses) are updated. The second nesting level (nested addresses) is not updated. Although I also get parameters from the second nesting level in the controller ("name"=>"UPDATE NAME OF NESTED ADDRESS").

{
"_method"=>"patch", 
"authenticity_token"=>"VZ09CR-aO2D4Wv3AwEa5PHXo-mA_--c6QPUN6f0Gb_9SJJSL2gIwzCl4G4SbzRy2t3wxJHytBWiPwysNJMrWgg", 
"person"=>{
  "first_name"=>"Liz", 
  "last_name"=>"Smith", 
  "addresses_attributes"=>{
    "0"=>{
      "_destroy"=>"false", 
      "kind"=>"Some kind", 
      "street"=>"Some street", 
      "nested_addresses_attributes"=>{
        "0"=>{
          "_destroy"=>"false", 
          "name"=>"UPDATE NAME OF NESTED ADDRESS", 
          "id"=>"1"
        }
      }, 
  "id"=>"10"}}}, 
"commit"=>"Update Person", 
"controller"=>"people",
"action"=>"update", 
"id"=>"3"}

I understand that even the first nesting level is magically handled behind the scenes, but I don't understand how? And how to handle the second level as well? In general, the Create Update, Delete methods do not work for the second nesting level.

Models


class Person < ApplicationRecord
  has_many :addresses, inverse_of: :person, :dependent => :destroy
  has_many :nested_addresses, through: :addresses, inverse_of: :person, :dependent => :destroy
  accepts_nested_attributes_for :addresses, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :nested_addresses, allow_destroy: true, reject_if: :all_blank
  validates :first_name, presence: true
  validates :last_name, presence: true
end
class NestedAddress < ApplicationRecord
  belongs_to :address
  validates :name, presence: true
end
class Address < ApplicationRecord
  belongs_to :person, optional: true
  has_many :nested_addresses, inverse_of: :address, :dependent => :destroy
  accepts_nested_attributes_for :nested_addresses, allow_destroy: true, reject_if: :all_blank
  validates :kind, presence: true
  validates :street, presence: true
end

Controllers

def person_params
  params.require(:person).permit(:first_name, :last_name, addresses_attributes: [:id, :kind, :street, :_destroy], nested_addresses_attributes: [:id, :name, :_destroy])
end
def address_params
  params.require(:address).permit(:kind, :street, :person_id, nested_addresses_attributes: [:id, :name, :_destroy])
end
def nested_address_params
  params.require(:nested_address).permit(:name, :address_id)
end

people/_form.html.erb

<%= form_with model: @person, local: true do |f| %>
  <%= render "shared/validation-messages", object: @person %>
  <%= f.label :first_name %>
  <%= f.text_field :first_name, class: 'form-control' %>
  <%= f.label :last_name %>
  <%= f.text_field :last_name, class: 'form-control' %>

  <br>
  <fieldset>
    <legend>Addresses:</legend>
    <%= f.fields_for :addresses do |addresses_form| %>
      <%= render "address_fields", f: addresses_form %>
    <% end %>
    <br>
    <%= link_to_add_fields "Add Addresses", f, :addresses, 'btn btn-outline-secondary'%>
  </fieldset>
  <br>
  <%= f.submit class: 'btn btn-success' %>
  <% if params[:action] === "edit" && params[:controller] === "people" %>
    <%= link_to "Delete Person", person_path(@person), method: :delete, data: { confirm: "Are You Sure?" }, class: 'btn btn-outline-danger' %>
  <% end %>
<% end %>

people/_address_fields.html.erb

<div class="card nested-fields">
  <div class="card-header">
    <div><%= f.object.id %></div>
  </div>
  <div class="card-body">
    <%= f.hidden_field :_destroy %>
    <div>
      <%= f.label :kind %>
      <%= f.text_field :kind, class: 'form-control' %>
    </div>
    <div>
      <%= f.label :street %>
      <%= f.text_field :street, class: 'form-control' %>
    </div>
    <br>
    <fieldset>
      <legend>Nested addresses:</legend>
      <%= f.fields_for :nested_addresses do |nested_addresses_form| %>
        <%= render "nested_address_fields", f: nested_addresses_form %>
      <% end %>
      <br>
      <%= link_to_add_fields "Add Nested Addresses", f, :nested_addresses, 'btn btn-outline-secondary btn-sm' %>
    </fieldset>
    <br>
    <div>
      <%= link_to "Remove address", '#', class: "remove_fields btn btn-outline-danger btn-sm" %>
    </div>
  </div>
</div>

people/_nested_address_fields.html.erb

<div class="card nested-fields">
  <div class="card-body">
    <%= f.hidden_field :_destroy %>
    <div>
      <%= f.label :name %>
      <%= f.text_field :name %>
    </div>
    <br>
    <div>
      <%= link_to "Remove nested address", '#', class: "remove_fields btn btn-outline-danger btn-sm" %>
    </div>
  </div>
  <br>
</div>

helpers/application_helper.rb

def link_to_add_fields(name, f, association, cl)
    new_object = f.object.send(association).klass.new
    id = new_object.object_id
    fields = f.fields_for(association, new_object, child_index: id) do |builder|
      render(association.to_s.singularize + "_fields", f: builder)
    end
    link_to(name, '#', class: 'add_fields ' + cl, data: {id: id, fields: fields.gsub("\n", "")}, role: 'button')
  end

Solution

  • Your strong parameters permit call should reflect the actual nesting structure of parameters:

    def person_params
      params.require(:person).permit(
        :first_name, :last_name,
        addresses_attributes: [
          :id, :kind, :street, :_destroy,
          { nested_addresses_attributes: [:id, :name, :_destroy] } # <= note the hash
        ]
      )
    end