ruby-on-railsnested-form-forcollection-select

form_for collection_select not showing saved selections on edit


I have a form that I'm using for both new and edit. The form has fill-ins for product name, description, etc.

Also, the user can either select an item from the nested drop down (collection_select), or they can create a new item. On "new" the form works fine - all entries and selections save.

When the user goes to edit the saved product, the form pre-loads all filled-in entries for that item, BUT will NOT pre-load their original selection within collection_select.

AND, if the user wants to edit the item and decides to create a new item INSTEAD of the previously selected collection_select item, an error appears stating the product has already been created with that chemical group. Any help with this double-dilemma would be appreciated. I'm new to RoR and I'm sure I'm missing something somewhere.

Here is my form

<%= render partial: 'layouts/errors', locals: {object: @product} %>

<%= form_for(@product) do |f| %>
    <div>
        <%= f.label :name %><br>
        <%= f.text_field :name %><br>
    <div>
    <div>
        <%= f.label :active_ingredient %><br>
        <%= f.text_field :active_ingredient %><br>
    <div>
    <div>
        <%= f.label :description %><br>
        <%= f.text_area :description %><br>
    </div>
    <div>
        <%= f.label :image %><br>
        <%= f.file_field :image %><br>
    </div>
    <div>
        <p>Select a Chemical Group:</p>
        <%= f.collection_select :chem_group_id, ChemGroup.all, :id, :name, include_blank: 'Select One', selected: @product.chem_group, value: @product.chem_group.name %>
    </div>
    <div>
        <p>Or, create a new Chemical Group:</p>
        <!-- NESTED FORM! User writing attributes for another object. Use fields_for -->
        <%= f.fields_for :chem_group do |cg| %>
            <%= cg.label :name %>
            <%= cg.text_field :name %>
        <% end %>
    </div>
    <div>
        <p>Select an Application Area:</p>
        <%= f.collection_select :application_area_id, ApplicationArea.all, :id, :area_name, include_blank: 'Select One', selected: @product.application_area, value: @product.application_area.area_name %>
    </div>
    <div>
        <p>Or, create a new Application Area:</p>
        <!-- NESTED FORM! User writing attributes for another object. Use fields_for -->
        <%= f.fields_for :application_area do |aa| %>
            <%= aa.label :area_name %>
            <%=aa.text_field :area_name %>
        <% end %>
    </div>
    <br>
    <%= f.submit "Save" %>
<% end %>

Here is my model

class Product < ApplicationRecord
  belongs_to :chem_group
  belongs_to :application_area
  belongs_to :user #admin creator
  accepts_nested_attributes_for :chem_group #tells the model to accept chem_group attributes from cg nested form in new product form
  accepts_nested_attributes_for :application_area

  validates :active_ingredient, presence: true
  validates :application_area, presence: true
  validates :description, presence: true
  validates :name, presence: true
  validate :not_a_duplicate #checking for what we DON'T WANT

  def chem_group_attributes=(attributes)
    self.chem_group = ChemGroup.find_or_create_by(attributes) if !attributes['name'].empty?
    self.chem_group
  end

  def application_area_attributes=(attributes)
    self.application_area = ApplicationArea.find_or_create_by(attributes) if !attributes['area_name'].empty?
    self.application_area
  end

  #if there is already a product with that name && chem_group, give error
  def not_a_duplicate
    #calling the instance of the attribute [string/integer: key]
      if Product.find_by(name: name, chem_group_id: chem_group_id)
        errors.add(:name, 'has already been created for that Chemical Group')
      end
    end
end

Here is my controller

class ProductsController < ApplicationController

    def new
        if logged_in?
            @product = Product.new
            1.times {@product.build_chem_group} #for the nested form. Builds the chem_group attributes
            @product.build_application_area
        else
            flash[:error] = "Sorry, you must be logged in to create a new product."
            redirect_to products_path
        end
    end

    def create
        @product = Product.new(product_params)
        @product.user_id = session[:user_id] #bc product belongs_to user. user_id required from model
        if @product.save #validation
            # @product.image.purge
            # @product.image.attach(params[:product][:image]) # allows image to be replaced if user changes image
            redirect_to product_path(@product)
        else
            @product.build_chem_group
            @product.build_application_area
            render :new
        end
    end

    def edit
        find_product
        1.times {@product.build_chem_group}
        if @product.user != current_user
            flash[:error] = "Sorry, you can only edit your own products"
            redirect_to products_path
        end
    end

    def update
        find_product
        if @product.update(product_params)
            redirect_to product_path(@product)
        else
            render :edit
        end
    end

    private

    def product_params
        params.require(:product).permit(:name, :description, :active_ingredient, :image, :chem_group_id, :application_area_id, chem_group_attributes: [:id, :name], application_area_attributes: [:id, :area_name])
        #chem_group_id and chem_group_attributes [:name] is permitting elements from new product form
    end

    def find_product
        @product = Product.find_by(id: params[:id])
    end

end

Solution

  • reading a form you provided:

    1. if you're editing a product the collection should show a previously chosen chem_group, and a fill in to create should be filled with it's name, as it's the same association object. It's not because you messed with selected and value options. selected in collection_select like this should point to id not to object. like: selected: @product.chem_group.id but it's really unnecessary here. you should simply skip those options (selected nad value as you're using form builder for @product).
    2. after someone tries to edit chem_group with new value in fill in real mess begins because in select field there is still an id pointing to previous chem_group and in chem_group_attributes there is only :name I'm guessing now that rails will saves it by chem_group_id and your custom validation will fail as it's existing object (it seams that this validation will never let you save the object without changing chem_group as its obviously existing - you're editing it!)

    Try that:

    1. change or remove custom validation. Or check if object found in validation isn't the one you're editing but was previously saved
    2. disable one of inputs by default (<%= cg.text_field :name, disabled: @product.chem_group.present? %>) and switch them depending what user is doing - selecting or typing (ex. use a checkbox and JS onchange). It's to be sure what params you're passing.
    3. also: chem_group_attributes: [:id, :name] c'mon you're not passing an id here - remove it.
    4. def chem_group_attributes=(attributes) overriding is dangerous. I know what you're trying to get but it might not be the most beautiful way to achieve that.

    Generally you also might want to see that: https://select2.org/tagging. It's still same complicated logic as you have here and also some JS works to make taggings work good on creating new associated objects but it looks nicer, has better UX, and I've been there - I'm using it with accepts_nested_attributes_for usually, you can hit me for examples.