在Rails partial中生成动态子行

7
我使用嵌套属性Railscast作为指南,实现了一个嵌套属性表单。因此,用户可以点击图标动态添加“子”行到我的视图中。
不幸的是,我只能使最后一个图标在我的视图中工作(如这里所示)。这个图标是在我的视图中生成的,但其他图标是在用于呈现每一行的局部中生成的。
这可行吗?如果可以,最好的方法是什么?
这是我的最新尝试。
Sheet has_many Slots。在sheet编辑视图中,我使用一个sheet表单构建器(sheet)来呈现我的slot部分,并将其传递给一个helperlink_to_add_fields,当单击时会生成一个新的行(这部分工作正常)。你会注意到我还试图将sheet传递到partial中,以便我可以从那里调用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'
< p > 部分内容 - _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

我在调用partial中的helper时遇到了“未定义的本地变量或方法'sheet'”错误。基本上,我需要将父表单构建器(sheet)对于每个链接都可用以使helper正常工作。或者我需要放弃这种方法并使用AJAX(也尝试过)。更新后,经过一些调试,很明显'sheet'被传递到了partial中。根本问题是我似乎设置了无限递归:1. Partial调用link_to_add_fields,以便我的“+”图标可以作为“添加子项”的链接。2. link_to_add_fields呈现partial,以便在按下“+”图标时生成字段。我遇到的另一个问题是当原始子项被呈现时,它们在属性集合中获得连续的索引(0、1、2...)。因此,即使我找出了在原始子项中呈现新子项的方法,我不确定如何在提交表单时保持子项的顺序,除非进行大量的jQuery操作或其他操作。

你能分享一下你写的点击添加按钮的部分代码和js吗?我感觉只是有些问题。 - Pardeep Dhingra
感谢@DickieBoy。我查看了Railscast和jQuery链接。这绝对是一个有趣的选项,但如果可能的话,我想让我的用户界面按照所示工作。这是一个生产应用程序(用PHP编写),当我迁移到RoR时,我不想混淆我的用户。 - steve klein
您的partials文件名前是否有下划线? _edit_fields.haml.html 文件是否实际命名为 edit_fields.haml.html?因为这样才能使rails正确处理它。 - PhilVarg
很烦人。只是为了明确一下,+按钮会添加一行,但它会添加在底部?你想要它在点击的+按钮上面的一行吗?如果是这样,请钩入从gem触发的nested:fieldAdded js事件。您应该能够获取事件的目标。它的index-1是您想要的新行,它将是列表中的最后一行。 - DickieBoy
@PhilVarg 是的,他们有。上面的错别字已经修正了。 - steve klein
显示剩余7条评论
3个回答

5

未定义的本地变量或方法'sheet'

是由于您呈现局部视图的方式引起的。

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

不能足以将变量传递给局部视图。您需要:

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

这将使f在您的局部视图中可用。


如果您在此之后遇到更多问题,请更新问题。 - DickieBoy
谢谢@DickieBoy。我目前正在处理其他事情,但很快会回来并告诉你结果。 - steve klein
@DickieBoy的回答很好。另外,如果你在这方面遇到更多困难,可以查看cocoon gem,它提供了易于嵌套关联表单的功能:https://github.com/nathanvda/cocoon - Ben Eggett
谢谢你的建议,Ben。我已经查看了这个宝石,并尝试了简单表单演示,但它只是传统的嵌套表单(我已经成功实现了),在子列表末尾只有一个“添加子项”的链接。我希望能够从子列表的任何位置生成新的子项。 - steve klein
@DickieBoy - 我很感激你的帮助,但是你的回答没有解决问题的根本问题。此外,我不认为你所说的是正确的 - 我刚刚运行了一个简单的测试,使用 = render 'foo_thing',x:'bar',y:'baz' 渲染了一个部分,并验证了 xy 都传递给了部分。 - steve klein
@steveklein,你能够设置一个演示应用程序,只包含复制问题所需的最少内容吗?我觉得我可能漏掉了一些东西。 - DickieBoy

4

我最终使用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。这将减少此函数中的大量代码,并使维护嵌套表单更容易。 - DickieBoy
谢谢@Dickie。那么您的意思是,我应该复制现有行而不是在JS/JQ中构建新行?我看到了一个SO帖子,解释了如何使用.clone来实现这一点 - 这是您想要的吗?听起来很有前途...可以列入“重构日”清单。 - steve klein
基本上是这样。我不会在现有行中这样做。使用部分渲染一个新的“slot”对象并克隆它。 - DickieBoy

1
这听起来与我几个月前需要解决的问题非常相似。如果我的理解正确,您的用户有一个“事物”列表,希望能够使用显示的表单添加另一个“事物”。我的解决方法是这样的:我的“事物”是帝国,用户可以更改名称、删除帝国或添加新帝国,在管理他们的个人数据(如更改电子邮件地址和密码)的上下文中进行操作。
<%= 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 %>

有趣的部分是计数器用于获取现有帝国数量(我记不清为什么我没有使用size或length),并在html输入字段中使用“n + 1”以确保新帝国包含在提交的表单中,并具有其正确的id。请注意,Rails没有等效的功能,因此要使用html输入标签。
[鉴于DickieBoys的评论,需要额外说明的是,“n + 1”方法可行,但可以替换为随机数字而仍然有效。我打算稍后进行实验,但它现在可行,所以我不急于更改。]
局部“fields”的外观如下,注意中间的“yield”。
<%= 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' %>

'fields' 部分包括一个 do-end 代码块,其中包含我的 empires 代码。该部分在 '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和StackOverflow都让我失望了!),自那以后就一直可靠地工作。关键是放弃寻找Rails解决方案来添加新帝国,而只使用具有新id的html来添加新帝国。您可能可以对其进行改进。希望它有所帮助。

1
我认为你的代码有问题。 我想象你的结构是 User->*Empire(用户has_many empires)。 用户1拥有一个带有id 1的帝国。 用户2拥有一个带有id 2的帝国。 你现在为用户1的表单包含了一个输入,其中包含user[empires_attributes][2][name]。 当提交时,我不确定这会发生什么(这可能是可以的,并且可以处理新的或记录进行验证)。 在这种情况下,你想要做的是使用某种随机ID,从问题中可以看到@steveklein正在使用new_object.object_id,这是可以的。 注意.object_id.id不同。 - DickieBoy
哈哈,它已经在生产环境运行了几个月没有出现问题。但你是正确的。我刚刚查看了数据库数据,update_attributes似乎忽略了提供的:id,并分配了一个独特的id(同样好)。我现在不打算立即更改我的生产代码,但已将其添加到我的维护任务中。同时,它并没有坏,只需要一些调整。发现得真好! - Matt Stevens
1
如果你有像这样的东西:用户有3个帝国,id为[1,3]。新输入将具有id 3。因此,提交的参数看起来像是user[empires_attributes][3][name] = "foo"user[empires_attributes][3][name] = "bar"。浏览器/服务器(记不清是哪一个)将覆盖第一个提交的“名称”为:user[empires_attributes][3][name] = "bar",这意味着你只会更新id 3,永远无法创建新的id。我认为如果你这样做,它出问题。 - DickieBoy
如果按照我最初的意图执行,是的。但由于它正在分配唯一的ID,这并没有发生。刚刚在测试中,我向上周五创建的一个虚拟用户添加了两个帝国,这些帝国获得了唯一的ID:2124、2128、2129。它似乎只是将n+1视为要忽略的占位符。 - Matt Stevens
不清楚这是否符合我的使用情况@Matt。正如我在问题中的图像所示,我需要能够从列表中的任何子项生成一个新的“子项”(在您的情况下是帝国),以便用户直观地在特定位置添加新的子项(在我的应用程序中位置很重要)。 - steve klein

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接