这是在 Rails 中比较难做的事情之一,因为它需要在服务器上构建表单,然后在用户添加或删除字段时使用 js 在浏览器上编辑表单。我将尝试展示最简单的工作示例。
让我们首先使用fields_for 在服务器上构建表单,它允许您为accepts_nested_fields_for 一个关系的模型添加嵌套字段。在您的情况下,您需要将表单嵌套两次(第一次用于Recipe 的Doses,第二次用于每个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 只会在填充关系时在其块内生成代码。因此,让我们在new 和edit 文件的app/controllers/recipes_controller.rb 操作中填充recipe 的doses 关系。当我们在那里时,让我们将所有成分添加到我们的 @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>
太好了,现在我们在用户单击复选框时隐藏该字段,并且由于选中了复选框,后端将销毁剂量。
现在让我们看一下nested_fields 生成的html,以了解我们如何让用户添加和删除它们:
<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 为每个已构建的doses 分配一个增量child_index。后端使用该child_index 来了解要删除哪些子项或更新哪个子项的哪些属性。
所以现在答案很明确了,我们只需要插入相同的<div> fields_for 并将这个新插入的<div> 的child_index 设置为比之前表单中的最高值更高的值。请记住,这只是一个index,而不是id,这意味着我们可以将它设置为一个非常大的数字,因为Rails 只会使用它来将嵌套字段属性保留在同一个组中,并在实际分配ids 时保存记录。
所以现在我们必须做出两个选择:
- 从哪里获取增量索引
- 从哪里获得
fields_for <div>
对于第一个选择,通常的答案是获取当前时间并将其用作child_index
对于第二种方法,通常的方法是将 html 块移动到 app/views/doses/_fields.html.erb 中自己的部分,然后在 app/bies/recipes/_form.htm.erb 中的表单内呈现该块两次。一旦进入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>
然后使用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 的更好名字