Search code examples
ruby-on-railsrubynested-formscocoon-gem

Rails Multiple Nested Forms


This is my first Rails project and I am attempting to create a form to add Products that can have multiple Property name and value pairs. I have followed several tutorials and looked at several answers here regarding nested forms, but I am stuck on whether the information is actually saving and displaying the information. I was using the Cocoon gem to dynamically create additional property name and value pairs. I've tried using a plain nested form to make sure I have structured the models correctly - that is the code below. I've included relevant code from my project below. Any help is greatly appreciated!

controllers/products_controller.erb

class ProductsController < ApplicationController
  def index
    @products = Product.all
    gon.products = Product.all
  end

  def show
    @product = Product.find(params[:id])
    @properties = Product.find(params[:id])
  end

  def new
    @product = Product.new
    @properties = @product.properties.build
    @product_properties = @properties.product_properties.build
  end

  def create
    @product = Product.new(product_params)

    if @product.save
    redirect_to products_path,
    notice: 'The product was successfully created.'
    else
      render 'new'
    end
  end

  private
  def product_params
    params.require(:product).permit(:name, :upc, :available_on,
      :properties_attributes => [:property_name,
        :product_properties_attributes => [:value]
        ])
  end
  def get_property
    @property = Property.find(params[:property_id])
  end
end

models/product.rb

class Product < ApplicationRecord
  has_many :properties
  has_many :product_properties,
           :through => :properties
  accepts_nested_attributes_for :properties
  accepts_nested_attributes_for :product_properties
  attr_accessor :properties_attributes,
                :product_properties_attributes

  validates :name, :upc, :available_on, presence: true
  validates :name, :upc, uniqueness: true
  validates :upc, numericality: { only_integer: true }

  validates :name, length: { maximum: 1024,
    too_long: "%{count} characters is the maximum allowed" }


  validate :check_length

  def check_length
    unless upc.size == 10 or upc.size == 12 or upc.size == 13
      errors.add(:upc, "length must be 10, 12, or 13 characters")
    end
  end

  validate :expiration_date_cannot_be_in_the_past

  def expiration_date_cannot_be_in_the_past
    errors.add(:available_on, "must be a future date") if
      !available_on.blank? and available_on < Date.today
  end

end

models/property.rb

class Property < ApplicationRecord
  belongs_to :product
  has_many :product_properties
  accepts_nested_attributes_for :product_properties
  attr_accessor :property_name,
                :value,
                 :product_properties_attributes


  validates :property_name, presence: true
  validates :property_name, uniqueness: true
  validates :property_name, length: { maximum: 255,
    too_long: "%{count} characters is the maximum allowed" }
end

models/product_property.rb

class ProductProperty < ApplicationRecord
  belongs_to :property
  belongs_to :product
  attr_accessor :value

  validates :value, presence: true
  validates :value, length: { maximum: 255,
    too_long: "%{count} characters is the maximum allowed" }

end

views/products/new.html.erb

<h1>New Product</h1>
<%= form_with scope: @product, url: products_path, local: true do |f| %>

<% if @product.errors.any? %>
   <div id="error_explanation">
     <h2>
       <%= pluralize(@product.errors.count, "error") %> prohibited
       this product from being saved:
     </h2>
     <ul>
       <% @product.errors.full_messages.each do |msg| %>
         <li><%= msg %></li>
       <% end %>
     </ul>
   </div>
 <% end %>

  <p>
    <strong>Name</strong><br>
    <%= f.text_field :name %>
  </p>

  <p>
    <strong>UPC</strong><br>
    <%= f.text_field :upc %>
  </p>

  <p>
    <strong>Available On</strong><br>
    <%= f.date_field :available_on %>
  </p>

  <h3>Properties</h3>
  <div id='properties'>
    <%= f.fields_for (:properties) do |property| %>
      <%= property.fields_for (:product_properties) do |product_property| %>
      <p>
        <strong>Property Name</strong><br>
        <%= property.text_field :property_name %>
      </p>

      <p>
        <strong>Property Value</strong><br>
        <%= product_property.text_field :value %>
      </p>
     <% end %>
<% end %>
  </div>


  <p>
    <%= f.submit "Add Product" %>
  </p>
<% end %>

views/products/index.html.erb

<h1>Products</h1>

<%= link_to 'New Product', new_product_path %>

<div id="search"></div>

<table>
  <tr>
    <th>Name</th>
    <th>UPC</th>
    <th>Available On</th>
    <th></th>
  </tr>

  <% @products.each do |product| %>
    <tr>
      <td><%= product.name %></td>
      <td><%= product.upc %></td>
      <td><%= product.available_on %></td>
       <% product.properties.each do |property| %>
       <td><%= property.property_name %></td>
       <% end %>
      <td><%= link_to 'Details', product_path(product) %></td>
    </tr>
  <% end %>
</table>

Solution

  • The new form_with which combines the previous form_for and form_tag makes it on the one hand pretty powerful but also sometimes very confusing. What used to be simply form_for @product, now you have to write

    form_with model: @product, local: true do |f|
    

    Your use of scope: just uses @product as a prefix and will not actually iterate over or use the @product to find associations, that is why we do not see the property_attributes, nor the implicit id field, even though you correctly have the accepts_nested_attributes (the _destroy field would be added by cocoon's link_to_remove_association --if you would want to go back to the ability to dynamically add/remove nested items).