New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add form_with to unify form_tag/form_for. #26976

Merged
merged 16 commits into from Nov 21, 2016

Conversation

@kaspth
Member

kaspth commented Nov 5, 2016

Ref #25197

form_tag and form_for serve very similar use cases. This
PR unifies that usage such that form_with can output just
the opening form tag akin to form_tag and can just work with
a url, for instance.

form_with by default doesn't attach a class or id to the form.

Ported over old tests where applicable to ensure maximum coverage,
but left some commented out because they don't yet apply (e.g.
fields_for later being replaced by fields).

Pending in this PR:

  • Add new fields DSL
  • Default to remote: true
  • Remove commented out tests in the form_with_test.rb.
  • Basic documentation
  • Changelog entry 馃榿

Later work up for grabs once this is in:

  • Add overridable field values, e.g. form.text_field :name, 'override'
  • Allow fields not on the model, e.g. form.text_field :not_implemented_on_model
  • Revisit form field defaults e.g. stop outputting ids and classes on fields by default
  • Revisit form options DSL methods
  • Implement form_tag and form_for through form_with (extract the implementations to a common object and set configs)

@kaspth kaspth added the actionview label Nov 5, 2016

@kaspth kaspth added this to the 5.1.0 milestone Nov 5, 2016

@kaspth kaspth self-assigned this Nov 5, 2016

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# TODO: Documentation
def fields(scope = nil, model: nil, **options, &block)
fields_for(scope || model, model, **options, &block)
end

This comment has been minimized.

@kaspth

kaspth Nov 6, 2016

Member

@dhh added the fields method to do:

form_with(model: @post) do |form|
  form.fields(:comment, model: @comment) do |fields|
    fields.text_field # ...
  end
end

But it reads a bit weird if the passed model is a collection: fields(:comments, model: @post.comments).

Not sure if you intended this to be much more than a fields_for alias with the don't output ids and classes by default config.

@kaspth

kaspth Nov 6, 2016

Member

@dhh added the fields method to do:

form_with(model: @post) do |form|
  form.fields(:comment, model: @comment) do |fields|
    fields.text_field # ...
  end
end

But it reads a bit weird if the passed model is a collection: fields(:comments, model: @post.comments).

Not sure if you intended this to be much more than a fields_for alias with the don't output ids and classes by default config.

kaspth added some commits Nov 5, 2016

Add form_with to unify form_tag/form_for.
`form_tag` and `form_for` serve very similar use cases. This
PR unifies that usage such that `form_with` can output just
the opening form tag akin to `form_tag` and can just work with
a url, for instance.

`form_with` by default doesn't attach class or id to the form 鈥
removing them on fields is moved out to a default revisiting PR later.

Ported over old tests where applicable to ensure maximum coverage,
but left some commented out because they don't yet apply (e.g.
`fields_for` later being replaced by `fields`).

[ Kasper Timm Hansen & Marek Kirejczyk ]
Add fields DSL method.
Strips `_for` and requires models passed as a keyword argument.
Document form_with.
Graft the `form_for` docs: rewrite, revise and expand where
needed.

Also test that a `format` isn't used when an explicit URL
is passed.
@kaspth

This comment has been minimized.

Show comment
Hide comment
@kaspth

kaspth Nov 6, 2016

Member

Documented form_with and had one hell of a time doing it! Really got to know the existing API and where the new one differs, plus in what way it does.

Will add docs for fields tomorrow.

Member

kaspth commented Nov 6, 2016

Documented form_with and had one hell of a time doing it! Really got to know the existing API and where the new one differs, plus in what way it does.

Will add docs for fields tomorrow.

Enable remote by default.
Brand new world! Forms submit via XHRs by default, woah.
@kaspth

This comment has been minimized.

Show comment
Hide comment
@kaspth

kaspth Nov 6, 2016

Member

Figured since form_with already doesn't output id and class by default, we may as well rejigger the remaining default at the form level: remote. It's now on by default!

Member

kaspth commented Nov 6, 2016

Figured since form_with already doesn't output id and class by default, we may as well rejigger the remaining default at the form level: remote. It's now on by default!

Readd commented out tests.
Gives us something to revise when we're redoing the
form options helpers.

Also deletes the needless tests for the unsupported namespace
option.
@kaspth

This comment has been minimized.

Show comment
Hide comment
@kaspth

kaspth Nov 8, 2016

Member

This should be everything that's needed to prevent the default ids:

diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index ef114b3..8550ad8 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -703,7 +703,7 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, remote: t
         html_options[:remote] = remote unless html_options.key?(:remote)

         if block_given?
-          builder = instantiate_builder(scope, model, options)
+          builder = instantiate_builder(scope, model, options.merge(skip_default_ids: true))
           output  = capture(builder, &Proc.new)
           html_options[:multipart] ||= builder.multipart?

@@ -963,12 +963,7 @@ def fields_for(record_name, record_object = nil, options = {}, &block)

       # TODO: Documentation
       def fields(scope = nil, model: nil, **options, &block)
-        # TODO: Remove when ids and classes are no longer output by default.
-        if model
-          scope ||= model_name_from_record_or_class(model).param_key
-        end
-
-        builder = instantiate_builder(scope, model, options)
+        builder = instantiate_builder(scope, model, options.merge(skip_default_ids: true))
         capture(builder, &block)
       end

@@ -1537,7 +1532,7 @@ def to_model
       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) : {}
+        @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids) : {}
         if @object_name.to_s.match(/\[\]$/)
           if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
             @auto_index = object.to_param
@@ -1840,6 +1835,7 @@ def fields_for(record_name, record_object = nil, fields_options = {}, &block)

       # TODO: Documentation
       def fields(scope = nil, model: nil, **options, &block)
+        options[:skip_default_ids] = true
         fields_for(scope || model, model, **options, &block)
       end

diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index cf8a6d6..b8c446c 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -13,6 +13,7 @@ def initialize(object_name, method_name, template_object, options = {})

           @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
           @object = retrieve_object(options.delete(:object))
+          @skip_default_ids = options.delete(:skip_default_ids)
           @options = options
           @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil
         end
@@ -81,9 +82,12 @@ def add_default_name_and_id_for_value(tag_value, options)
           def add_default_name_and_id(options)
             index = name_and_id_index(options)
             options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
-            options["id"] = options.fetch("id") { tag_id(index) }
-            if namespace = options.delete("namespace")
-              options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
+
+            unless skip_default_ids?
+              options["id"] = options.fetch("id") { tag_id(index) }
+              if namespace = options.delete("namespace")
+                options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
+              end
             end
           end

@@ -154,6 +158,10 @@ def add_options(option_tags, options, value = nil)
           def name_and_id_index(options)
             options.key?("index") ? options.delete("index") || "" : @auto_index
           end
+
+          def skip_default_ids?
+            @skip_default_ids
+          end
       end
     end
   end

I think there's likely more code we can skip in the tags.

Member

kaspth commented Nov 8, 2016

This should be everything that's needed to prevent the default ids:

diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index ef114b3..8550ad8 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -703,7 +703,7 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, html: {}, remote: t
         html_options[:remote] = remote unless html_options.key?(:remote)

         if block_given?
-          builder = instantiate_builder(scope, model, options)
+          builder = instantiate_builder(scope, model, options.merge(skip_default_ids: true))
           output  = capture(builder, &Proc.new)
           html_options[:multipart] ||= builder.multipart?

@@ -963,12 +963,7 @@ def fields_for(record_name, record_object = nil, options = {}, &block)

       # TODO: Documentation
       def fields(scope = nil, model: nil, **options, &block)
-        # TODO: Remove when ids and classes are no longer output by default.
-        if model
-          scope ||= model_name_from_record_or_class(model).param_key
-        end
-
-        builder = instantiate_builder(scope, model, options)
+        builder = instantiate_builder(scope, model, options.merge(skip_default_ids: true))
         capture(builder, &block)
       end

@@ -1537,7 +1532,7 @@ def to_model
       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) : {}
+        @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids) : {}
         if @object_name.to_s.match(/\[\]$/)
           if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
             @auto_index = object.to_param
@@ -1840,6 +1835,7 @@ def fields_for(record_name, record_object = nil, fields_options = {}, &block)

       # TODO: Documentation
       def fields(scope = nil, model: nil, **options, &block)
+        options[:skip_default_ids] = true
         fields_for(scope || model, model, **options, &block)
       end

diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index cf8a6d6..b8c446c 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -13,6 +13,7 @@ def initialize(object_name, method_name, template_object, options = {})

           @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
           @object = retrieve_object(options.delete(:object))
+          @skip_default_ids = options.delete(:skip_default_ids)
           @options = options
           @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil
         end
@@ -81,9 +82,12 @@ def add_default_name_and_id_for_value(tag_value, options)
           def add_default_name_and_id(options)
             index = name_and_id_index(options)
             options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
-            options["id"] = options.fetch("id") { tag_id(index) }
-            if namespace = options.delete("namespace")
-              options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
+
+            unless skip_default_ids?
+              options["id"] = options.fetch("id") { tag_id(index) }
+              if namespace = options.delete("namespace")
+                options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
+              end
             end
           end

@@ -154,6 +158,10 @@ def add_options(option_tags, options, value = nil)
           def name_and_id_index(options)
             options.key?("index") ? options.delete("index") || "" : @auto_index
           end
+
+          def skip_default_ids?
+            @skip_default_ids
+          end
       end
     end
   end

I think there's likely more code we can skip in the tags.

[ci skip] Document the fields helpers.
Treat both the FormBuilder and FormHelper.
@kaspth

This comment has been minimized.

Show comment
Hide comment
@kaspth

kaspth Nov 13, 2016

Member

Allright, I think this is ready for review 馃榿 鈥 I'll merge this tomorrow/tuesday/later depending on the feedback.

Member

kaspth commented Nov 13, 2016

Allright, I think this is ready for review 馃榿 鈥 I'll merge this tomorrow/tuesday/later depending on the feedback.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# <input type="text" name="post[title]" value="<the title of the post>">
# </form>
#
# The parameters in the forms are accessible in controlleres according to

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

controllers

@vipulnsward

vipulnsward Nov 13, 2016

Member

controllers

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
#
# For ease of comparison the examples above left out the submit button,
# as well as the auto generated hidden fields that enable UTF-8 support
# and adds an authenticity token needed for Cross Site Request Forgery

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

Don't think this needs title case. It is referenced as small case in its own documentation.

@vipulnsward

vipulnsward Nov 13, 2016

Member

Don't think this needs title case. It is referenced as small case in its own documentation.

#
# ==== +form_with+ options
#
# * <tt>:url</tt> - The URL the form submits to. Akin to values passed to

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

I would use "similar" instead of "Akin", since its easier to understand.

@vipulnsward

vipulnsward Nov 13, 2016

Member

I would use "similar" instead of "Akin", since its easier to understand.

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

Akin seems fine to me.

@kaspth

kaspth Nov 14, 2016

Member

Akin seems fine to me.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# either "get" or "post". If "patch", "put", "delete", or another verb
# is used, a hidden input named <tt>_method</tt> is added to
# simulate the verb over post.
# * <tt>:format</tt> - The format of the route post submits to.

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

post, put, patch, etc

@vipulnsward

vipulnsward Nov 13, 2016

Member

post, put, patch, etc

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# * <tt>:scope</tt> - The scope to prefix input field names with and
# thereby how the submitted parameters are grouped in controllers.
# * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and
# <tt>:scope</tt> by plus fill out input field values.

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

either comma after "by", or "and" instead of "by".

@vipulnsward

vipulnsward Nov 13, 2016

Member

either comma after "by", or "and" instead of "by".

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# E.g. turn <tt>params[:post]</tt> into <tt>params[:article]</tt>.
# * <tt>:authenticity_token</tt> - Authenticity token to use in the form.
# Override with a custom authenticity token or pass <tt>false</tt> to
# skip the authenticity_token field altogether.

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

<tt>:authenticity_token</tt>

@vipulnsward

vipulnsward Nov 13, 2016

Member

<tt>:authenticity_token</tt>

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# <form action="http://www.example.com" method="post" data-behavior="autosave" name="go">
# <input name="_method" type="hidden" value="patch" />
# ...
# </form>

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

Looks like wrong output?

@vipulnsward

vipulnsward Nov 13, 2016

Member

Looks like wrong output?

# Some ORM systems do not use IDs on nested models so in this case you want to be able
# to disable the hidden id.
#
# In the following example the Post model has many Comments stored within it in a NoSQL database,

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

it, in

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

Think it's valid without the comma.

@kaspth

kaspth Nov 14, 2016

Member

Think it's valid without the comma.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# Writers are considered a nested attributes setter if they're of the
# <tt>*_attributes=</tt> form, e.g. <tt>address_attributes=</tt>.
#
# Depending on the association's reader method return value a different

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

value, a

@vipulnsward

vipulnsward Nov 13, 2016

Member

value, a

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# <tt>*_attributes=</tt> form, e.g. <tt>address_attributes=</tt>.
#
# Depending on the association's reader method return value a different
# form builder is yielded. For single object returns a one-to-one builder,

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

object, returns

@vipulnsward

vipulnsward Nov 13, 2016

Member

object, returns

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

No those words are meant to stay together. We're talking about the return value of the reader method.

@kaspth

kaspth Nov 14, 2016

Member

No those words are meant to stay together. We're talking about the return value of the reader method.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# Note +fields+ automatically generates a hidden field to store the record
# ID. For circumstances where this is not needed pass
# <tt>include_id: false</tt> to skip it.
def fields(scope = nil, model: nil, **options, &block)

This comment has been minimized.

@vipulnsward

vipulnsward Nov 13, 2016

Member

I believe this needs the same fixes from above.

@vipulnsward

vipulnsward Nov 13, 2016

Member

I believe this needs the same fixes from above.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
#
# By default +form_with+ attaches the <tt>data-remote</tt> attribute
# submitting the form via an XMLHTTPRequest in the background if an
# an Unobtrusive JavaScript driver, like jquery-ujs, is used. See the

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

Since we're actually dropping jquery-ujs, maybe we need to tell a different story here.

@dhh

dhh Nov 14, 2016

Member

Since we're actually dropping jquery-ujs, maybe we need to tell a different story here.

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

I thought we we're just dropping the jquery in jquery-ujs. Isn't the plan to keep the ujs behavior around?

@kaspth

kaspth Nov 14, 2016

Member

I thought we we're just dropping the jquery in jquery-ujs. Isn't the plan to keep the ujs behavior around?

This comment has been minimized.

@dhh

dhh Nov 15, 2016

Member

Yeah, UJS is still there. Just that referencing jquery-ujs seems dated when that won't be the default.

@dhh

dhh Nov 15, 2016

Member

Yeah, UJS is still there. Just that referencing jquery-ujs seems dated when that won't be the default.

This comment has been minimized.

@kaspth

kaspth Nov 15, 2016

Member

Got it 馃憤

@kaspth

kaspth Nov 15, 2016

Member

Got it 馃憤

# <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
# This is helpful when fragment-caching the form. Remote forms
# get the authenticity token from the <tt>meta</tt> tag, so embedding is
# unnecessary unless you support browsers without JavaScript.

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

Since our new default is remote: true, I think we need to think this through a little more. Like having authenticity_token be reliant on that value. If you do remote: false, THEN we'll by default include authenticity_token, but otherwise not. And in both cases you can always supply one via the option to overwrite the default.

@dhh

dhh Nov 14, 2016

Member

Since our new default is remote: true, I think we need to think this through a little more. Like having authenticity_token be reliant on that value. If you do remote: false, THEN we'll by default include authenticity_token, but otherwise not. And in both cases you can always supply one via the option to overwrite the default.

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

Yup, figured it was just flipping the default. But of course there's repercussions.

@kaspth

kaspth Nov 14, 2016

Member

Yup, figured it was just flipping the default. But of course there's repercussions.

This comment has been minimized.

@matthewd

matthewd Nov 14, 2016

Member

I suspect we should keep the token by default, because otherwise our default forms are broken on JS-less browsers (and anywhere without UJS present). I could maybe see the UJS gem toggling it off by default, but making UJS a hard dep for form submissions to work [out of the box] seems a bit rough.

@matthewd

matthewd Nov 14, 2016

Member

I suspect we should keep the token by default, because otherwise our default forms are broken on JS-less browsers (and anywhere without UJS present). I could maybe see the UJS gem toggling it off by default, but making UJS a hard dep for form submissions to work [out of the box] seems a bit rough.

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

True, I guess that was the reason for config.action_view.embed_authenticity_token_in_remote_forms as well.

@kaspth

kaspth Nov 14, 2016

Member

True, I guess that was the reason for config.action_view.embed_authenticity_token_in_remote_forms as well.

This comment has been minimized.

@dhh

dhh Nov 15, 2016

Member

True. Guess it can just ignore those embedded tokens. I was thinking that remote: true would force the form remote, but of course, if JS is off, that won't be read.

@dhh

dhh Nov 15, 2016

Member

True. Guess it can just ignore those embedded tokens. I was thinking that remote: true would force the form remote, but of course, if JS is off, that won't be read.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# unnecessary unless you support browsers without JavaScript.
# * <tt>:remote</tt> - Set to true to allow the Unobtrusive
# JavaScript drivers to control the submit behavior, defaulting to
# to an XHR submit. Disable with <tt>remote: false</tt>.

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

The default is true, so needs to flip what this is for.

@dhh

dhh Nov 14, 2016

Member

The default is true, so needs to flip what this is for.

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

Ah yes!

@kaspth

kaspth Nov 14, 2016

Member

Ah yes!

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# JavaScript drivers to control the submit behavior, defaulting to
# to an XHR submit. Disable with <tt>remote: false</tt>.
# * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name
# utf8 is not output. Default is true.

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

When we have options that are default true, then it's generally better to flip the key name to a negative, imo. So we'd go with skip_enforcing_utf8: true.

@dhh

dhh Nov 14, 2016

Member

When we have options that are default true, then it's generally better to flip the key name to a negative, imo. So we'd go with skip_enforcing_utf8: true.

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

This was lifted from form_for. I figured the parity was better when it was a relatively oft unused option. Do you prefer that we rename still?

@kaspth

kaspth Nov 14, 2016

Member

This was lifted from form_for. I figured the parity was better when it was a relatively oft unused option. Do you prefer that we rename still?

This comment has been minimized.

@dhh

dhh Nov 15, 2016

Member

This is a new API, so I saw we make the best choices we know how. Keeping parity is not important.

@dhh

dhh Nov 15, 2016

Member

This is a new API, so I saw we make the best choices we know how. Keeping parity is not important.

This comment has been minimized.

@kaspth

kaspth Nov 15, 2016

Member

Sure, then we should give that treatment to these options too:

skip_id: true # Was `include_id: false` for nested `fields`
skip_authenticity_token: true # Was just `authenticity_token: false`
local: true # Was `remote: true`. Not sure about this one though.
@kaspth

kaspth Nov 15, 2016

Member

Sure, then we should give that treatment to these options too:

skip_id: true # Was `include_id: false` for nested `fields`
skip_authenticity_token: true # Was just `authenticity_token: false`
local: true # Was `remote: true`. Not sure about this one though.

This comment has been minimized.

@kaspth

kaspth Nov 20, 2016

Member

Backing out of the skip_authenticy_token: true change. Since specifying a custom token via authenticy_token: 'abcdef' is valid. And would rather not have two options for the same thing.

@kaspth

kaspth Nov 20, 2016

Member

Backing out of the skip_authenticy_token: true change. Since specifying a custom token via authenticy_token: 'abcdef' is valid. And would rather not have two options for the same thing.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# form_with(model: @post, url: super_posts_path)
# form_with(model: @post, scope: :article)
# form_with(model: @post, format: :json)
# form_with(model: @post, authenticity_token: false) # Disables the token.

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

Fix when we default token to off with remote: true.

@dhh

dhh Nov 14, 2016

Member

Fix when we default token to off with remote: true.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
html_options = html.merge(options.except(:index, :include_id, :builder))
html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
html_options[:remote] = remote unless html_options.key?(:remote)

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

All this prep work is only used in the block_given? path, so it should be done there. Also, I think there's more we can do to make this a Composed Method. A lot of mechanics exposed at a high level.

@dhh

dhh Nov 14, 2016

Member

All this prep work is only used in the block_given? path, so it should be done there. Also, I think there's more we can do to make this a Composed Method. A lot of mechanics exposed at a high level.

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

It's used in the else too as the last parameter here too: html_options = html_options_for_form(url || {}, html_options)

But yeah, let me see what I can do with the composition 馃憤

@kaspth

kaspth Nov 14, 2016

Member

It's used in the else too as the last parameter here too: html_options = html_options_for_form(url || {}, html_options)

But yeah, let me see what I can do with the composition 馃憤

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# Zip code: <%= address_fields.text_field :zip_code %>
# <% end %>
# ...
# <% end %>

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

Mixed use of ERB insertion tags. Should use them for form_with too.

@dhh

dhh Nov 14, 2016

Member

Mixed use of ERB insertion tags. Should use them for form_with too.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# <% end %>
#
# When address is already an association on a Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

I'd actually like to kill accepts_nested_attributes_for in due time. Don't think we should promote it for this new API. Rather, let's just show how to do it by hand in the controller.

@dhh

dhh Nov 14, 2016

Member

I'd actually like to kill accepts_nested_attributes_for in due time. Don't think we should promote it for this new API. Rather, let's just show how to do it by hand in the controller.

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

Oh yeah! 馃敟 Reading through this I thought accepts_nested_attributes_for stuck out, refrained from doing anything because I didn't have a better idea yet. But yes, let's start down the path toward something better!

@kaspth

kaspth Nov 14, 2016

Member

Oh yeah! 馃敟 Reading through this I thought accepts_nested_attributes_for stuck out, refrained from doing anything because I didn't have a better idea yet. But yes, let's start down the path toward something better!

This comment has been minimized.

@dhh

dhh Nov 15, 2016

Member

For starters, I'd just drop the entire topic for now.

@dhh

dhh Nov 15, 2016

Member

For starters, I'd just drop the entire topic for now.

Show outdated Hide outdated actionview/lib/action_view/helpers/form_helper.rb
# Note +fields+ automatically generates a hidden field to store the record
# ID. For circumstances where this is not needed pass
# <tt>include_id: false</tt> to skip it.
def fields(scope = nil, model: nil, **options, &block)

This comment has been minimized.

@dhh

dhh Nov 14, 2016

Member

Seems all this doc stuff is repeated from above?

@dhh

dhh Nov 14, 2016

Member

Seems all this doc stuff is repeated from above?

This comment has been minimized.

@kaspth

kaspth Nov 14, 2016

Member

Yup, copied to mirror how fields_for documents itself (i.e. both as the helper and in the builder). I'll update this to just refer to the fields helper docs above.

@kaspth

kaspth Nov 14, 2016

Member

Yup, copied to mirror how fields_for documents itself (i.e. both as the helper and in the builder). I'll update this to just refer to the fields helper docs above.

@kaspth

This comment has been minimized.

Show comment
Hide comment
@kaspth

kaspth Nov 14, 2016

Member

@vipulnsward @dhh thanks for the feedback, I'll look more into this tomorrow 馃憤

Member

kaspth commented Nov 14, 2016

@vipulnsward @dhh thanks for the feedback, I'll look more into this tomorrow 馃憤

kaspth added some commits Nov 20, 2016

Invert `include_id` to `skip_id`.
`skip_id: true` reads better than `include_id: false` (since the
`include_id` default is true).
Invert `remote` to `local`.
Since forms are remote by default, the option name makes more sense
as `local: true`.
Refer to the brand spanking new rails-ujs.
Soon to be bundled in Rails proper, so jquery-ujs is out.
Make `form_with` a bit more composed.
The flow is still not quite what it should be because the legacy
methods and these new ones pull at opposite ends.

Lots of options have been renamed, so now the new pieces don't fit
in so well.

I'll try to work on this in later commits after this PR (it's likely
there's a much better way to structure this whole part of Action View).
@kaspth

This comment has been minimized.

Show comment
Hide comment
@kaspth

kaspth Nov 20, 2016

Member

@dhh inverted the option names, removed the nested attributes example and tried to make form_with a bit more composed.

However, that carried over a lot of baggage from copying over html_options_for_form

def html_options_for_form(url_for_options, options)
options.stringify_keys.tap do |html_options|
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
# The following URL is unescaped, this is just a hash of options, and it is the
# responsibility of the caller to escape all the values.
html_options["action"] = url_for(url_for_options)
html_options["accept-charset"] = "UTF-8"
html_options["data-remote"] = true if html_options.delete("remote")
if html_options["data-remote"] &&
!embed_authenticity_token_in_remote_forms &&
html_options["authenticity_token"].blank?
# The authenticity token is taken from the meta tag in this case
html_options["authenticity_token"] = false
elsif html_options["authenticity_token"] == true
# Include the default authenticity_token, which is only generated when its set to nil,
# but we needed the true value to override the default of no authenticity_token on data-remote.
html_options["authenticity_token"] = nil
end
end
end
鈥 which was the most sensible way to do it that I could see.

I think it would be easier to improve the code structure further here in more focused commits after this PR.

Member

kaspth commented Nov 20, 2016

@dhh inverted the option names, removed the nested attributes example and tried to make form_with a bit more composed.

However, that carried over a lot of baggage from copying over html_options_for_form

def html_options_for_form(url_for_options, options)
options.stringify_keys.tap do |html_options|
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
# The following URL is unescaped, this is just a hash of options, and it is the
# responsibility of the caller to escape all the values.
html_options["action"] = url_for(url_for_options)
html_options["accept-charset"] = "UTF-8"
html_options["data-remote"] = true if html_options.delete("remote")
if html_options["data-remote"] &&
!embed_authenticity_token_in_remote_forms &&
html_options["authenticity_token"].blank?
# The authenticity token is taken from the meta tag in this case
html_options["authenticity_token"] = false
elsif html_options["authenticity_token"] == true
# Include the default authenticity_token, which is only generated when its set to nil,
# but we needed the true value to override the default of no authenticity_token on data-remote.
html_options["authenticity_token"] = nil
end
end
end
鈥 which was the most sensible way to do it that I could see.

I think it would be easier to improve the code structure further here in more focused commits after this PR.

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Nov 21, 2016

Member

Looks pretty good to me 馃憤

Member

dhh commented Nov 21, 2016

Looks pretty good to me 馃憤

@kaspth kaspth merged commit 67f81cc into rails:master Nov 21, 2016

2 of 3 checks passed

continuous-integration/travis-ci/push The Travis CI build could not complete due to an error
Details
codeclimate Code Climate didn't find any new or fixed issues.
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details

@kaspth kaspth deleted the kaspth:form-with-helper branch Nov 21, 2016

kamipo added a commit to kamipo/rails that referenced this pull request Nov 24, 2016

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Mar 14, 2017

Member

@kaspth Thanks for this PR.

You added a fields method to Action View which lets you group multiple fields together, so that:

<%= fields :comment do |fields| %>
  <%= fields.text_field :subject %>
  <%= fields.text_field :body %>
<% end %>

becomes:

<input type="text" name="comment[subject] id="comment_subject">
<input type="text" name="comment[body] id="comment_body">

As you see, the HTML has lost the nuance that those two fields are grouped together.

In my mind, since you are grouping them together in your code, a better output would be:

<fieldset>
  <input type="text" name="comment[subject] id="comment_subject">
  <input type="text" name="comment[body] id="comment_body">
</fieldset>

since the fieldset HTML tag is used to group related elements in a form:

Grouping related form controls makes forms more understandable for all users, as related controls are easier to identify. It also makes it easier for people to focus on smaller and more manageable groups rather than try to grasp the entire form at once.

Grouping needs to be carried out visually and in the code, for example, by using the <fieldset> and <legend> elements to associate related form controls.

@kaspth (and @dhh -- original requester for this PR)鈥β爓hat is your opinion on this?

Could the fields method introduced here have at least an option to wrap the fields in a <fieldset>?

Member

claudiob commented Mar 14, 2017

@kaspth Thanks for this PR.

You added a fields method to Action View which lets you group multiple fields together, so that:

<%= fields :comment do |fields| %>
  <%= fields.text_field :subject %>
  <%= fields.text_field :body %>
<% end %>

becomes:

<input type="text" name="comment[subject] id="comment_subject">
<input type="text" name="comment[body] id="comment_body">

As you see, the HTML has lost the nuance that those two fields are grouped together.

In my mind, since you are grouping them together in your code, a better output would be:

<fieldset>
  <input type="text" name="comment[subject] id="comment_subject">
  <input type="text" name="comment[body] id="comment_body">
</fieldset>

since the fieldset HTML tag is used to group related elements in a form:

Grouping related form controls makes forms more understandable for all users, as related controls are easier to identify. It also makes it easier for people to focus on smaller and more manageable groups rather than try to grasp the entire form at once.

Grouping needs to be carried out visually and in the code, for example, by using the <fieldset> and <legend> elements to associate related form controls.

@kaspth (and @dhh -- original requester for this PR)鈥β爓hat is your opinion on this?

Could the fields method introduced here have at least an option to wrap the fields in a <fieldset>?

@kaspth

This comment has been minimized.

Show comment
Hide comment
@kaspth

kaspth Mar 14, 2017

Member

I'd rather just defer any extra markup onto people themselves, so they have the complete freedom on how to design their forms.

Member

kaspth commented Mar 14, 2017

I'd rather just defer any extra markup onto people themselves, so they have the complete freedom on how to design their forms.

# simulate the verb over post.
# * <tt>:format</tt> - The format of the route the form submits to.
# Useful when submitting to another resource type, like <tt>:json</tt>.
# Skipped if a <tt>:url</tt> is passed.

This comment has been minimized.

@aditya-vector

aditya-vector Dec 11, 2017

This means when passing :url option, I would have to explicitly attach the format to the url as well? Wouldnt it be nicer to allow url without an explicit format and allow :format option too?

@aditya-vector

aditya-vector Dec 11, 2017

This means when passing :url option, I would have to explicitly attach the format to the url as well? Wouldnt it be nicer to allow url without an explicit format and allow :format option too?

@aledalgrande

This comment has been minimized.

Show comment
Hide comment
@aledalgrande

aledalgrande Mar 29, 2018

Contributor

This doesn't seem to be in the Rails guides, is it?

Contributor

aledalgrande commented Mar 29, 2018

This doesn't seem to be in the Rails guides, is it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment