【问题标题】:Rails Nesteds Forms with dynamic fields带有动态字段的 Rails 嵌套表单
【发布时间】:2019-10-02 16:13:35
【问题描述】:

假设我有一个包含名称字段的食谱表。表单看起来像这样:

<%= 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

【问题讨论】:

  • 看看cocoon gem。我相信这正是您想要的。
  • 谢谢。目前我试图在不使用宝石的情况下做到这一点。也许将来我会看看。

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


【解决方案1】:

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

让我们首先使用fields_for 在服务器上构建表单,它允许您为accepts_nested_fields_for 一个关系的模型添加嵌套字段。在您的情况下,您需要将表单嵌套两次(第一次用于RecipeDoses,第二次用于每个DoseIngredient)。用户不会真正看到 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 只会在填充关系时在其块内生成代码。因此,让我们在newedit 文件的app/controllers/recipes_controller.rb 操作中填充recipedoses 关系。当我们在那里时,让我们将所有成分添加到我们的 @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 来了解要删除哪些子项或更新哪个子项的哪些属性。

所以现在答案很明确了,我们只需要插入相同的&lt;div&gt; fields_for 并将这个新插入的&lt;div&gt;child_index 设置为比之前表单中的最高值更高的值。请记住,这只是一个index,而不是id,这意味着我们可以将它设置为一个非常大的数字,因为Rails 只会使用它来将嵌套字段属性保留在同一个组中,并在实际分配ids 时保存记录。

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

  1. 从哪里获取增量索引
  2. 从哪里获得 fields_for &lt;div&gt;

对于第一个选择,通常的答案是获取当前时间并将其用作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 的更好名字

【讨论】:

  • 谢谢。它帮助我处理嵌套表单。当我设法动态添加一些字段时,我会再次评论我是如何做到的,以防它对某人有用。
【解决方案2】:

另一种选择是使用像 https://github.com/schneems/wicked 这样的 gem

通过这种方式,您可以构建一个多步骤表单,并根据“上一步”在表单中保存数据。

对于做任何嵌套的事情来说也是一个很好的宝石 - https://github.com/nathanvda/cocoon

【讨论】:

  • 谢谢。目前我试图在不使用宝石的情况下做到这一点。也许将来我会看看那些宝石..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-04-05
  • 1970-01-01
  • 2023-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多