Rails:如何使用索引在fields_for中循环?

111

有没有一种方法(或者类似的功能)可以使用fields_for_with_index

例如:

<% f.fields_for_with_index :questions do |builder, index| %>  
  <%= render 'some_form', :f => builder, :i => index %>
<% end %>

正在呈现的部分需要知道在fields_for循环中当前索引是什么。


3
我想在核心Rails中加入这个功能...我也经常遇到这种情况。 - Nathan Bertram
9个回答

164
答案非常简单,因为解决方案在Rails中已经提供。您可以使用f.options参数。因此,在渲染的_some_form.html.erb文件中,可以这样访问索引:
<%= f.options[:child_index] %>

你不需要做任何其他事情。


更新:看起来我的答案不够清楚...

原始的HTML文件:

<!-- Main ERB File -->
<% f.fields_for :questions do |builder| %>  
  <%= render 'some_form', :f => builder %>
<% end %>

子表单呈现:

<!-- _some_form.html.erb -->
<%= f.options[:child_index] %>

11
这里无法运行,你能提供Rails文档链接吗? - Lucas Renan
2
@LucasRenan和@graphmeter - 请再次仔细阅读问题,您需要在呈现的子表单中(在本例中为:_some_form.html.erb)调用<%= f.options[:child_index] %>,而不是在原始的“builder”中。回答已更新以提供更多说明。 - Sheharyar
1
奇怪,我得到了 nil - bcackerman
1
@Sheharyar 这在Rails 4中是有效的。但是它给我一些像“1450681048049,1450681050158,1450681056830,1450681141951,1450681219262”这样的值。但我需要以这种形式的索引:“1,2,3,4,5”。我该怎么办? - vidal
2
太棒了。这绝对应该是被接受的答案。 - jeffdill2
显示剩余4条评论

123

2
这个方法可行,而且比 project_fields.options[:child_index] 更简洁。所以我更喜欢这种方式! - HighHopes
它运行得很好。有没有使用索引+1的选项?因为它从零开始。 - Carlos Morales
1
这是最干净的答案,使用了Rails的功能而不是绕过的方法。 - undefined

94

下面的回答是多年前发布的,现代方法请参见: https://dev59.com/G2445IYBdhLWcg3wcaAN#22640703


实际上,以下方法更好,更贴近Rails文档:

<% @questions.each.with_index do |question,index| %>
    <% f.fields_for :questions, question do |fq| %>  
        # here you have both the 'question' object and the current 'index'
    <% end %>
<% end %>

来源于: http://railsapi.com/doc/rails-v3.0.4/classes/ActionView/Helpers/FormHelper.html#M006456

你也可以指定要使用的实例:

  <%= form_for @person do |person_form| %>
    ...
    <% @person.projects.each do |project| %>
      <% if project.active? %>
        <%= person_form.fields_for :projects, project do |project_fields| %>
          Name: <%= project_fields.text_field :name %>
        <% end %>
      <% end %>
    <% end %>
  <% end %>

2
我希望fields_for内置了这个功能,但由于没有,你的答案救了我的一天。谢谢。 - Anders Kindberg
1
如果您需要更多控制哪个索引被呈现,您还可以在fields_for上使用未记录的选项:child_index,例如:fields_for(:projects, project, child_index: index)。 - Anders Kindberg
这是可用的Rails API链接:http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-fields_for - Archonic
10
Rails 4.0.2+用户应该查看Ben的回答,因为“index”现在已经内置于生成器中。 - notapatch
1
@Marco先生,您是王者啊!感谢您救了我的一天! :-) 希望我能够给您几个十+......! - user5084534
1
这个答案应该被编辑并更新,因为notapatch说了。同时加上Ben的答案。 - JudgeProphet

17

适用于Rails 4或以上版本

<%= form_for @person do |person_form| %>
  <%= person_form.fields_for :projects do |project_fields| %>
    <%= project_fields.index %>
  <% end %>
<% end %>

针对Rails 3的猴子补丁支持

为了使得f.index在Rails 3中正常工作,您需要在项目初始化程序中添加一个猴子补丁来为fields_for添加这个功能。

# config/initializers/fields_for_index_patch.rb

module ActionView
  module Helpers
    class FormBuilder

      def index
        @options[:index] || @options[:child_index]
      end

      def fields_for(record_name, record_object = nil, fields_options = {}, &block)
        fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
        fields_options[:builder] ||= options[:builder]
        fields_options[:parent_builder] = self
        fields_options[:namespace] = options[:namespace]

        case record_name
          when String, Symbol
            if nested_attributes_association?(record_name)
              return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
            end
          else
            record_object = record_name.is_a?(Array) ? record_name.last : record_name
            record_name   = ActiveModel::Naming.param_key(record_object)
        end

        index = if options.has_key?(:index)
                  options[:index]
                elsif defined?(@auto_index)
                  self.object_name = @object_name.to_s.sub(/\[\]$/,"")
                  @auto_index
                end

        record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]"
        fields_options[:child_index] = index

        @template.fields_for(record_name, record_object, fields_options, &block)
      end

      def fields_for_with_nested_attributes(association_name, association, options, block)
        name = "#{object_name}[#{association_name}_attributes]"
        association = convert_to_model(association)

        if association.respond_to?(:persisted?)
          association = [association] if @object.send(association_name).is_a?(Array)
        elsif !association.respond_to?(:to_ary)
          association = @object.send(association_name)
        end

        if association.respond_to?(:to_ary)
          explicit_child_index = options[:child_index]
          output = ActiveSupport::SafeBuffer.new
          association.each do |child|
            options[:child_index] = nested_child_index(name) unless explicit_child_index
            output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
          end
          output
        elsif association
          fields_for_nested_model(name, association, options, block)
        end
      end

    end
  end
end

到目前为止,更好的答案是使用.index,这样会更简洁。 - Lucas Andrade

7

请查看渲染一组局部视图。如果您的需求是模板需要迭代一个数组,并为每个元素呈现一个子模板。

<%= f.fields_for @parent.children do |children_form| %>
  <%= render :partial => 'children', :collection => @parent.children, 
      :locals => { :f => children_form } %>
<% end %>

这将渲染“_children.erb”并将本地变量'children'传递给模板进行显示。迭代计数器将自动提供给模板,名称形式为partial_name_counter。在上面的示例中,模板将使用children_counter

希望这可以帮助到您。


当我这样做时,会出现“未定义的本地变量或方法'question_comment_form_counter'”的错误。question_comment_form是我的部分名称... - Shpigford
问题模型有多个评论(has_many :comments),在新建或编辑问题时,如何构建评论?尝试执行 1.times { question.comments.build }。 - Syed Aslam

5

我发现在Rails提供的方式中,至少在-v3.2.14版本中,没有一个像样的方法可以解决这个问题。

@Sheharyar Naseer提到了可以使用选项哈希来解决这个问题,但是我认为他所建议的方式并不能实现。

我尝试了如下做法 =>

<%= f.fields_for :blog_posts, {:index => 0} do |g| %>
  <%= g.label :gallery_sets_id, "Position #{g.options[:index]}" %>
  <%= g.select :gallery_sets_id, @posts.collect  { |p| [p.title, p.id] } %>
  <%# g.options[:index] += 1  %>
<% end %>

或者

<%= f.fields_for :blog_posts do |g| %>
  <%= g.label :gallery_sets_id, "Position #{g.object_name.match(/(\d+)]/)[1]}" %>
  <%= g.select :gallery_sets_id, @posts.collect  { |p| [p.title, p.id] } %>
<% end %>

在我的情况下,g.object_name 返回一个字符串,类似于这样的 "gallery_set[blog_posts_attributes][2]",对于第三个呈现的字段,所以我只需要匹配该字符串中的索引并使用它。


实际上,更酷(也可能更干净?)的方法是传递一个 lambda 并调用它来增加。

# /controller.rb
index = 0
@incrementer = -> { index += 1}

在视图中

<%= f.fields_for :blog_posts do |g| %>
  <%= g.label :gallery_sets_id, "Position #{@incrementer.call}" %>
  <%= g.select :gallery_sets_id, @posts.collect  { |p| [p.title, p.id] } %>
<% end %>

2

已添加到 fields_for 子索引:0

<%= form_for @person do |person_form| %>
  <%= person_form.fields_for :projects, child_index: 0 do |project_fields| %>
    <%= project_fields.index %>
  <% end %>
<% end %>

这是最新的最佳答案。 - genkilabs
有其他人遇到这个问题出现了重复的字段吗? - user266647

1

我知道可能有点晚了,但最近我必须这样做,你可以像这样获取fields_for的索引

<% f.fields_for :questions do |builder| %>
  <%= render 'some_form', :f => builder, :i => builder.options[:child_index] %>
<% end %>

我希望这可以帮到你 :)

0
如果您想要控制索引,请查看index选项。
<%= f.fields_for :other_things_attributes, @thing.other_things.build do |ff| %>
  <%= ff.select :days, ['Mon', 'Tues', 'Wed'], index: 2 %>
  <%= ff.hidden_field :special_attribute, 24, index: "boi" %>
<%= end =>

这将产生

<select name="thing[other_things_attributes][2][days]" id="thing_other_things_attributes_7_days">
  <option value="Mon">Mon</option>
  <option value="Tues">Tues</option>
  <option value="Wed">Wed</option>
</select>
<input type="hidden" value="24" name="thing[other_things_attributes][boi][special_attribute]" id="thing_other_things_attributes_boi_special_attribute">

如果表单已提交,参数将包含类似以下内容

{
  "thing" => {
  "other_things_attributes" => {
    "2" => {
      "days" => "Mon"
    },
    "boi" => {
      "special_attribute" => "24"
    }
  }
}

我不得不使用索引选项来使我的多下拉框正常工作。祝你好运。


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