问题
Goal
I would like to set up an order form, where users can order one product. After having filled out the product_category, a user can select
- the product belonging to the product_category
- the quantity of each option belonging to the product_category.
Current state
The way I currently set up my code, causes issues when the form has to be built up again:
- when a validation issue is triggered, (1) product_category, (2) product and (3) options are empty, but also the reservation still saves, thereby creating situation where a reservation is saved twice.
=> I know it's because I first save the reservation in the controller and then the options, but I don't know how to solve this (e.g. it is saved when the validation is triggered and when the user has filled in the form properly afterwards).
Code
models
class Order < ApplicationRecord
belongs_to :store
belongs_to :product
has_many :order_options, dependent: :destroy
has_many :options, through: :order_options
accepts_nested_attributes_for :order_options
end
class OrderOption < ApplicationRecord
belongs_to :option
belongs_to :order
accepts_nested_attributes_for :option
end
class Option < ApplicationRecord
belongs_to :product_category
has_many :order_options, dependent: :destroy
has_many :orders, through: :order_options
end
class ProductCategory < ApplicationRecord
belongs_to :store
has_many :products, dependent: :destroy
accepts_nested_attributes_for :products, allow_destroy: true
has_many :options, dependent: :destroy
accepts_nested_attributes_for :options, allow_destroy: true
end
order_controller
class OrdersController < ApplicationController
# skip_before_action :authenticate_user!
def new
@user = current_user
@store = Store.find(params[:store_id])
@order = Order.new
@order.build_order_contact
@product_category_list = @store.product_categories
@all_options = @store.options
@products = []
@options = []
if params[:product_category].present?
@products = ProductCategory.find(params[:product_category]).products
@options = ProductCategory.find(params[:product_category]).options
else
end
if request.xhr?
respond_to do |format|
format.json {
render json: {products: @products, options: @options}
}
format.js
end
end
authorize @order
end
def create
@user = current_user
@store = Store.find(params[:store_id])
@order = Order.new(order_params)
@order.store = @store
authorize @order
if @order.save
params[:order_options_attributes].each do |order_option|
if @option = Option.find_by(id: order_option[:option_id])
@option_quantity = order_option[:option_quantity]
@order.options << @option
order_option = @order.order_options.where(option: @option)
order_option.update(option_quantity: @option_quantity)
end
end
redirect_to store_path(@store)
else
@product_category_list = @store.product_categories
render 'new'
end
end
views/orders/new.js
$("#product_options").html("<%= escape_javascript(render partial: 'option_fields', collection: @options) %>");
$("#dynamic-products").empty();
<% @products.each do |pro| %>
$("#dynamic-products").append('<option value="<%= pro.id %>"><%= pro.name %></option>')
<% end %>
views/orders/new.html.erb
<%= simple_form_for [@store, @order] do |f|%>
<%= f.simple_fields_for :products do |product| %>
<%= product.input :product_category, collection: @product_category_list, prompt: "Select type of product", label:false,
input_html:{
id: "product_category"
}%>
<%= f.association :product, collection: @products, input_html:{
value: @products.object_id,
id: "dynamic-products"
} %>
<div class="product_category-options" id="product_options">
</div>
<% end %>
<% end %>
<script >
// dynamic products and options for change category
$(document).on("change", "#product_category", function(){
var product_category = $(this).val();
$.ajax({
url: "/stores/<%= @store.id %>/orders/new",
method: "GET",
// dataType: "json",
dataType: "script",
data: {product_category: product_category},
error: function (xhr, status, error) {
console.error('AJAX Error: ' + status + error);
},
success: function (response) {
}
});
});
// dynamic products and option for releading form (e.g. new)
$(document).ready(function(){
var product_category = $("#product_category").val();
$.ajax({
url: "/stores/<%= @store.id %>/orders/new",
method: "GET",
dataType: "json",
data: {product_category: product_category},
error: function (xhr, status, error) {
console.error('AJAX Error: ' + status + error);
},
success: function (response) {
}
});
});
</script>
views/orders/_option_fields.html.erb
<div class="product_option order-form-quantity-row border-bottom col col-sm-10">
<div class="product_option_name order-form-quantity-name">
<strong> <%= option_fields.name %></strong>
</div>
<div class="order-form-input">
<%= hidden_field_tag("order_options_attributes[]option_id", option_fields.id ) %>
<%= select_tag("order_options_attributes[]option_quantity", options_for_select((0..9)), {class:'form-control col col-sm-12'} ) %>
</div>
</div>
回答1:
This is very over-complicated and missguided. All you really need is something like:
<%= simple_form_for([@store, @order]) do |f| %>
<% f.simple_fields_for(:order_options) do |ff| %>
<%= ff.association :option %>
<%= ff.input :option_quantity %>
<% end %>
# ...
<% end %>
class OrdersController
# Use callbacks to DRY your code
before_action :set_store, only: [:new, :create, :index]
def new
@order = @store.order.new
# seed the record to create the inputs
5.times { @order.order_options.build }
authorize @order
end
def create
@order = @store.orders.new(order_params) do |order|
order.user = current_user
end
if @order.save
redirect_to @order.store
else
render :new
end
end
def set_store
@store = Store.find(params[:store_id])
end
def order_params
params.require(:order)
.permit(:foo, :bar,
order_options_attributes: [:option_id, :option_quantity]
)
end
end
You don't need to accept nested attributes for the option unless you are letting users create them on the fly which does not seem like a good idea since you already have 100 levels too much of complexity in a single component.
You also don't need to ever do params[:order_options_attributes].each do |order_option|
and iterate through the nested attributes. Really DON'T ever do this as it defeats the whole purpose of using nested attributes in the first place.
When you use the order_options_attributes=
setter created by accepts_nested_attributes
Rails will handle assigning the attributes to new instances of order_options and will do it before the record is saved. When you call save it will persist everything at once in a transaction which avoids most of the issues you have.
You can use validates_associated to trigger the validations on the order_options before saving.
If you then want to to use AJAX to spruce it up feel free. But you should really start by just setting up something simple and synchronous so that you understand how nested attributes work.
In general this code seems to be suffering from going to fast. Start by setting up just the basics (ie just creating an order for a product). Test it - refactor - and then add more features. If you try to do everything at once you usually end up with a dumpster fire.
来源:https://stackoverflow.com/questions/58693234/how-to-set-up-a-order-form-with-options-belonging-to-a-product-category