I have 4 models: Business, Store, Product and StoreProduct
Business has many stores and products
I want a store to have many products and products should belong to many stores. When selecting products that belong to a store, I also want to specify quantity of each products. This should be done through nested forms in the new and edit store forms and I am using stimulus nested form component.
I am getting unpermitted parameters when saving store form.
error
Unpermitted parameters: :Product_ids, :quantity, :_destroy. Context: { controller:
StoresController, action: update, request: #<ActionDispatch::Request:0x00007f5cc927cfe0>,
params: {"_method"=>"patch", "authenticity_token"=>"[FILTERED]", "store"=>{"name"=>"Arikeade Trendings, Challenge",
"Product_ids"=>"6", "quantity"=>"10", "_destroy"=>"false"},
"button"=>"", "controller"=>"stores", "action"=>"update", "business_id"=>"3", "id"=>"2"} }
store.rb
# validates presence, uniqueness, length and case-sensitivity of name attribute
validates :name, presence: true, uniqueness: {scope: :business_id, message: "Store name must be unique"}, length: { minimum: 3, maximum: 255 }
belongs_to :user
belongs_to :business
# store products association
has_many :store_products, inverse_of: :store, dependent: :destroy
has_many :products, through: :store_products, dependent: :destroy
# accepted nested form attributes
accepts_nested_attributes_for :store_products, reject_if: :all_blank, allow_destroy: true
end
store_product.rb
class StoreProduct < ApplicationRecord
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
belongs_to :product
belongs_to :store
end
product.rb
class Product < ApplicationRecord
...
belongs_to :user
belongs_to :business
# store products association
has_many :store_products, dependent: :destroy
has_many :stores, through: :store_products
'''
end
stores_controller.rb
class StoresController < ApplicationController
...
def new
@store = Store.new
end
def create
@store = @business.stores.build(store_params)
@store.user = current_user
if @store.save
respond_to do |format|
format.html { redirect_to business_store_path(@business, @store), notice: 'store successfully created' }
end
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @store .update(store_params)
respond_to do |format|
format.html { redirect_to business_store_url(@business, @store), notice: 'store successfully updated' }
end
else
render :edit, status: :unprocessable_entity
end
end
private
def store_params
params.require(:store).permit(:name, product_ids: [], store_products_attributes: [:id, :_destroy, :quantity, :product_id])
end
def find_store
@store ||= Store.find(params[:id])
end
def find_business
@business ||= Business.find(params[:business_id])
end
def find_products
@business = Business.find(params[:business_id])
@products ||= @business.products.pluck(:name, :id)
end
...
end
stores/form
<div data-controller='nested-form'>
<%= form_with model: [business, store] do |f| %>
<%= render 'shared/error_messages', f: f %>
<div class="mb-3">
<%= f.text_field :name, placeholder: 'Store name', class: 'form-control' %>
</div>
<!--<div data-controller="tom-select">
<small>Add products to store</small>
<%#= f.select :product_ids, @products, {}, { multiple: true, id: "select-products", class: 'mb-3 form-control' } %>
</div>-->
<div class="d-block w-100 mt-3">
<h6><small>Add store products:</small></h6>
<hr>
</div>
<%# Custom logic for nested form %>
<template data-nested-form-target="template">
<div class="nested-form-wrapper row w-100" data-new-record="<%= f.object.new_record? %>">
<div class="col-md-4" data-controller="tom-select">
<%= f.select :Product_id, @products, { include_blank: "Select Product" }, class: "form-control mb-3 form-control" %>
</div>
<div class="col-md-4">
<%= f.number_field :quantity, placeholder: 'Quantity', class: "form-control" %>
</div>
<div class="col-md-3">
<button type="button" class="btn btn-danger btn-sm border" style="font-size: 10px;" data-action="nested-form#remove">Remove product</button>
</div>
<%= f.hidden_field :_destroy %>
</div>
</template>
<!-- Inserted elements will be injected before that target. -->
<div data-nested-form-target="target"></div>
<button type="button" class="btn btn-success w-100 mb-3" style="font-size: 10px;" data-action="nested-form#add">Add Product</button>
<div class="submit">
<%= link_to business_stores_path(@business), class: "btn btn-light rounded-1 border me-2" do %>
<%= image_tag "x.png", width: "19" %>
<% end %>
<%= button_tag type: 'Submit', class: "btn btn-success rounded-1 border" do %>
<%= image_tag "check.png", width: "19" %>
<% end %>
</div>
<% end %>
</div>
A few things:
your form is producing the params "Product_ids"
but you are allowing :product_ids
in your store_params
method in your controller (capital P is the error).
your :store_product_attributes
in the store_params
method is expecting a hash of values to be returned like:
'store_product_attributes'=>{'quantity'=>'10', '_delete'=>'false'}
You need to look at the generated HTML in your form. It should be producing form field names like store_product_attributes[quantity]
and store_product_attributes[_delete]
One way is something like:
f.fields_for :product_attributes do |prod_attr|
...
<%= prod_attr.number_field :quantity, placeholder: 'Quantity', class: "form-control" %>
...
<%= prod_attr.hidden_field :_destroy %>
...
end
I'm not up to speed on stimulus, so not sure how that affect this. But this is standard code for getting nested attributes in form params.