I have a nested association for a customer. The setup is like this:
Address model
belongs_to :customer, optional: true # Yes, optional
has_one :physical_address, dependent: :destroy
has_one :postal_address, dependent: :destroy
accepts_nested_attributes_for :physical_address
accepts_nested_attributes_for :postal_address
Postal Address model
# same for physical_address.rb
belongs_to :address
Customer controller:
def create
@customer = current_user.customers.build(customer_params)
if @customer.save
return puts "customer saves"
end
puts @customer.errors.messages
#redirect_to new_customer_path
render :new
end
private
def customer_params
params.require(:customer).permit(
address_attributes: address_params
)
end
def address_params
return ([
postal_address_attributes: shared_address_params,
#physical_address_attributes: shared_address_params
])
end
def shared_address_params
params.fetch(:customer).fetch("address").fetch("postal_address").permit(
:street, etc...
)
end
Customer model:
has_one :address, dependent: :destroy
accepts_nested_attributes_for :address
A customer is created ok but not the address. Here's the form, for example:
<form>
<input name="customer[address][postal_address][street]" value="Foo Street" />
</form>
Logging "params", I see all the values but address is not creating. I believe the error lies in shared_address_params
. Any ideas?
I think you just managed to lose yourself in layers of indirection and complexity in that parameters whitelist.
What you basically want is:
def customer_params
params.require(:customer)
.permit(
address_attributes: {
physical_address_attributes: [:street, :foo, :bar, :baz],
postal_address: [:street, :foo, :bar, :baz]
}
)
end
As you can see here you need the param key customer[address_attributes]
not just customer[address]
.
Now lets refactor to cut the duplication:
def customer_params
params.require(:customer)
.permit(
address_attributes: {
physical_address_attributes: address_attributes,
postal_address: address_attributes
}
)
end
def address_attributes
[:street, :foo, :bar, :baz]
end
As you can see there should be very little added complexity here any and if you need to make it more flexible add arguments to the address_attributes
method - after all building the whitelist is just simple array and hash manipulation.
If you want to handle mapping some sort of shared attributes to the two address types you really should do it in the model instead of bloating the controller with business logic. Like for example by creating setters and getters for a "virtual attribute":
class Address < ApplicationController
def shared_address_attributes
post_address_attributes.slice("street", "foo", "bar", "baz")
end
def shared_address_attributes=(**attrs)
# @todo map the attributes to the postal and
# visiting address
end
end
That way you would just setup the form and whitelist it like any other attribute and the controller doesn't need to be concerned with the nitty gritty details.
def customer_params
params.require(:customer)
.permit(
address_attributes: {
shared_address_attributes: address_attributes,
physical_address_attributes: address_attributes,
postal_address: address_attributes
}
)
end