Issue with Nested Forms in Rails 6.1.3.2
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.
Project GitHub: cjmccormick88/testapp-nested
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. Audited is used for audit trail.
Clients Controller
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: [:id, :address_line1, :address_line2, :city, :state, :country])
end
end
Client Model
class Client < ApplicationRecord
audited
has_many :shipping_addresses, :inverse_of => :client, autosave: true
accepts_nested_attributes_for :shipping_addresses
end
Shipping Address Model
class ShippingAddress < ApplicationRecord
audited
belongs_to :client
validates :client, :presence => true
end
Client View Form Partial
<%= 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.
Shipping Addresses Controller
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
The application accepts the entry of the client form and it manipulates the model for the shipping address; however, the only entry in the table on each row is the client_id value for the client foreign key. It is not committing the other components of the hash into the table.
Things Tried
- I've tried the application posted on GitHub at stevepolitodesign/rails-nested-form-app.
- I've tried a suggestion made from a similar post on rails forum: difficulties-with-nested-form-implementation-rails-6-1-3/78776/3.
- I've gone through as much documentation as I can track down on the standard rails guides for nested forms.
Results from all of these did not yield good results. Item # 1 was a decent app in terms of the pathway it takes; however, when you are using bootstrap it does not seem to work. It could be that the code there has to be modified some to allow that functionality. So far, any posts made for a request regarding bootstrap with that design have not yielded fruit.
Scope
I'm looking to understand the problem that is happening and/or find a better way to accomplish this function that cooperates well with Bootstrap use.
I think the main problem was that you were calling @client.shipping_addresses.build
in multiple places, so the values from the form were probably getting overridden.
- Remove
@client.shipping_addresses.build(client_params[:shipping_addresses_attributes])
and@client.save
and fromClientControllers#create
. - Remove
autosave: true
fromhas_many :shipping_addresses
from theClient
model. I'm not sure if that was also causing an issue or not. - Replace
@client.shipping_addresses.build
with:shipping_addresses
inapp/views/clients/_form.html.erb
.
I made a PR against your repo that should solve the problem