我有一个非常标准的用例 . 我有一个父对象和一个子对象列表 . 我希望有一个表格形式,我可以一次编辑所有的孩子,作为表中的行 . 我还希望能够插入一个或多个新行,并在提交时将它们创建为新记录 .
当我使用 fields_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)
"OK",我想 . “我只是确保所有字段都使用 []
表单而不是 [index]
表单 . 但是我无法弄清楚如何说服 fields_for
这样做一致 . 即使我给它一个明确的字段名称前缀和对象:
fields_for 'parent[children_attributes][]', child do |f| ...
只要 child
被持久化,它就会自动修改字段名,以便它们变为例如 parent[children_attributes][0][fieldname]
,同时将新记录的字段名保留为 parent[children_attributes][][fieldname]
. 再一次,Rack barfs .
我不知所措 . 我是如何使用像 fields_for
这样的标准Rails助手来提交多条新记录以及现有记录,将它们解析为params中的数组,并将所有缺少ID的记录创建为DB中的新记录?我不幸运,我只需手动生成所有字段名称?
8 回答
正如其他人所提到的,
[]
应该包含新记录的键,否则它会混合散列和数组类型 . 您可以使用fields_for上的child_index
选项进行设置 .我通常使用
object_id
执行此操作,以确保在有多个新项目时它是唯一的 .这是一个执行此操作的抽象辅助方法 . 这假设有一个部分名称为
item_fields
,它将呈现 .你可以像这样使用它 . 参数包括:链接的名称,父级的表单构建器以及父模型上的关联名称 .
这里有一些CoffeeScript用于侦听该链接的click事件,插入字段,并使用当前时间更新对象id以为其提供唯一键 .
该代码取自this RailsCasts Pro episode,需要付费订阅 . 但是,有一个完整的工作示例免费提供on GitHub .
Update: 我想指出插入
child_index
占位符并不总是必要的 . 如果您不想使用JavaScript动态插入新记录,可以提前构建它们:Rails会自动为新记录插入索引,因此它应该可以正常工作 .
所以,我对我经常看到的解决方案不满意,那就是在服务器或客户端JS上为新元素生成伪索引 . 这感觉就像一个kludge,特别是考虑到Rails / Rack完全能够解析项目列表这一事实,只要它们都使用空括号(
[]
)作为索引 . 这是我最后编写的代码的近似值:使用
[]
结束字段名称前缀,再加上index: nil
选项,禁用索引生成Rails,因此有助于尝试提供持久化对象 . 此代码段适用于新对象和已保存对象 . 由于它们始终使用[]
,因此生成的表单参数将被解析为params
中的数组:accepts_nested_attributes_for :children
生成的Parent#children_attributes=
方法处理此数组就好了,更新已更改的记录,添加新记录(缺少"id"
键的记录),并删除带有"_destroy"
键集的记录 .我仍然感到困扰的是Rails使得这非常困难,并且我不得不恢复到硬编码的字段名称前缀字符串而不是使用例如
f.fields_for :children, index: nil
. 为了记录,甚至做以下事情:...无法禁用字段索引生成 .
我正在考虑编写一个Rails补丁来使这更容易,但我不知道是否有足够的人关心或者它是否会被接受 .
编辑:用户@Macario已经让我了解为什么Rails更喜欢字段名称中的显式索引:一旦你进入三层嵌套模型,就需要是一种区分第三级属性属于哪个二级模型的方法 .
常见的解决方案是在[]中添加占位符,并在将代码段插入表单时将其替换为唯一编号 . 时间戳大部分时间都有效 .
也许你应该欺骗 . 将新记录放在另一个虚拟属性中,该属性是实际属性的装饰器 .
它不漂亮,但它应该工作 . 可能需要一些额外的努力来支持表单的往返验证错误 .
我在所有最后的项目中遇到过这个用户案例,我希望这会继续下去,正如julian7指出的那样,在[]中提供一个唯一的id是必要的 . 在我看来,通过js可以做得更好 . 我一直在拖动和改进一个jquery插件来处理这种情况 . 它适用于现有记录并添加新记录,但是需要一定的标记并且它会优雅地降级,下面是代码和示例:
https://gist.github.com/3096634
使用插件的注意事项:
fields_for调用应包装在
<fieldset>
中,其数据关联属性等于模型的复数名称,以及类'nested_models' .应该在调用fields_for之前在视图中构建一个对象 .
对象字段perse应该包含在
<fieldset>
中,类为"new",但仅当记录是新的时(如果我删除了此要求,则不记得) .必须存在标签内“_destroy”属性的复选框,插件将使用标签文本创建销毁链接 .
一个带有“add_record”类的链接应存在于fieldset.nested_models中,但在包含模型字段的fieldset之外 .
公寓从这种滋扰它为我的工作奇迹 .
在检查要点后,这个要求必须更加清晰 . 如果您改进代码或使用它,请告诉我:) .
顺便说一下,我的灵感来自Ryan Bates的第一个嵌套模特截屏视频 .
已删除长帖
瑞恩有一集关于此:http://railscasts.com/episodes/196-nested-model-form-revised
看起来您需要手动生成唯一索引 . Ryan使用了
object_id
.我认为你可以通过将记录的id包含为隐藏字段来使其工作
有一个名为cocoon的宝石这样做,我会选择更精简的摩托车DIY方法,但它是专门为这种情况而建的 .