带有动态字段的Rails嵌套表单

时间:2019-10-02 16:13:35

标签: ruby-on-rails ruby-on-rails-5

假设我有一个配方表,其中有一个名称字段。表单看起来像这样:

<%= form_with(model: recipe, local: true) do |form| %>
  <%= form.text_field :name %>
  <%= form.submit %>
<% end %>

,但是如果配方通过中间表与配料的量与配料(包含名称字段)有关。 我应该怎样做才能创建食谱,选择配料并输入配料量。如果配方包含多种成分,还可以选择生成更多字段。所有这些都以一种形式存在。 像这样:

<%= form_with(model: recipe, local: true) do |form| %>

  <%= form.text_field :name %>

  (select field to choose an ingredient)

  (field for recipe_ingredient to ingress the amount of the ingredient)

  (button to generate more fields for other ingredients)

  <%= form.submit %>
<% end %>

rails版本:5.2.2

2 个答案:

答案 0 :(得分:3)

这是在Rails中要做的较难的事情之一,因为它需要在服务器上构建表单,然后在用户添加或删除字段时使用js在浏览器上编辑表单。我将尝试显示最低限度的工作示例。

首先让我们使用fields_for在服务器上构建表单,该表单使您可以为accepts_nested_fields_for与其中一种关系的模型添加嵌套字段。在您的情况下,您将需要嵌套两次表单(第一次嵌套Recipe的{​​{1}},第二次嵌套每个Dose的{​​{1}})。用户不会真正看到Dose模型,因为它只是作为中间表存在。

假设您已经按照以下步骤设置了应用程序:

Ingredient

然后将其添加到Dose模型中:

rails g scaffold Ingredient name:string

rails g scaffold Recipe name:string

rails g scaffold Dose ingredient:references recipe:references amount:decimal

这就是Recipe模型:

has_many :doses
has_many :ingredients, through: :doses

accepts_nested_attributes_for :doses, allow_destroy: true

现在编辑Dose文件并添加accepts_nested_attributes_for :ingredient 的字段

app/views/recipes/_form.html.erb

这不会做太多,因为如果填充了关系,那么fields_for只会在其块内生成代码。因此,让我们在Dose文件的<%= form.fields_for :doses do |dose_form| %> <div class="field"> <%= dose_form.label :_destroy %> <%= dose_form.check_box :_destroy %> </div> <div class="field"> <%= dose_form.label :ingredient_id %> <%= dose_form.select :ingredient_id, @ingredients %> </div> <div class="field"> <%= dose_form.label :amount %> <%= dose_form.number_field :amount %> </div> <% end %> recipe操作中填充doses的{​​{1}}关系。在那里,我们将所有成分添加到new变量中,并允许将嵌套属性添加到strong_parameters edit哈希中。

app/controllers/recipes_controller.rb

您可以构建任意数量的剂量,一旦设置了js部分,我们就可以在前端“构建”它们。现在仅显示1个剂量字段即可。

运行迁移并启动服务器并创建一些ingredients,然后在创建新的recipe

时会在下拉列表中看到它们。

现在您有了一个有效的嵌套字段解决方案,但是我们必须在后端构建剂量,然后将具有一定数量的构建剂量的渲染表格发送到浏览器。让我们添加一些js,以允许用户即时创建和销毁嵌套字段。

销毁字段很容易,因为我们已经设置好了。如果选中@ingredients,则只需要隐藏该字段即可。为此,我们安装刺激物

permitted

让我们在def new @recipe = Recipe.new @ingredients = Ingredient.all.pluck(:name, :id) 1.times{ @recipe.doses.build } end def edit @ingredients = Ingredient.all.pluck(:name, :id) end ... def recipe_params params.require(:recipe).permit(:name, doses_attributes: [:id, :ingredient_id, :amount, :_destroy]) end

中创建一个新的刺激控制器
_destroy

并更新我们的bundle exec rails webpacker:install:stimulus 以使用控制器:

app/javascript/controllers/fields_for_controller.js

太好了,现在我们在用户单击复选框时隐藏了该字段,由于选中了复选框,后端将销毁剂量。

现在,让我们看一下HTML import {Controller} from "stimulus" export default class extends Controller { static targets = ["fields"] hide(e){ e.target.closest("[data-target='fields-for.fields']").style = "display: none;" } } 生成的内容,以了解如何让用户添加和删除它们:

app/views/recipes/_form.html.erb

有趣的是<div data-controller="fields-for"> <%= form.fields_for :doses do |dose_form| %> <div data-target="fields-for.fields"> <div class="field"> <%= dose_form.label :_destroy %> <%= dose_form.check_box :_destroy, {data: {action: "fields-for#hide"}} %> </div> <div class="field"> <%= dose_form.label :ingredient_id %> <%= dose_form.select :ingredient_id, @ingredients %> </div> <div class="field"> <%= dose_form.label :amount %> <%= dose_form.number_field :amount %> </div> </div> <% end %> </div> ,特别是nested_fields,事实证明<div data-target="fields-for.fields"> <div> <label for="recipe_doses_attributes_0__destroy">Destroy</label> <input name="recipe[doses_attributes][0][_destroy]" type="hidden" value="0" /><input data-action="fields-for#hide" type="checkbox" value="1" name="recipe[doses_attributes][0][_destroy]" id="recipe_doses_attributes_0__destroy" /> </div> <div class="field"> <label for="recipe_doses_attributes_0_ingredient_id">Ingredient</label> <select name="recipe[doses_attributes][0][ingredient_id]" id="recipe_doses_attributes_0_ingredient_id"><option value="1">first ingredient</option> <option selected="selected" value="2">second ingredient</option> <option value="3">second ingredient</option></select> </div> <div class="field"> <label for="recipe_doses_attributes_0_amount">Amount</label> <input type="number" value="2.0" name="recipe[doses_attributes][0][amount]" id="recipe_doses_attributes_0_amount" /> </div> </div> <input type="hidden" value="3" name="recipe[doses_attributes][0][id]" id="recipe_doses_attributes_0_id" /> 向每个已构建的recipe[doses_attributes][0][ingredient_id]分配了增量[0]。后端使用该fields_for知道要删除的子项或要在哪个子项上更新的属性。

因此,答案很明确,我们只需要插入创建的child_index doses,并将此新插入的child_index的{​​{1}}设置为大于表格中先前的最高值。请记住,这只是一个<div>,而不是fields_for,这意味着我们可以将其设置为一个非常大的数字,因为Rails只会使用它来将嵌套字段的属性保留在同一组中并实际分配{保存记录时的{1}} s。

所以现在我们必须做出两个选择:

  1. 从何处获得增量索引
  2. 从哪里获得child_index <div>的地方

对于第一种选择,通常的答案是仅获取当前时间并将其用作index

对于第二个,通常的方法是将html块移动到id中自己的部分中,然后在id的表单内两次渲染该块。一旦进入fields_for循环。第二次在<div>的data属性中,我们在其中创建了一个新表单以生成一个child_index

app/views/doses/_fields.html.erb

然后使用js从按钮的数据标签中获取部分内容,以更新app/bies/recipes/_form.htm.erb并将更新后的html插入表单中。由于按钮已经具有form.fields_for,我们只需要向我们的button

添加添加操作即可
field_for

现在,我们不需要预先构建<div data-controller="fields-for"> <%= form.fields_for :doses do |dose_form| %> <%= render "doses/fields", dose_form: dose_form %> <% end %> <%= button_tag("Add Dose", {data: { action: "fields-for#add", fields: form.fields_for(:doses, Dose.new, child_index:"new_field"){|dose_form| render("doses/fields", dose_form: dose_form)}}}) %> </div> 。为此,使用gem更为简单,但是这样做的好处是您可以完全根据需要进行设置,并且不会向应用程序中添加不需要的任何代码。

我还突然想到child_indexdata-action='fields-for#add'的更好称呼

答案 1 :(得分:0)

另一种选择是使用https://github.com/schneems/wicked之类的宝石

这样,您可以构建一个多步骤表单,并根据“上一步”在表单中获取数据。

也是做嵌套工作的绝佳宝石-https://github.com/nathanvda/cocoon