How to set up a order form with options belonging to a product_category

情到浓时终转凉″ 提交于 2019-12-11 14:46:51

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!