具有 Rails form_with 和强参数的三重嵌套形式

Triple-nested form with Rails form_with and strong parameters

我是 运行 Rails 6.0.0.rc1 并且无法使三重嵌套表单正常工作。我有 productsoptionsoption_values。因此,一个产品可能有一个名为 "Color" 的选项和一个名为 "Red." 的选项值 我想在产品表单的 classic nested form 中创建所有这些。

表单有效,我可以保存带有选项的产品,但不能保存提交时的选项值。我不确定为什么当我尝试在 fields_for 选项中嵌入 fields_for 选项值时它不起作用。

我在这里做错了什么?我觉得我错过了一些明显的东西,但无法弄清楚。 (可能不相关,但请注意,我需要将每个对象的范围限定为 account_id 和我的用户 has_one :account 这就是隐藏字段的原因。)

这是我的产品型号:

class Product < ApplicationRecord
  belongs_to :account

  has_many :options, dependent: :destroy
  accepts_nested_attributes_for :options, allow_destroy: true

  validates :account_id,  presence: true
  validates :name,        presence: true
end

选配型号:

class Option < ApplicationRecord
  belongs_to :account
  belongs_to :product

  has_many :option_values, dependent: :destroy
  accepts_nested_attributes_for :option_values, allow_destroy: true

  validates :account_id,  presence: true
  validates :name,        presence: true
end

选项值模型:

class OptionValue < ApplicationRecord
  belongs_to :account
  belongs_to :option

  validates :account_id,  presence: true
  validates :name,        presence: true
end

这是产品表格:

<%= form_with(model: product, local: true) do |f| %>
  <%= f.fields_for :options do |options_form| %>
    <fieldset class='form-group'>
      <%= options_form.hidden_field :account_id, value: current_user.account.id %>
      <%= options_form.label :name, 'Option' %>
      <%= options_form.text_field :name, class: 'form-control' %>
    </fieldset>

    <%= f.fields_for :option_values do |values_form| %>
      <fieldset class='form-group'>
        <%= values_form.label :name, 'Value' %>
        <%= values_form.text_field :name, class: 'form-control' %>
      </fieldset>
    <% end %>
  <% end %>
<% end %>

产品控制器:

class ProductsController < ApplicationController
  def new
    @product = Product.new
    @product.options.build
  end

  def create
    @account = current_user.account
    @product = @account.products.build(product_params)

    respond_to do |format|
      if @product.save
        format.html { redirect_to @product, notice: 'Product was successfully created.' }
      else
        format.html { render :new }
      end
    end
  end

  private 
    def product_params
      params.require(:product).permit(
        :account_id, :name,
        options_attributes: [
          :id, :account_id, :name, :_destroy,
          option_values_attributes:[:id, :account_id, :name, :_destroy ]
        ]
      )
    end
end

首先,您需要将表单更改为在 option 中嵌套 option_values,并在选项值中添加 account_id 字段:

<%= form_with(model: product, local: true) do |f| %>
  <%= f.fields_for :options do |options_form| %>
    <fieldset class='form-group'>
      <%= options_form.hidden_field :account_id, value: current_user.account.id %>
      <%= options_form.label :name, 'Option' %>
      <%= options_form.text_field :name, class: 'form-control' %>
    </fieldset>

    <%= options_form.fields_for :option_values do |values_form| %>
      <fieldset class='form-group'>
        <%= values_form.hidden_field :account_id, value: current_user.account.id %>
        <%= values_form.label :name, 'Value' %>
        <%= values_form.text_field :name, class: 'form-control' %>
      </fieldset>
    <% end %>
  <% end %>
<% end %>

您还需要在控制器中构建嵌套记录。另一种选择是通过 javascript 动态构建它们(例如,查看 cocoon gem)。要构建 3 个选项,每个选项有 3 个值:

def new 
  @account = current_user.account 
  # it is better to create associated product 
  @product = @account.products.new 
  3.times do 
    option = @product.options.build 
    3.times { option.option_values.build } 
  end 
end

更新:

因为我试图跟进 this Nested Form Railscast,最大的问题是我没有意识到 Ryan Bates 正在使用的版本是 "Edit",而不是 "New",所以我通过控制台添加了产品、选项和值,并让表单使用以下代码:

_form.html.erb

  <%= f.fields_for :options do |builder| %>
    <%= render 'option_fields', f: builder %>
  <% end %>

_option_fields.html.erb

<fieldset class='form-group'>
  <%= f.hidden_field :account_id, value: current_user.account.id %>

  <%= f.label :name, 'Option' %>
  <%= f.text_field :name, class: 'form-control' %>
  <br>
  <%= f.check_box :_destroy, class: 'form-check-input' %>
  <%= f.label :_destroy, 'Remove Option' %>

  <small id="optionHelp" class="form-text text-muted">
    (e.g. "Size" or "Color")
  </small>

  <%= f.fields_for :option_values do |builder| %>
    <%= render 'option_value_fields', f: builder %>
  <% end %>

</fieldset>

_option_value_fields.html.erb

<fieldset class='form-group'>
  <%= f.hidden_field :account_id, value: current_user.account.id %>
  <%= f.label :name, 'Value' %>
  <%= f.text_field :name, class: 'form-control' %>
  <br>
  <%= f.check_box :_destroy, class: 'form-check-input' %>
  <%= f.label :_destroy, 'Remove Value' %>

  <small id="optionValueHelp" class="form-text text-muted">
    (e.g. "Small, Medium, Large" or "Red, Green, Blue")
  </small>
</fieldset>

此外,与 Railscast 的唯一区别是在控制器中使用了强参数,因此您只需要像这样嵌套它们:

产品控制器

def product_params
      params.require(:product).permit(:account_id, :name, options_attributes [:id, :account_id, :name, :_destroy, option_values_attributes: [:id, :account_id, :name, :_destroy]])
end

选项控制器

def option_params
     params.require(:option).permit(:account_id, :name, option_values_attributes [:id, :account_id, :name, :_destroy])
end

选项值控制器

def option_value_params
     params.require(:option_value).permit(:account_id, :option_id, :name)
end

我不会在控制器中构建嵌套对象,而是像 Railscast 剧集中那样使用 Javascript 或她的回答中建议的 Cocoon gem as Vasilisa

只是想分享最终有效的代码,以防其他人遇到类似问题。我认为 Railscast 虽然很旧,但仍然是对 Rails 中嵌套表单的一个很好的介绍,但您只需要了解使用 form_with 和强参数所需的更改。非常感谢 Vasilisa 帮我解决了这个问题。

您在遵循 Rails Nested Form Railscast 时需要注意的主要 "gotchas" 是:

  • form_with 与旧的 rails form_tag
  • 有一些不同的语法
  • 确保您在创建表单块时没有任何拼写错误或名称问题,因为它们嵌套了两次
  • 与控制器中的嵌套参数相同,请注意语法和拼写错误
  • 请注意,Ryan Bates 正在使用未通过他正在构建的表单添加的数据进行演示,因此如果您想继续操作,则需要在控制台中创建一些数据
  • 使用强参数,您必须明确列出 :_destroy 作为参数,以便他的 "Remove" 复选框起作用