57

我有一个非常标准的用例。我有一个父对象和一个子对象列表。我想要一个表格形式,我可以在其中一次编辑所有子项,作为表格中的行。我还希望能够插入一个或多个行,并在提交时将它们创建为新记录。

当我使用 afields_for为 has-many 相关的嵌套记录呈现一系列子表单时,rails 会生成字段名称,例如parent[children_attributes][0][fieldname]parent[children_attributes][1][fieldname]等等。

这会导致 Rack 解析一个 params 哈希,如下所示:

{ "parent" => { 
    "children" => {
      "0" => { ... },
      "1" => { ... } } }

当传递一个的(非持久化的)对象时,同样fields_for会生成一个如下所示的字段名称:

parent[children_attributes][][fieldname]

请注意[]其中没有索引。

不能[0]与包含,等的字段以相同的形式发布[1],因为 Rack 会感到困惑并引发

TypeError: expected Array (got Rack::Utils::KeySpaceConstrainedParams)

“好吧”,我想。“我会确保所有字段都使用[]表单而不是[index]表单。但我不知道如何说服fields_for他们始终如一地这样做。即使我给它一个明确的字段名称前缀和对象:

fields_for 'parent[children_attributes][]', child do |f| ...

只要child是持久化的,它就会自动修改字段名,使它们成为例如parent[children_attributes][0][fieldname],而将新记录的字段名保留为parent[children_attributes][][fieldname]。再一次,机架呕吐。

我不知所措。我如何使用标准 Rails 助手fields_for来提交多个记录以及现有记录,将它们解析为参数中的数组,并将所有缺少 ID 的记录创建为数据库中的新记录?我运气不好,我只需要手动生成所有字段名称吗?

4

8 回答 8

45

正如其他人所提到的,[]应该包含新记录的键,否则它会将散列与数组类型混合。您可以使用child_indexfields_for 上的选项进行设置。

f.fields_for :items, Item.new, child_index: "NEW_ITEM" # ...

我通常使用object_id代替来确保它是唯一的,以防有多个新项目。

item = Item.new
f.fields_for :items, item, child_index: item.object_id # ...

这是一个执行此操作的抽象辅助方法。这假设有一个部分名称item_fields将呈现。

def link_to_add_fields(name, f, association)
  new_object = f.object.send(association).klass.new
  id = new_object.object_id
  fields = f.fields_for(association, new_object, child_index: id) do |builder|
    render(association.to_s.singularize + "_fields", f: builder)
  end
  link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end

你可以像这样使用它。参数是:链接名称、父表单构建器以及父模型上的关联名称。

<%= link_to_add_fields "Add Item", f, :items %>

这里有一些 CoffeeScript 来监听那个链接的点击事件,插入字段,并用当前时间更新对象 id 给它一个唯一的键。

jQuery ->
  $('form').on 'click', '.add_fields', (event) ->
    time = new Date().getTime()
    regexp = new RegExp($(this).data('id'), 'g')
    $(this).before($(this).data('fields').replace(regexp, time))
    event.preventDefault()

该代码取自需要付费订阅的RailsCasts Pro 剧集。但是,在 GitHub 上可以免费获得完整的工作示例。

更新:我想指出插入child_index占位符并不总是必要的。如果您不想使用 JavaScript 动态插入新记录,您可以提前构建它们:

def new
  @project = Project.new
  3.times { @project.items.build }
end

<%= f.fields_for :items do |builder| %>

Rails 会自动为新记录插入索引,因此它应该可以正常工作。

于 2012-07-12T16:09:43.857 回答
23

所以,我对我最常看到的解决方案并不满意,即在服务器或客户端 JS 中为新元素生成伪索引。这感觉像是一个杂物,特别是考虑到 Rails/Rack 完全能够解析项目列表,只要它们都使用空括号 ( []) 作为索引。这是我最终得到的代码的近似值:

# note that this is NOT f.fields_for.
fields_for 'parent[children_attributes][]', child, index: nil do |f|
  f.label :name
  f.text_field :name
  # ...
end

以 结尾的字段名称前缀[],再加上index: nil选项,禁用索引生成 Rails 因此有助于尝试提供持久对象。此代码段适用于新对象和已保存对象。生成的表单参数,因为它们始终使用[],被解析为 中的数组params

params[:parent][:children_attributes] # => [{"name" => "..."}, {...}]

生成的Parent#children_attributes=方法可以accepts_nested_attributes_for :children很好地处理这个数组,更新更改的记录,添加新的记录(缺少"id"键的),并删除具有"_destroy"键集的记录。

我仍然很困扰 Rails 让这变得如此困难,我不得不恢复为硬编码的字段名称前缀字符串而不是使用 eg f.fields_for :children, index: nil。为了记录,即使执行以下操作:

f.fields_for :children, index: nil, child_index: nil do |f| ...

...未能禁用字段索引生成。

我正在考虑编写一个 Rails 补丁来简化此操作,但我不知道是否有足够多的人关心它,或者它是否会被接受。

编辑:用户@Macario 告诉我为什么 Rails 更喜欢在字段名称中使用显式索引:一旦您进入三层嵌套模型,就需要有一种方法来区分第三级属性属于哪个第二级模型。

于 2012-07-13T16:07:16.157 回答
14

常见的解决方案是在 [] 中添加一个占位符,并在将代码段插入表单时将其替换为唯一编号。时间戳在大部分时间都有效。

于 2012-07-12T07:20:17.413 回答
3

长帖删了

瑞安对此有一个插曲: http ://railscasts.com/episodes/196-nested-model-form-revised

看起来您需要手动生成唯一索引。Ryan 使用了object_id这个。

于 2012-07-12T08:05:23.573 回答
3

我在最近的所有项目中都遇到过这个用户案例,我希望这种情况会继续下去,正如 julian7 指出的那样,有必要在 [] 中提供一个唯一的 id。在我看来,这最好通过 js 完成。我一直在拖动和改进一个 jquery 插件来处理这种情况。它适用于现有记录并用于添加新记录,但需要一定的标记并且它会优雅地降级,这是代码和示例:

https://gist.github.com/3096634

使用插件的注意事项:

  1. fields_for 调用应包含在<fieldset>具有数据关联属性的数据关联属性中,该属性等于模型的复数名称,以及类“nested_models”。

  2. 在调用 fields_for 之前,应该在视图中构建一个对象。

  3. 对象字段本身应该<fieldset>用类“new”包装,但前提是记录是新的(不记得我是否删除了这个要求)。

  4. 标签内的“_destroy”属性的复选框必须存在,插件将使用标签文本创建销毁链接。

  5. 与“add_record”类的链接应该存在于 fieldset.nested_models 但在包含模型字段的字段集之外。

Appart from this nuisances 它一直在为我创造奇迹。
在检查了要点之后,这个要求必须更清楚。如果您改进代码或使用它,请告诉我:)。
顺便说一句,我受到 Ryan Bates 的第一个嵌套模型截屏视频的启发。

于 2012-07-12T08:23:58.870 回答
3

也许你应该作弊。将新记录放在不同的人造属性中,该属性是实际记录的装饰器。

parent[children_attributes][0][fieldname]
parent[new_children_attributes][][fieldname]

它不漂亮,但它应该工作。可能需要一些额外的努力来支持验证错误的表单往返。

于 2012-07-12T06:12:40.357 回答
1

有一种叫做茧的宝石可以做到这一点,我会选择一种更精简的 mor DIY 方法,但它是专门为这种情况而设计的。

于 2012-07-13T18:01:54.090 回答
1

我认为您可以通过将记录的 id 作为隐藏字段来使其工作

于 2012-07-12T07:50:27.343 回答