首页 文章

如何通过Rails 3.2质量分配提交多个新项目

提问于
浏览
56

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

当我使用 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 回答

  • 45

    正如其他人所提到的, [] 应该包含新记录的键,否则它会混合散列和数组类型 . 您可以使用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用于侦听该链接的click事件,插入字段,并使用当前时间更新对象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()
    

    该代码取自this RailsCasts Pro episode,需要付费订阅 . 但是,有一个完整的工作示例免费提供on GitHub .

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

    def new
      @project = Project.new
      3.times { @project.items.build }
    end
    
    <%= f.fields_for :items do |builder| %>
    

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

  • 1

    所以,我对我经常看到的解决方案不满意,那就是在服务器或客户端JS上为新元素生成伪索引 . 这感觉就像一个kludge,特别是考虑到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更喜欢字段名称中的显式索引:一旦你进入三层嵌套模型,就需要是一种区分第三级属性属于哪个二级模型的方法 .

  • 23

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

  • 3

    也许你应该欺骗 . 将新记录放在另一个虚拟属性中,该属性是实际属性的装饰器 .

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

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

  • 1

    我在所有最后的项目中遇到过这个用户案例,我希望这会继续下去,正如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的第一个嵌套模特截屏视频 .

  • 3

    已删除长帖

    瑞恩有一集关于此:http://railscasts.com/episodes/196-nested-model-form-revised

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

  • 14

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

  • 3

    有一个名为cocoon的宝石这样做,我会选择更精简的摩托车DIY方法,但它是专门为这种情况而建的 .

相关问题