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
reading a form you provided:
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
).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:
<%= 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.chem_group_attributes: [:id, :name]
c'mon you're not passing
an id here - remove it.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.