From 9f0f7ec2223ae60ee6e2eab5bc9f036a480e9f81 Mon Sep 17 00:00:00 2001
From: Kasper Timm Hansen <kaspth@gmail.com>
Date: Sun, 18 Dec 2016 20:01:54 +0100
Subject: form_with: allow methods outside the model.

Has the handy effect of making the initial examples in the form_with
docs work too.

Had to do some finagling such that form_with's without a scope didn't
wrap their names in braces ala `[title]`.
---
 actionview/lib/action_view/helpers/form_helper.rb  | 14 +++-
 actionview/lib/action_view/helpers/tags/base.rb    | 17 ++++-
 .../test/template/form_helper/form_with_test.rb    | 81 ++++++++++++++++++----
 3 files changed, 96 insertions(+), 16 deletions(-)

diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index 4a52a69b7b..c446e5bc55 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -513,6 +513,17 @@ module ActionView
       #     <input type="text" name="post[title]" value="<the title of the post>">
       #   </form>
       #
+      #   # Though the fields don't have to correspond to model attributes:
+      #   <%= form_with model: Cat.new do |form| %>
+      #     <%= form.text_field :cats_dont_have_gills %>
+      #     <%= form.text_field :but_in_forms_they_can %>
+      #   <% end %>
+      #   # =>
+      #   <form action="/cats" method="post" data-remote="true">
+      #     <input type="text" name="cat[cats_dont_have_gills]">
+      #     <input type="text" name="cat[but_in_forms_they_can]">
+      #   </form>
+      #
       # The parameters in the forms are accessible in controllers according to
       # their name nesting. So inputs named +title+ and <tt>post[title]</tt> are
       # accessible as <tt>params[:title]</tt> and <tt>params[:post][:title]</tt>
@@ -700,6 +711,7 @@ module ActionView
       #     form_with(**options.merge(builder: LabellingFormBuilder), &block)
       #   end
       def form_with(model: nil, scope: nil, url: nil, format: nil, **options)
+        options[:allow_method_names_outside_object] = true
         options[:skip_default_ids] = true
 
         if model
@@ -1626,7 +1638,7 @@ module ActionView
       def initialize(object_name, object, template, options)
         @nested_child_index = {}
         @object_name, @object, @template, @options = object_name, object, template, options
-        @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids) : {}
+        @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {}
 
         convert_to_legacy_options(@options)
 
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index b8c446cbed..74d6324771 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -14,6 +14,7 @@ module ActionView
           @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
           @object = retrieve_object(options.delete(:object))
           @skip_default_ids = options.delete(:skip_default_ids)
+          @allow_method_names_outside_object = options.delete(:allow_method_names_outside_object)
           @options = options
           @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil
         end
@@ -26,7 +27,11 @@ module ActionView
         private
 
           def value(object)
-            object.public_send @method_name if object
+            if @allow_method_names_outside_object
+              object.public_send @method_name if object && object.respond_to?(@method_name)
+            else
+              object.public_send @method_name if object
+            end
           end
 
           def value_before_type_cast(object)
@@ -93,7 +98,10 @@ module ActionView
 
           def tag_name(multiple = false, index = nil)
             # a little duplication to construct less strings
-            if index
+            case
+            when @object_name.empty?
+              "#{sanitized_method_name}#{"[]" if multiple}"
+            when index
               "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
             else
               "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
@@ -102,7 +110,10 @@ module ActionView
 
           def tag_id(index = nil)
             # a little duplication to construct less strings
-            if index
+            case
+            when @object_name.empty?
+              sanitized_method_name.dup
+            when index
               "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
             else
               "#{sanitized_object_name}_#{sanitized_method_name}"
diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb
index c078b47e14..96b797992f 100644
--- a/actionview/test/template/form_helper/form_with_test.rb
+++ b/actionview/test/template/form_helper/form_with_test.rb
@@ -323,6 +323,75 @@ class FormWithActsLikeFormForTest < FormWithTest
     assert_dom_equal expected, output_buffer
   end
 
+  def test_form_with_only_url_on_create
+    form_with(url: "/posts") do |f|
+      concat f.label :title, "Label me"
+      concat f.text_field :title
+    end
+
+    expected = whole_form("/posts") do
+      '<label for="title">Label me</label>' +
+      '<input type="text" name="title">'
+    end
+
+    assert_dom_equal expected, output_buffer
+  end
+
+  def test_form_with_only_url_on_update
+    form_with(url: "/posts/123") do |f|
+      concat f.label :title, 'Label me'
+      concat f.text_field :title
+    end
+
+    expected = whole_form("/posts/123") do
+      '<label for="title">Label me</label>' +
+      '<input type="text" name="title">'
+    end
+
+    assert_dom_equal expected, output_buffer
+  end
+
+  def test_form_with_general_attributes
+    form_with(url: "/posts/123") do |f|
+      concat f.text_field :no_model_to_back_this_badboy
+    end
+
+    expected = whole_form("/posts/123") do
+      '<input type="text" name="no_model_to_back_this_badboy">'
+    end
+
+    assert_dom_equal expected, output_buffer
+  end
+
+  def test_form_with_attribute_not_on_model
+    form_with(model: @post) do |f|
+      concat f.text_field :this_dont_exist_on_post
+    end
+
+    expected = whole_form("/posts/123", method: :patch) do
+      '<input type="text" name="post[this_dont_exist_on_post]">'
+    end
+
+    assert_dom_equal expected, output_buffer
+  end
+
+  def test_form_with_doesnt_call_private_or_protected_properties_on_form_object_skipping_value
+    obj = Class.new do
+      private def private_property
+        "That would be great."
+      end
+
+      protected def protected_property
+        "I believe you have my stapler."
+      end
+    end.new
+
+    form_with(model: obj, scope: "other_name", url: "/", id: "edit-other-name") do |f|
+      assert_dom_equal '<input type="hidden" name="other_name[private_property]">', f.hidden_field(:private_property)
+      assert_dom_equal '<input type="hidden" name="other_name[protected_property]">', f.hidden_field(:protected_property)
+    end
+  end
+
   def test_form_with_with_collection_radio_buttons
     post = Post.new
     def post.active; false; end
@@ -599,18 +668,6 @@ class FormWithActsLikeFormForTest < FormWithTest
     assert_dom_equal expected, output_buffer
   end
 
-  def test_form_tags_do_not_call_private_properties_on_form_object
-    obj = Class.new do
-      private def private_property
-        raise "This method should not be called."
-      end
-    end.new
-
-    form_with(model: obj, scope: "other_name", url: "/", id: "edit-other-name") do |f|
-      assert_raise(NoMethodError) { f.hidden_field(:private_property) }
-    end
-  end
-
   def test_form_with_with_method_as_part_of_html_options
     form_with(model: @post, url: "/", id: "create-post", html: { method: :delete }) do |f|
       concat f.text_field(:title)
-- 
cgit v1.2.3