【问题标题】:Generating dynamic child rows in Rails partial在 Rails 部分中生成动态子行
【发布时间】:2015-07-19 12:01:59
【问题描述】:

我已经使用nested attributes Railscast 作为指导实现了一个嵌套的属性形式。因此,用户可以单击图标将“子”行动态添加到我的视图中。

不幸的是,我只能为我视图中的最后一个图标(插图here)进行这项工作。这个图标是在我的视图中生成的,但其他图标是在用于渲染每一行的部分中生成的。

可以这样做吗?如果是这样,最好的方法是什么?

这是我最近的尝试。

has_many 插槽。在工作表编辑视图中,我使用工作表表单构建器 (sheet) 来渲染我的插槽部分并将其传递给帮助器 link_to_add_fields,该帮助器呈现一个链接,单击时将生成一个新行(这部分工作正常) .您会注意到我还尝试将sheet 传递给部分,以便我可以从那里调用link_to_add_fields,但这就是它崩溃的地方:

视图 - edit.html.haml:

= sheet.fields_for :slots do |builder|
  = render 'slots/edit_fields', f: builder, sheet:sheet
= link_to_add_fields image_tag("plus.jpg", size:"18x18", alt:"Plus"), sheet, :slots, 'slots/edit'

部分-_edit_fields.html.haml:

- random_id = SecureRandom.uuid
.row.signup{:id => "edit-slot-#{random_id}"}
  .col-md-1
    %span.plus-icon
      = link_to_add_fields image_tag("plus.jpg", size:"18x18", alt:"Plus"), sheet, :slots, 'slots/edit'
    %span.minus-icon
      = image_tag "minus.jpg", size:"18x18", alt:"Minus"
  .col-md-2= f.text_field :label
  ... other fields ...

辅助方法:

def link_to_add_fields(name, f, association, partial)
  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(partial.to_s.singularize + "_fields", f: builder, name: name)
  end
  link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end

我在从局部调用助手时得到undefined local variable or method 'sheet'。基本上,我需要在每个链接上都可以使用工作表(父)表单构建器,以便助手工作。或者我需要放弃这种方法并使用 AJAX(也尝试过)。

更新

经过一番调试,很明显sheet 正在传递给部分。根本问题是我似乎在设置一个无休止的递归:

  1. 部分调用link_to_add_fields,以便我的+ 图标可以用作“添加子”链接。
  2. link_to_add_fields 呈现部分,以便在按下 + 图标时可以生成字段。

我遇到的另一个问题是,当渲染原始子代时,它们会在属性集合中获得顺序索引(0、1、2、...)。因此,即使我找到了一种在原始数据中呈现新子行的方法,我也不确定在没有大量 jQuery 体操之类的情况下提交表单时如何保持子行的顺序。

【问题讨论】:

  • 你能分享你为点击添加按钮编写的部分代码和js吗?我觉得只是有一些问题。
  • 好的,我已经添加了代码,但我完全不确定这是不是正确的方法。
  • 我想我明白你想要做什么。不将此作为答案发布,因为它不是。也许只保留最后一个+ 按钮,但添加一种定位元素的方式?喜欢:jqueryui.com/sortable 我相信这会解决您的问题并添加更多功能。我过去是手动完成的。但是有一个 Rails 演员:railscasts.com/episodes/147-sortable-lists
  • 谢谢@DickieBoy。我查看了 Railscast 和 jQuery 链接。这绝对是一个有趣的选择,但如果可能的话,我想让我的 UI 工作。这是一个 prod 应用程序(用 PHP 编写),当我移植到 RoR 时,我不想让我的用户感到困惑。
  • 您的部分文件名中是否有前置下划线? edit_fields.haml.html 是否真的命名为 _edit_fields.haml.html?因为它需要为了让rails正确处理它

标签: ruby-on-rails nested-attributes railscasts ruby-on-rails-4.2


【解决方案1】:

我最终用 jQuery 解决了这个问题。不是超级优雅但非常有效。基本上,我只是为 + 图标添加了一个单击处理程序,该处理程序构建了新行并将其插入到需要的位置(基本上只是删除了复制 Rails 生成的 HTML 所需的 jQuery)。以防我的用例在将来对其他人有所帮助,这里是 jQuery(请参阅原始帖子中的 imgur 链接以供参考):

//click handler to add edit row when user clicks on the plus icon
$(document).on('click', '.plus-icon', function (event) {

  //build a new row
  var one_day = 24 * 3600 * 1000;
  var d = new Date();
  var unique_id = d.getTime() % one_day + 10000;            // unique within a day and offset past original IDs
  var new_div =
    $('<div/>', {'class': 'row signup'}).append(
      $('<div/>', {'class': 'col-md-1'}).append(
        $('<span/>', {'class': 'plus-icon'}).append(
          '<img alt="Plus" src="/assets/plus.jpg" width="18" height="18" />'
        )
      ).append(
        $('<span/>', {'class': 'minus-icon'}).append(
          '<img alt="Minus" src="/assets/minus.jpg" width="18" height="18" />'
        )
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][label]" id="sheet_slots_attributes_'+unique_id+'_label">'
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][name]" id="sheet_slots_attributes_'+unique_id+'_name">'
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][email]" id="sheet_slots_attributes_'+unique_id+'_email">'
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][phone]" id="sheet_slots_attributes_'+unique_id+'_phone">'
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][comments]" id="sheet_slots_attributes_'+unique_id+'_comments">'
      )
    );
  var new_input = 
    '<input id="sheet_slots_attributes_'+unique_id+'_id" type="hidden" name="sheet[slots_attributes]['+unique_id+'][id]" value="'+unique_id+'">';

  //insert new row before clicked row
  $(new_div).insertBefore($(this).parent().parent());
  $(new_input).insertBefore($(this).parent().parent());
});

我在这里被困了一段时间......提醒一下,通常有多种方法可以解决问题,重要的是不要被一个想法困住。感谢大家的建议和意见。

【讨论】:

  • 很高兴你把它整理好了!我可以建议您将输入隐藏在页面上(或在 js 中),然后用 js 复制它,然后替换 ids。它将减少该函数中的大量代码,并使嵌套表单的维护变得更容易。
  • 谢谢@Dickie。所以你是说我应该复制一个现有的行,而不是在 JS/JQ 中构建新行?我看到一篇 SO 帖子解释了如何使用 .clone 执行此操作 - 这就是您的想法吗?听起来很有希望……可以列入“重构日”的清单
  • 差不多。我不会用现有的行来做。使用部分渲染一个新的 slot 对象并克隆它。
【解决方案2】:

这听起来非常接近几个月前我必须解决的问题。

如果我的回答正确,您的用户有一个“事物”列表,并且您希望您的用户能够使用正在显示的表单添加另一个“事物”。

我是这样处理的。我的“东西”是帝国,用户可以更改名称、删除帝国或添加新帝国,以管理他们的个人数据,例如更改他们的电子邮件地址和密码。

<%= form_for @user do |f| %>
<%= render layout: "users/partials/fields",
                 locals: {f: f} do %>

<b>Empires</b><br>
  <% n = 0 %>
  <%= f.fields_for :empires do |empire_form, index| %>
    <% n += 1 %>
    <%= empire_form.label :name, class: 'ms-indent' %>
    <%= empire_form.text_field :name, class: 'form_control' %>
    <%= empire_form.label :_destroy, 'Delete' %>
    <%= empire_form.check_box :_destroy %>
    <br>
  <% end %>

  <!-- Empty field for new Empire -->
  <b class='ms-indent'>Name</b>
    <input class="form_control" value="" name="user[empires_attributes][<%=n+1%>][name]" id="user_empires_attributes_0_name" type="text"> <b>new</b>
  <br>

<% end %>
  <%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>

有趣的是获取现有帝国数量的计数器(我不记得为什么我不只使用大小或长度)并在 html 输入字段中使用“n+1”以确保包含新帝国在提交的表格中,带有正确的 ID。注意 html 输入标签的使用,Rails 没有等价物。

[只是根据 DickieBoys 的评论补充说明。这种“n+1”方法有效,但可以用随机数代替,仍然有效。我打算稍后对此进行试验,但它确实有效,所以我并不着急。]

部分“字段”看起来像这样,注意中间的“产量”。

<%= f.label :name %>
<%= f.text_field :name, class: 'form-control', autofocus: true %>

<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>

<%= yield %>

<%= f.label :password %>
<%= f.password_field :password, class: 'form-control', value: "" %>

<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>

“字段”部分包括一个包含我的帝国代码的 do-end 块。部分包括这个块,它声明'yield'。

最后,更新控制器代码。

def update
    @user = User.find(params[:id])
    updated = false
    begin
      updated = @user.update_attributes(user_params)
    rescue ActiveRecord::DeleteRestrictionError
      flash[:warning] = "Empire contains ships. Delete these first."
    end 
    if updated
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end

我不会假装这是唯一的解决方案,甚至不一定是最好的。但是花了几天时间才搞定(Google 和 * 让我失望了!)并且从那时起一直可靠地工作。真正的关键是放弃寻找添加新帝国的 Rails 解决方案的尝试,而只需为新帝国使用带有新 id 的 html。你也许可以改进它。希望对您有所帮助。

【讨论】:

  • 我认为您的代码有问题。我想你的结构 User-&gt;*Empire (用户 has_many 帝国。)用户 1 有一个帝国 id 1。用户 2 有一个帝国 id 2。用户 1 的表单现在包含一个带有 @ 的输入987654328@我不确定提交时会做什么(可能没问题,并通过验证处理新的或记录)。在这种情况下,您想要做的是使用某种随机 id,从问题中您可以看到@steveklein 正在使用new_object.object_id,这很好。注意.object_id.id 不同。
  • 大声笑,它已经生产了几个月没有问题。但你是对的。我只是查看了 db 数据,似乎 update_attributes 忽略了提供的 :id 并分配了一个唯一的 id(也一样)。我暂时不打算更改我的生产代码,但我已将其添加到我的维护任务中。同时它没有坏,只是需要一些按摩。很好看!
  • 我认为它如果你有类似的东西:用户有3个帝国,ids [1,3]。新输入的 id 为 3。因此提交的参数看起来像(ish)user[empires_attributes][3][name] = "foo"user[empires_attributes][3][name] = "bar"。浏览器/服务器(不记得哪个)将覆盖第一个 name 提交给:user[empires_attributes][3][name] = "bar" 这意味着您将只更新 id 3 而永远无法创建新的。
  • 如果它按照我最初的预期执行,是的。但是,由于它正在分配唯一的 id,这并没有发生。在刚才的测试中,我向周五创建的虚拟用户添加了两个帝国,这些帝国得到了唯一的 id; 2124, 2128, 2129。似乎只是将 n+1 视为要忽略的占位符。
  • 不清楚这是否解决了我的用例@Matt。正如我认为从我的问题中的图像中可以清楚地看到的那样,我需要能够从列表中的任何孩子生成一个新的“孩子”(在你的情况下是帝国),以便用户直观地在特定位置添加新孩子(职位在我的申请中很重要)。
【解决方案3】:

undefined local variable or method 'sheet'

是由您渲染部分的方式引起的。

= render 'slots/edit_fields', f: builder, sheet:sheet

不足以将变量传递给部分。你需要:

= render partial: 'slots/edit_fields', locals: {f: builder, sheet:sheet}

这将使 f 在您的部分中可用。

【讨论】:

  • 如果您在此之后遇到更多问题,请更新问题。
  • 谢谢@DickieBoy。我目前正在处理其他事情,但很快就会回来并让您知道。
  • @DickieBoy 的回答很好。附带说明一下,如果您发现自己在这方面遇到更多困难,请查看 cocoon gem 以获取简单的嵌套关联表格:github.com/nathanvda/cocoon
  • 感谢本的提示。我看过 gem,甚至玩过简单的表单演示,但它是传统的嵌套表单(我工作得很好),在子列表末尾有一个“添加子”链接。我希望能够从子列表中的任何位置生成一个新子。
  • @DickieBoy - 感谢您的帮助,但您的回答并未解决问题的根本问题。另外,我不认为你说的是​​真的——我只是运行了一个简单的测试来渲染一个带有 = render 'foo_thing', x: 'bar', y: 'baz' 的部分,并验证了 xy 都传递给了部分。