Search code examples
ruby-on-railsrubynested-forms

Unpermitted parameters for nested many to many associations in Rails 7


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>

Solution

  • A few things:

    1. 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).

    2. 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.