Difficulties with Nested Form Implementation Rails 6.1.3

Hi,

I’m working on a project that will utilize nested forms that incorporates Rails 6.1.3 and Bootstrap 5.1.2.

I’m having difficulty getting the nested form feature to work with the form helpers and controllers per the documentation on Rails Form Helpers.

The setup of the application can be viewed on my public github( https://github.com/cjmccormick88/testapp-nested ).

The setup of the application is as follows:

There are two models: client and shipping address. Client accepts nested attributes for the shipping address model. A client can have many shipping addresses.

Authentication is being handled by Devise. Bootstrap is used for styling.

Clients controller is as follows:

class ClientsController < ApplicationController
  
  before_action :set_client, only: %i[ show edit update destroy ]

  # GET /clients or /clients.json
  def index
    @clients = Client.all
  end

  # GET /clients/1 or /clients/1.json
  def show
  end

  # GET /clients/new
  def new
    @client = Client.new
    @client.shipping_addresses.build
  end

  # GET /clients/1/edit
  def edit
  end

  # POST /clients or /clients.json
  def create
    @client = Client.new(client_params)
    @client.shipping_addresses.build(client_params[:shipping_addresses_attributes])
    @client.save

    respond_to do |format|
      if @client.save
        format.html { redirect_to @client, notice: "Client was successfully created." }
        format.json { render :show, status: :created, location: @client }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @client.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /clients/1 or /clients/1.json
  def update
    respond_to do |format|
      if @client.update(client_params)
        format.html { redirect_to @client, notice: "Client was successfully updated." }
        format.json { render :show, status: :ok, location: @client }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @client.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /clients/1 or /clients/1.json
  def destroy
    @client.destroy
    respond_to do |format|
      format.html { redirect_to clients_url, notice: "Client was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_client
      @client = Client.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def client_params
      params.require(:client).permit(:client_name, shipping_addresses_attributes: [:address_line1, :address_line2, :city, :state, :country])
    end

end

Client Model is as follows:

 class Client < ApplicationRecord
 
   audited
 
   has_many :shipping_addresses, :inverse_of => :client, autosave: true
   accepts_nested_attributes_for  :shipping_addresses
     
 
 end

Shipping Address Model is as follows:

class ShippingAddress < ApplicationRecord
 
   audited
   belongs_to :client
  validates :client, :presence => true

end

The form helper for clients is as follows:


<%= form_with(model: client) do |form| %>
   <% if client.errors.any? %>
     <div id="error_explanation">
       <h2><%= pluralize(client.errors.count, "error") %> prohibited this client from being saved:</h2>
 
       <ul>
         <% client.errors.each do |error| %>
           <li><%= error.full_message %></li>
         <% end %>
       </ul>
     </div>
   <% end %>
 
   <div class="field">
     <%= form.label :client_name %>
     <%= form.text_field :client_name %>
   </div>
 
   <%= form.fields_for @client.shipping_addresses.build do |s| %>
     
     <div class="field">
       <%= s.label :address_line1, 'Address Line 1' %>
       <%= s.text_field :address_line1 %>
     </div>
 
     <div class="field">
       <%= s.label :address_line2, 'Address Line 2' %>
       <%= s.text_field :address_line2 %>
     </div>
 
     <div class="field">
       <%= s.label :city, 'City' %>
       <%= s.text_field :city %>
     </div>
 
     <div class="field">
       <%= s.label :state, 'State' %>
       <%= s.text_field :state %>
     </div>
 
     <div class="field">
       <%= s.label :country, 'Country' %>
       <%= s.text_field :country %>
     </div>
 
   <% end %>
 
   <div class="actions">
     <%= form.submit %>
   </div>
   
 <% end %>

In addition, there is a controller for shipping addresses if someone chooses to view those pages on their own.

class ShippingAddressesController < ApplicationController
   before_action :set_shipping_address, only: %i[ show edit update destroy ]
 
   # GET /shipping_addresses or /shipping_addresses.json
   def index
     @shipping_addresses = ShippingAddress.all
   end
 
   # GET /shipping_addresses/1 or /shipping_addresses/1.json
   def show
   end
 
   # GET /shipping_addresses/new
   def new
     @shipping_address = ShippingAddress.new
   end
 
   # GET /shipping_addresses/1/edit
   def edit
   end
 
   # POST /shipping_addresses or /shipping_addresses.json
   def create
     @shipping_address = ShippingAddress.new(shipping_address_params)
 
     respond_to do |format|
       if @shipping_address.save
         format.html { redirect_to @shipping_address, notice: "Shipping address was successfully created." }
         format.json { render :show, status: :created, location: @shipping_address }
       else
         format.html { render :new, status: :unprocessable_entity }
         format.json { render json: @shipping_address.errors, status: :unprocessable_entity }
       end
     end
   end
 
   # PATCH/PUT /shipping_addresses/1 or /shipping_addresses/1.json
   def update
     respond_to do |format|
       if @shipping_address.update(shipping_address_params)
         format.html { redirect_to @shipping_address, notice: "Shipping address was successfully updated." }
         format.json { render :show, status: :ok, location: @shipping_address }
       else
         format.html { render :edit, status: :unprocessable_entity }
         format.json { render json: @shipping_address.errors, status: :unprocessable_entity }
       end
     end
   end
 
   # DELETE /shipping_addresses/1 or /shipping_addresses/1.json
   def destroy
     @shipping_address.destroy
     respond_to do |format|
       format.html { redirect_to shipping_addresses_url, notice: "Shipping address was successfully destroyed." }
       format.json { head :no_content }
     end
   end
 
   private
     # Use callbacks to share common setup or constraints between actions.
     def set_shipping_address
       @shipping_address = ShippingAddress.find(params[:id])
     end
 
     # Only allow a list of trusted parameters through.
     def shipping_address_params
       params.require(:shipping_address).permit(:address_line1, :address_line2, :city, :state, :country, :client_id)
     end
 end

Behavior of the Application

Currently, the application does accept the submission of a client and it makes a blank entry in the shipping addresses table and posts the id that corresponds to its foreign key. However, no other values are created in the entry.

Corrective Actions

I’ve gone through the rails documentation, read forum posts, and scoured the web for articles on nested forms from version to version. So far, I’ve had no luck in finding a way to allow the parameters to be saved into the other table that have been successful.

I also tried the dynamic nested form developed at stevepolitodesign for Rails 6. The app works, but with bootstrap it does not work. So, there is a problem with the more Javascript-based approach there.

Scope

I’m seeking help to understand what is wrong with the configurations if they are incorrect and/or if anyone is having similar issues and if they have been resolved. This project is an example derived from a larger project I’m working on that will need to use nested attributes to be beneficial to users.

Thanks!!

Try adding :id to the shipping_addresses_attributes array. That’s the only thing I see that’s different from your implementation and any of mine.

Walter

Thanks for your reply.

I added the ‘id’ into the array again. The controller continues to manipulate the other model, but it does not insert the full hash. The image below demonstrates what I’m describing best. As you can see, Client column displays the client_id in the shipping_addresses table. Item numbers 5-20 were before the ‘id’ was added. The next entry is with the ‘id’ added back. It would appear this does not resolve the issue.

What do you see in your console when you submit the form? Are any of the attributes being noted as illegal and skipped?

Walter

The console prints the following when the command is executed. The only abnormality I continue to see is that it will say :shipping_address is not a permitted parameter. However, it’s been told to accept the nested attribute for that model…so…why it says this I’m not sure.

Started GET "/clients" for 127.0.0.1 at 2021-08-26 09:11:36 -0500
Processing by ClientsController#index as HTML
  User Load (6.8ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
  Rendering layout layouts/application.html.erb
  Rendering clients/index.html.erb within layouts/application
  Client Load (23.6ms)  SELECT `clients`.* FROM `clients`
  ↳ app/views/clients/index.html.erb:14
  Rendered clients/index.html.erb within layouts/application (Duration: 213.6ms | Allocations: 5252)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 257.5ms | Allocations: 8814)
Completed 200 OK in 384ms (Views: 221.6ms | ActiveRecord: 75.5ms | Allocations: 13546)


Started GET "/clients/new" for 127.0.0.1 at 2021-08-26 09:11:39 -0500
Processing by ClientsController#new as HTML
  User Load (8.2ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
  Rendering layout layouts/application.html.erb
  Rendering clients/new.html.erb within layouts/application
  Rendered clients/_form.html.erb (Duration: 12.5ms | Allocations: 2054)
  Rendered clients/new.html.erb within layouts/application (Duration: 24.1ms | Allocations: 2418)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 86.0ms | Allocations: 5856)
Completed 200 OK in 266ms (Views: 91.7ms | ActiveRecord: 21.9ms | Allocations: 17504)


Started POST "/clients" for 127.0.0.1 at 2021-08-26 09:11:59 -0500
Processing by ClientsController#create as HTML
  Parameters: {"authenticity_token"=>"[FILTERED]", "client"=>{"client_name"=>"FDSAFDF", "shipping_address"=>{"address_line1"=>"DSAFFSA", "address_line2"=>"ADSFDAS", "city"=>"AASDF", "state"=>"DFD", "country"=>"DFD"}}, "commit"=>"Create Client"}
  User Load (9.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
Unpermitted parameter: :shipping_address
Unpermitted parameter: :shipping_address
  TRANSACTION (13.7ms)  BEGIN
  ↳ app/controllers/clients_controller.rb:32:in `create'
  Client Create (9.2ms)  INSERT INTO `clients` (`client_name`, `created_at`, `updated_at`) VALUES ('FDSAFDF', '2021-08-26 14:11:59.529388', '2021-08-26 14:11:59.529388')
  ↳ app/controllers/clients_controller.rb:32:in `create'
  Audited::Audit Create (9.2ms)  INSERT INTO `audits` (`auditable_id`, `auditable_type`, `user_id`, `user_type`, `action`, `audited_changes`, `version`, `remote_address`, `request_uuid`, `created_at`) VALUES (21, 'Client', 2, 'User', 'create', '---\nclient_name: FDSAFDF\n', 1, '127.0.0.1', '9cb00f7b-d499-46e7-af26-176550db08b1', '2021-08-26 14:11:59')
  ↳ app/controllers/clients_controller.rb:32:in `create'
  ShippingAddress Create (14.6ms)  INSERT INTO `shipping_addresses` (`client_id`, `created_at`, `updated_at`) VALUES (21, '2021-08-26 14:11:59.956052', '2021-08-26 14:11:59.956052')
  ↳ app/controllers/clients_controller.rb:32:in `create'
  Audited::Audit Create (21.6ms)  INSERT INTO `audits` (`auditable_id`, `auditable_type`, `user_id`, `user_type`, `action`, `audited_changes`, `version`, `remote_address`, `request_uuid`, `created_at`) VALUES (17, 'ShippingAddress', 2, 'User', 'create', '---\naddress_line1: \'\'\naddress_line2: \ncity: \'\'\nstate: \'\'\ncountry: \'\'\nclient_id: 21\n', 1, '127.0.0.1', '9cb00f7b-d499-46e7-af26-176550db08b1', '2021-08-26 14:12:00')
  ↳ app/controllers/clients_controller.rb:32:in `create'
  TRANSACTION (45.5ms)  COMMIT
  ↳ app/controllers/clients_controller.rb:32:in `create'
Redirected to http://localhost:3000/clients/21
Completed 302 Found in 709ms (ActiveRecord: 171.4ms | Allocations: 24112)


Started GET "/clients/21" for 127.0.0.1 at 2021-08-26 09:12:00 -0500
Processing by ClientsController#show as HTML
  Parameters: {"id"=>"21"}
  User Load (3.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
  Client Load (7.0ms)  SELECT `clients`.* FROM `clients` WHERE `clients`.`id` = 21 LIMIT 1
  ↳ app/controllers/clients_controller.rb:72:in `set_client'
  Rendering layout layouts/application.html.erb
  Rendering clients/show.html.erb within layouts/application
  Rendered clients/show.html.erb within layouts/application (Duration: 2.9ms | Allocations: 276)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 49.9ms | Allocations: 3741)
Completed 200 OK in 147ms (Views: 57.6ms | ActiveRecord: 10.7ms | Allocations: 6526)


Started GET "/clients" for 127.0.0.1 at 2021-08-26 09:12:06 -0500
Processing by ClientsController#index as HTML
  User Load (25.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
  Rendering layout layouts/application.html.erb
  Rendering clients/index.html.erb within layouts/application
  Client Load (9.0ms)  SELECT `clients`.* FROM `clients`
  ↳ app/views/clients/index.html.erb:14
  Rendered clients/index.html.erb within layouts/application (Duration: 30.9ms | Allocations: 2503)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 58.1ms | Allocations: 5878)
Completed 200 OK in 156ms (Views: 54.4ms | ActiveRecord: 34.3ms | Allocations: 7588)


Started GET "/" for 127.0.0.1 at 2021-08-26 09:12:12 -0500
Processing by PagesController#home as HTML
  User Load (4.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
  Rendering layout layouts/application.html.erb
  Rendering pages/home.html.erb within layouts/application
  Rendered pages/home.html.erb within layouts/application (Duration: 0.5ms | Allocations: 112)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 11.0ms | Allocations: 3453)
Completed 200 OK in 27ms (Views: 12.2ms | ActiveRecord: 4.3ms | Allocations: 5140)


Started GET "/shipping_addresses" for 127.0.0.1 at 2021-08-26 09:12:14 -0500
Processing by ShippingAddressesController#index as HTML
  User Load (7.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
  Rendering layout layouts/application.html.erb
  Rendering shipping_addresses/index.html.erb within layouts/application
  ShippingAddress Load (8.6ms)  SELECT `shipping_addresses`.* FROM `shipping_addresses`
  ↳ app/views/shipping_addresses/index.html.erb:19
  Rendered shipping_addresses/index.html.erb within layouts/application (Duration: 113.3ms | Allocations: 3051)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 255.5ms | Allocations: 6613)
Completed 200 OK in 341ms (Views: 300.5ms | ActiveRecord: 15.9ms | Allocations: 9515)

Try changing this to <%= form.fields_for :shipping_addresses do |s| %>

I think what’s happening here is you are creating a new instance each time in this view, instead of using the instances already defined on the parent model. On your controller, make sure you build a new instance of the shipping address in your #new and #edit methods, and on your model, add reject_if: :all_blank to the accepts_nested_attributes_for invocation. That way you will always have a “new” field ready to be added, and a field for each child record already saved.

Walter

Another interesting things I observed is that in the Rails Guide, they say that the fields_for helper should be written in the following fashion:

<%= form_with model: @person do |form| %>
  Addresses:
  <ul>
    <%= form.fields_for :addresses do |addresses_form| %>
      <li>
        <%= addresses_form.label :kind %>
        <%= addresses_form.text_field :kind %>

        <%= addresses_form.label :street %>
        <%= addresses_form.text_field :street %>
        ...
      </li>
    <% end %>
  </ul>
<% end %>

My fields_for helper is different.

<%= form.fields_for @client.shipping_addresses.build do |s| %>
     
     <div class="field">
       <%= s.label :address_line1, 'Address Line 1' %>
       <%= s.text_field :address_line1 %>
     </div>
 
     <div class="field">
       <%= s.label :address_line2, 'Address Line 2' %>
       <%= s.text_field :address_line2 %>
     </div>
 
     <div class="field">
       <%= s.label :city, 'City' %>
       <%= s.text_field :city %>
     </div>
 
     <div class="field">
       <%= s.label :state, 'State' %>
       <%= s.text_field :state %>
     </div>
 
     <div class="field">
       <%= s.label :country, 'Country' %>
       <%= s.text_field :country %>
     </div>
 
   <% end %>

I changed my helper and tried the method in the guide and it through the following error:

I think the reason mine was different was because this popped up before and I read someone’s version and saw they tried something similar.

We simultaneously posted.

Ok…

It seems that did make it start working much better. It’s now placing the items into a row and saving it into the other model.

But…it’s making two records. The first has the correct information, the second one it makes is a copy of the id in a blank record.

Fixed it!

I needed the @client.shipping_addresses.build in the new and edit, but not in the create. It was doing two things with the one because the parameters are already called by the client object.

Thanks for all your help!! :slight_smile:

That’s great. If you add the reject_if: :all_blank to your model, then the empty form won’t get saved on an edit/update.

Walter