我有一个非常标准的用例。我有一个父对象和一个子对象列表。我想要一个表格形式,我可以一次编辑所有子项,作为表中的行。我还希望能够插入一个或多个新行,并在提交时将它们创建为新记录。
当我使用 fields_for
为由 has-many 相关的嵌套记录呈现一系列子表单时,rails 会生成字段名称,例如 parent[children_attributes][0][fieldname]
、parent[children_attributes][1][fieldname]
等等。
这会导致 Rack 解析如下所示的参数哈希:
{ "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]
。再一次,机架吠叫。
我不知所措。我到底如何使用像fields_for
这样的标准 Rails 助手来提交多个新记录以及现有记录,将它们解析为参数中的数组,并将所有缺少 ID 的记录创建为数据库中的新记录?我运气不好,我只需要手动生成所有字段名称吗?
正如其他人所提到的,[]
应该包含新记录的键,否则它会将哈希与数组类型混合在一起。您可以使用fields_for上的child_index
选项进行设置。
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 将自动为新记录插入索引,因此它应该可以正常工作。
因此,我对最常看到的解决方案不满意,即在服务器或客户端 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" => "..."}, {...}]
accepts_nested_attributes_for :children
生成的Parent#children_attributes=
方法可以很好地处理此数组,更新更改的记录,添加新记录(缺少"id"
键的记录(,并删除具有"_destroy"
键集的记录。
我仍然为Rails使这变得如此困难而烦恼,并且我不得不恢复为硬编码的字段名称前缀字符串,而不是使用例如 f.fields_for :children, index: nil
.作为记录,即使执行以下操作:
f.fields_for :children, index: nil, child_index: nil do |f| ...
。无法禁用字段索引生成。
我正在考虑编写一个 Rails 补丁来简化此操作,但我不知道是否有足够多的人关心它,或者它是否会被接受。
编辑:用户@Macario已经告诉我为什么Rails更喜欢字段名称中的显式索引:一旦你进入三层嵌套模型,就需要有一种方法来区分第三级属性属于哪个二级模型。
常见的解决方案是在 [] 中添加一个占位符,并在将代码段插入表单时将其替换为唯一编号。时间戳大部分时间都有效。
也许你应该作弊。将新记录放在不同的人造属性中,该属性是实际记录的装饰器。
parent[children_attributes][0][fieldname]
parent[new_children_attributes][][fieldname]
它不漂亮,但它应该有效。可能需要一些额外的努力来支持对验证错误的表单进行往返。
我在最后的所有项目中都遇到过这个用户案例,我希望这种情况能继续下去,正如 julian7 指出的那样,有必要在 [] 中提供一个唯一的 id。在我看来,这最好通过 js 来完成。我一直在拖动和改进一个 jquery 插件来处理这种情况。它适用于现有记录和添加新记录,但需要一定的标记并且会优雅地降级,以下是代码和示例:
https://gist.github.com/3096634
使用插件的注意事项:
-
fields_for调用应包装在数据关联属性等于模型复数名称的
<fieldset>
和类"nested_models"中。 -
应在调用 fields_for 之前在视图中生成对象。
-
对象字段 perse 应该包装在类"new"的
<fieldset>
中,但前提是记录是新的(不记得我是否删除了这个要求(。 -
标签内"_destroy"属性的复选框必须存在,插件将使用标签文本来创建销毁链接。
-
带有类"add_record"的链接应存在于fieldset.nested_models内,但存在于包含模型字段的字段集之外。
公寓从这个麻烦它一直在为我创造奇迹。
在检查要点后,此要求必须更加清晰。如果您改进了代码或:)使用它,请告诉我。
顺便说一句,我的灵感来自瑞安贝茨的第一个嵌套模型截屏视频。
长帖子已删除
瑞安对此有一集:http://railscasts.com/episodes/196-nested-model-form-revised
看起来您需要手动生成唯一索引。瑞安为此使用了object_id
。
我认为您可以通过将记录的 id 作为隐藏字段<</p>
叫做茧的宝石可以做到这一点,我会选择更精简的 mor DIY aproach,但它是专门为这种情况而构建的。