public
Description: Ruby on Rails
Homepage: http://rubyonrails.org
Clone URL: git://github.com/rails/rails.git
Fixed index and auto index for nested fields_for [#327 state:resolved]

Signed-off-by: Joshua Peek <josh@joshpeek.com>
kjg (author)
Sat Jul 19 13:08:53 -0700 2008
josh (committer)
Sat Jul 19 13:10:12 -0700 2008
commit  1b4b1aa725a4f44c3473ae99b36d7cededba2bea
tree    747fe8107d57a5b07ea268823b7e168e893073ab
parent  706425e154a2a2581195c98309f30a18a0002a58
...
528
529
530
531
 
532
533
534
 
 
535
536
537
...
708
709
710
711
 
712
713
714
...
726
727
728
 
 
 
 
 
 
 
729
730
731
...
738
739
740
 
 
 
 
 
 
 
 
 
741
742
743
 
744
745
746
 
747
748
749
750
 
751
752
753
...
528
529
530
 
531
532
 
 
533
534
535
536
537
...
708
709
710
 
711
712
713
714
...
726
727
728
729
730
731
732
733
734
735
736
737
738
...
745
746
747
748
749
750
751
752
753
754
755
756
757
758
 
759
760
761
 
762
763
764
765
 
766
767
768
769
0
@@ -528,10 +528,10 @@ module ActionView
0
 
0
       def initialize(object_name, method_name, template_object, object = nil)
0
         @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
0
-        @template_object= template_object
0
+        @template_object = template_object
0
         @object = object
0
-        if @object_name.sub!(/\[\]$/,"")
0
-          if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
0
+        if @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
0
+          if (object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
0
             @auto_index = object.to_param
0
           else
0
             raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
0
@@ -708,7 +708,7 @@ module ActionView
0
         end
0
 
0
         def sanitized_object_name
0
-          @sanitized_object_name ||= @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
0
+          @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
0
         end
0
 
0
         def sanitized_method_name
0
@@ -726,6 +726,13 @@ module ActionView
0
       def initialize(object_name, object, template, options, proc)
0
         @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
0
         @default_options = @options ? @options.slice(:index) : {}
0
+        if @object_name.to_s.match(/\[\]$/)
0
+          if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
0
+            @auto_index = object.to_param
0
+          else
0
+            raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
0
+          end
0
+        end
0
       end
0
 
0
       (field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
0
@@ -738,16 +745,25 @@ module ActionView
0
       end
0
 
0
       def fields_for(record_or_name_or_array, *args, &block)
0
+        if options.has_key?(:index)
0
+          index = "[#{options[:index]}]"
0
+        elsif defined?(@auto_index)
0
+          self.object_name = @object_name.to_s.sub(/\[\]$/,"")
0
+          index = "[#{@auto_index}]"
0
+        else
0
+          index = ""
0
+        end
0
+
0
         case record_or_name_or_array
0
         when String, Symbol
0
-          name = "#{object_name}[#{record_or_name_or_array}]"
0
+          name = "#{object_name}#{index}[#{record_or_name_or_array}]"
0
         when Array
0
           object = record_or_name_or_array.last
0
-          name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
0
+          name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
0
           args.unshift(object)
0
         else
0
           object = record_or_name_or_array
0
-          name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
0
+          name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
0
           args.unshift(object)
0
         end
0
 
...
6
7
8
9
 
10
11
12
...
22
23
24
 
25
26
27
...
30
31
32
33
34
35
36
...
447
448
449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
451
452
...
831
832
833
834
835
836
837
...
6
7
8
 
9
10
11
12
...
22
23
24
25
26
27
28
...
31
32
33
 
34
35
36
...
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
...
942
943
944
 
945
946
947
0
@@ -6,7 +6,7 @@ silence_warnings do
0
     alias_method :title_before_type_cast, :title unless respond_to?(:title_before_type_cast)
0
     alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast)
0
     alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast)
0
-    alias_method :secret?, :secret 
0
+    alias_method :secret?, :secret
0
 
0
     def new_record=(boolean)
0
       @new_record = boolean
0
@@ -22,6 +22,7 @@ silence_warnings do
0
     attr_reader :post_id
0
     def save; @id = 1; @post_id = 1 end
0
     def new_record?; @id.nil? end
0
+    def to_param; @id; end
0
     def name
0
       @id.nil? ? 'new comment' : "comment ##{@id}"
0
     end
0
@@ -30,7 +31,6 @@ end
0
 
0
 class Comment::Nested < Comment; end
0
 
0
-
0
 class FormHelperTest < ActionView::TestCase
0
   tests ActionView::Helpers::FormHelper
0
 
0
@@ -447,6 +447,117 @@ class FormHelperTest < ActionView::TestCase
0
     assert_dom_equal expected, output_buffer
0
   end
0
 
0
+  def test_nested_fields_for_with_nested_collections
0
+    form_for('post[]', @post) do |f|
0
+      concat f.text_field(:title)
0
+      f.fields_for('comment[]', @comment) do |c|
0
+        concat c.text_field(:name)
0
+      end
0
+    end
0
+
0
+    expected = "<form action='http://www.example.com' method='post'>" +
0
+               "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" +
0
+               "<input name='post[123][comment][][name]' size='30' type='text' id='post_123_comment__name' value='new comment' />" +
0
+               "</form>"
0
+
0
+    assert_dom_equal expected, output_buffer
0
+  end
0
+
0
+  def test_nested_fields_for_with_index
0
+    form_for('post', @post, :index => 1) do |c|
0
+      concat c.text_field(:title)
0
+      c.fields_for('comment', @comment, :index => 1) do |r|
0
+        concat r.text_field(:name)
0
+      end
0
+    end
0
+
0
+    expected = "<form action='http://www.example.com' method='post'>" +
0
+               "<input name='post[1][title]' size='30' type='text' id='post_1_title' value='Hello World' />" +
0
+               "<input name='post[1][comment][1][name]' size='30' type='text' id='post_1_comment_1_name' value='new comment' />" +
0
+               "</form>"
0
+
0
+    assert_dom_equal expected, output_buffer
0
+  end
0
+
0
+  def test_nested_fields_for_with_index
0
+    form_for(:post, @post, :index => 1) do |f|
0
+      f.fields_for(:comment, @post) do |c|
0
+        concat c.text_field(:title)
0
+      end
0
+    end
0
+
0
+    expected = "<form action='http://www.example.com' method='post'>" +
0
+               "<input name='post[1][comment][title]' size='30' type='text' id='post_1_comment_title' value='Hello World' />" +
0
+               "</form>"
0
+
0
+    assert_dom_equal expected, output_buffer
0
+  end
0
+
0
+  def test_nested_fields_for_with_index_on_both
0
+    form_for(:post, @post, :index => 1) do |f|
0
+      f.fields_for(:comment, @post, :index => 5) do |c|
0
+        concat c.text_field(:title)
0
+      end
0
+    end
0
+
0
+    expected = "<form action='http://www.example.com' method='post'>" +
0
+               "<input name='post[1][comment][5][title]' size='30' type='text' id='post_1_comment_5_title' value='Hello World' />" +
0
+               "</form>"
0
+
0
+    assert_dom_equal expected, output_buffer
0
+  end
0
+
0
+  def test_nested_fields_for_with_auto_index
0
+    form_for("post[]", @post) do |f|
0
+      f.fields_for(:comment, @post) do |c|
0
+        concat c.text_field(:title)
0
+      end
0
+    end
0
+
0
+    expected = "<form action='http://www.example.com' method='post'>" +
0
+               "<input name='post[123][comment][title]' size='30' type='text' id='post_123_comment_title' value='Hello World' />" +
0
+               "</form>"
0
+
0
+    assert_dom_equal expected, output_buffer
0
+  end
0
+
0
+  def test_nested_fields_for_with_auto_index_on_both
0
+    form_for("post[]", @post) do |f|
0
+      f.fields_for("comment[]", @post) do |c|
0
+        concat c.text_field(:title)
0
+      end
0
+    end
0
+
0
+    expected = "<form action='http://www.example.com' method='post'>" +
0
+               "<input name='post[123][comment][123][title]' size='30' type='text' id='post_123_comment_123_title' value='Hello World' />" +
0
+               "</form>"
0
+
0
+    assert_dom_equal expected, output_buffer
0
+  end
0
+
0
+  def test_nested_fields_for_with_index_and_auto_index
0
+    form_for("post[]", @post) do |f|
0
+      f.fields_for(:comment, @post, :index => 5) do |c|
0
+        concat c.text_field(:title)
0
+      end
0
+    end
0
+
0
+    form_for(:post, @post, :index => 1) do |f|
0
+      f.fields_for("comment[]", @post) do |c|
0
+        concat c.text_field(:title)
0
+      end
0
+    end
0
+
0
+    expected = "<form action='http://www.example.com' method='post'>" +
0
+               "<input name='post[123][comment][5][title]' size='30' type='text' id='post_123_comment_5_title' value='Hello World' />" +
0
+               "</form>" +
0
+               "<form action='http://www.example.com' method='post'>" +
0
+               "<input name='post[1][comment][123][title]' size='30' type='text' id='post_1_comment_123_title' value='Hello World' />" +
0
+               "</form>"
0
+
0
+    assert_dom_equal expected, output_buffer
0
+  end
0
+
0
   def test_fields_for
0
     fields_for(:post, @post) do |f|
0
       concat f.text_field(:title)
0
@@ -831,7 +942,6 @@ class FormHelperTest < ActionView::TestCase
0
     assert_dom_equal expected, output_buffer
0
   end
0
 
0
-
0
   protected
0
     def comments_path(post)
0
       "/posts/#{post.id}/comments"

Comments

glennpow Fri Jul 25 08:26:16 -0700 2008

I am attempting to utilize mass-assign for my models, but can’t get the update_attributes to work correctly when I have association_collections as child models. My create methods work fine, since the hash passes in an Array value for these collections. I.E.: emails => [ {...}, {...} ] ) But, when I try to edit/update my model, the field names in the form naturally have the :id of the email included in them, so that the resulting value passed to the mass-assign is not an Array, but a Hash of this form: emails => {“5” => {...}, “6” => {...}} Where the 5 and 6 are the :id values of the respective emails. This gets passed into the update_attributes, which eventually gets to: AssociationCollection.replace(other_array) which assumes “emails” to be an Array. Shouldn’t this replace method be “smarter” so that if a Hash is passed in, it will ascertain the :id values from it, and then reassign the model attributes accordingly? Furthermore, what would happen if there were a combination of updated models (emails) and perhaps one new email (that didn’t have an :id). Does the mass-assign functionality handle this situation? Any help would be appreciated. -Glenn

acook8103 Tue Oct 07 20:10:18 -0700 2008

Generation of HTML element id and form field name are inconsistent after 2nd level of nesting.


<% form_for([: inventory, @request]) do |f| %>
    <% line_item = @request.line_items.first %>
    <% f.fields_for "existing_line_item_attributes[]", line_item do |li| %>
      <%= li.text_field :equipment_type_id %>
      <% equipment_li = line_item.equipment_line_items.first %>
      <% li.fields_for "existing_equipment_line_item_attributes[]", equipment_li do |eli| %>
        <%= eli.text_field :equipment_id %>
      <% end %>
    <% end %>
<% end %>

Generates this:

<form action="/inventory/requests/350903690" class="edit_request" id="edit_request_350903690" method="post">
      <input id="request_existing_line_item_attributes_51161156_equipment_type_id" name="request[existing_line_item_attributes][51161156][equipment_type_id]" size="30" type="text" value="691081111" />

        <input id="request_existing_line_item_attributes___existing_equipment_line_item_attributes_860256615_equipment_id" name="request[existing_line_item_attributes[]][existing_equipment_line_item_attributes][860256615][equipment_id]" size="30" type="text" value="1029003634" />

</form>

You notice that the in the 3rd level that the 2nd level ID is missing and that the “[]” are enclosed within the first field.

I was able to get the the above form to work as expected by removing the brackets and explicitly setting the id.Generation of HTML element id and form field name are inconsistent after 2nd level of nesting.


<% form_for([:inventory, @request]) do |f| %>
    <% line_item = @request.line_items.first %>
    <% f.fields_for "existing_line_item_attributes", line_item, :index => line_item.id do |li| %>
      <%= li.text_field :equipment_type_id %>
      <% equipment_li = line_item.equipment_line_items.first %>
      <% li.fields_for "existing_equipment_line_item_attributes", equipment_li, :index => equipment_li.id do |eli| %>
        <%= eli.text_field :equipment_id %>
      <% end %>
    <% end %>
<% end %>

I perused the code, but am not familiar enough with things to spot anything.

Thanks for the original patch. Made my code less ugly.

acook8103 Tue Oct 07 20:14:37 -0700 2008

Textile Reference I followed said to do pre and code.

Let’s try just code so you can see the html. I also took out lt and gt.

form action="/inventory/requests/350903690" class="edit_request" id="edit_request_350903690" method="post"> input id="request_existing_line_item_attributes_51161156_equipment_type_id" name="request[existing_line_item_attributes][51161156][equipment_type_id]" size="30" type="text" value="691081111" / input id="request_existing_line_item_attributes___existing_equipment_line_item_attributes_860256615_equipment_id" name="request[existing_line_item_attributes[]][existing_equipment_line_item_attributes][860256615][equipment_id]" size="30" type="text" value="1029003634" / /form