假设我有一个配方表,其中包含一个名称字段。表单看起来像这样:
<%= 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
这是在Rails中要做的难点之一,因为它需要在服务器上构建表单,然后在用户添加或删除字段时使用js在浏览器上编辑表单。我将尝试显示最低限度的工作示例。
首先使用fields_for在服务器上构建表单,该表单使您可以为accepts_nested_fields_for
关系之一的模型添加嵌套字段。在您的情况下,您将需要嵌套两次窗体(第一个用于Recipe
的Dose
,第二个用于每个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
文件并添加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 %>
这不会做太多,因为如果填充了关系,那么fields_for只会在其块内生成代码。因此,让我们在recipe
文件的doses
和new
操作中填充edit
的app/controllers/recipes_controller.rb
关系。在此期间,让我们将所有成分添加到@ingredients
变量中,并将嵌套的属性添加到strong_parameters 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
您可以构建任意数量的剂量,一旦设置了js部分,我们就可以在前端“构建”它们。现在仅显示1个就可以显示我们的剂量范围。
运行迁移并启动服务器并创建一些ingredients,然后在创建新的recipe时会在下拉列表中看到它们
现在您有了一个有效的嵌套字段解决方案,但是我们必须在后端中构建剂量,然后将具有一定数量的构建剂量的渲染表格发送到浏览器。让我们添加一些js,以允许用户即时创建和销毁嵌套字段。
销毁字段很容易,因为我们已经设置好了。如果_destroy
复选框处于启用状态,我们只需要隐藏该字段即可。为此,让我们安装Stimulus
bundle exec rails webpacker:install:stimulus
然后让我们在app/javascript/controllers/fields_for_controller.js
中创建一个新的刺激控制器>
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>
[好,现在我们在用户单击复选框时隐藏了该字段,并且后端将销毁剂量,因为选中了该复选框。
现在让我们看一下html 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
知道要删除哪些子项或在哪个子项上更新哪些属性。
所以现在答案很明确,我们只需要插入创建的相同<div>
fields_for
并将此新插入的child_index
的<div>
设置为比表格中先前的最大值更高的值。请记住,这只是一个index
,而不是一个id
,这意味着我们可以将其设置为一个非常大的数字,因为Rails仅将其用于将嵌套字段属性保留在同一组中,并在分配时实际分配id
保存记录。
所以现在我们必须做出两个选择:
fields_for
<div>
对于第一个选择,通常的答案是只获取当前时间并将其用作child_index
对于第二个,通常的方法是在app/views/doses/_fields.html.erb
中将html块移到其自己的部分中,然后在app/bies/recipes/_form.htm.erb
的表单内将该块渲染两次。进入form.fields_for
循环。在button
的data属性中第二次创建新表格,只是生成一个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>
然后使用js从按钮的数据标签中获取部分内容以更新
child_index
,并将更新后的html插入表单。由于按钮已经具有data-action='fields-for#add'
,所以我们只需要将添加操作添加到app/javascript/controllers/fields_for_controller.js
add(e){ e.preventDefault() e.target.insertAdjacentHTML('beforebegin', e.target.dataset.fields.replace(/new_field/g, new Date().getTime())) }
现在我们不需要预先构建
doses
。为此,使用gem更为简单,但是这样做的好处是您可以完全根据需要进行设置,并且不会向应用程序中添加不需要的任何代码。
也让我想到Portion
原本是Dose
的更好称呼
另一种选择是使用像https://github.com/schneems/wicked这样的宝石>
这样,您可以构建多步骤表单,并基于“上一步”获得表单中的数据。