Skip to content

Commit

Permalink
Translate FormBuilder#button calls with formmethod:
Browse files Browse the repository at this point in the history
When submitting a `<form>`, browsers will serialize the element that
initiated the submission as part of the [FormData][], including its
`name` and `value` attributes.

Browser support for `<form>` submission HTTP verbs is limited to `GET`
and `POST`. Rails currently works around this [limitation by
constructing `<input type="hidden" name="_method" value="VERB">` which
serializes `_method="VERB"` to the FormData][_method].

To support varied HTTP actions within the same form, this commit
intervenes when a `form.button formmethod: "..."` call is made during
form construction, and translates any `formmethod:` value to the
corresponding work-around version.

[FormData]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
[_method]: https://edgeguides.rubyonrails.org/form_helpers.html#how-do-forms-with-patch-put-or-delete-methods-work-questionmark
[button-formmethod]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formmethod
  • Loading branch information
seanpdoyle committed Jan 8, 2021
1 parent 46165a0 commit b8c9c9d
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 3 deletions.
20 changes: 18 additions & 2 deletions actionview/CHANGELOG.md
@@ -1,14 +1,30 @@
* Change `ActionView::Helpers::FormBuilder#button` to transform `formmethod`
attributes into `_method="$VERB"` Form Data to enable varied same-form actions:

<%= form_with model: post, method: :put do %>
<%= form.button "Update" %>
<%= form.button "Delete", formmethod: :delete %>
<% end %>
<%# => <form action="posts/1">
=> <input type="hidden" name="_method" value="put">
=> <button type="submit">Update</button>
=> <button type="submit" formmethod="post" name="_method" value="delete">Delete</button>
=> </form>
%>

*Sean Doyle*

* Change `ActionView::Helpers::UrlHelper#button_to` to *always* render a
`<button>` element, regardless of whether or not the content is passed as
the first argument or as a block.

<%= button_to "Delete", post_path(@post), method: :delete %>
<%# => <form method="/posts/1"><input type="_method" value="delete"><button type="submit">Delete</button></form>
<%# => <form action="/posts/1"><input type="hidden" name="_method" value="delete"><button type="submit">Delete</button></form>

<%= button_to post_path(@post), method: :delete do %>
Delete
<% end %>
<%# => <form method="/posts/1"><input type="_method" value="delete"><button type="submit">Delete</button></form>
<%# => <form action="/posts/1"><input type="hidden" name="_method" value="delete"><button type="submit">Delete</button></form>

*Sean Doyle*, *Dusan Orlovic*

Expand Down
5 changes: 5 additions & 0 deletions actionview/lib/action_view/helpers/form_helper.rb
Expand Up @@ -2546,6 +2546,11 @@ def button(value = nil, options = {}, &block)
value = @template.capture { yield(value) }
end

formmethod = options[:formmethod]
if /(post|get)/i.match(formmethod).nil? && formmethod.present? && !options.key?(:name) && !options.key?(:value)
options.merge! formmethod: :post, name: "_method", value: formmethod
end

@template.button_tag(value, options)
end

Expand Down
60 changes: 60 additions & 0 deletions actionview/test/template/form_helper_test.rb
Expand Up @@ -2386,6 +2386,66 @@ def test_submit_with_object_which_is_namespaced
end
end

def test_button_with_get_formmethod_attribute
form_for(@post, as: :another_post) do |f|
concat f.button "GET", formmethod: :get
end

expected = whole_form("/posts/123", "edit_another_post", "edit_another_post", method: "patch") do
"<button type='submit' formmethod='get' name='button'>GET</button>"
end

assert_dom_equal expected, output_buffer
end

def test_button_with_post_formmethod_attribute
form_for(@post, as: :another_post) do |f|
concat f.button "POST", formmethod: :post
end

expected = whole_form("/posts/123", "edit_another_post", "edit_another_post", method: "patch") do
"<button type='submit' formmethod='post' name='button'>POST</button>"
end

assert_dom_equal expected, output_buffer
end

def test_button_with_other_formmethod_attribute
form_for(@post, as: :another_post) do |f|
concat f.button "Delete", formmethod: :delete
end

expected = whole_form("/posts/123", "edit_another_post", "edit_another_post", method: "patch") do
"<button type='submit' formmethod='post' name='_method' value='delete'>Delete</button>"
end

assert_dom_equal expected, output_buffer
end

def test_button_with_other_formmethod_attribute_and_name
form_for(@post, as: :another_post) do |f|
concat f.button "Delete", formmethod: :delete, name: "existing"
end

expected = whole_form("/posts/123", "edit_another_post", "edit_another_post", method: "patch") do
"<button type='submit' formmethod='delete' name='existing'>Delete</button>"
end

assert_dom_equal expected, output_buffer
end

def test_button_with_other_formmethod_attribute_and_value
form_for(@post, as: :another_post) do |f|
concat f.button "Delete", formmethod: :delete, value: "existing"
end

expected = whole_form("/posts/123", "edit_another_post", "edit_another_post", method: "patch") do
"<button type='submit' formmethod='delete' name='button' value='existing'>Delete</button>"
end

assert_dom_equal expected, output_buffer
end

def test_nested_fields_for
@comment.body = "Hello World"
form_for(@post) do |f|
Expand Down
30 changes: 29 additions & 1 deletion guides/source/form_helpers.md
Expand Up @@ -323,14 +323,42 @@ Output:
<form accept-charset="UTF-8" action="/search" method="post">
<input name="_method" type="hidden" value="patch" />
<input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
...
<!-- ... -->
</form>
```

When parsing POSTed data, Rails will take into account the special `_method` parameter and act as if the HTTP method was the one specified inside it ("PATCH" in this example).

When rendering a form, submission buttons can override the declared `method` attribute through the `formmethod:` keyword:

```erb
<%= form_with url: "/posts/1", method: :patch do |form| %>
<%= form.button "Delete", formmethod: :delete, data: { confirm: "Are you sure?" } %>
<%= form.button "Update" %>
<% end %>
```

Similar to `<form>` elements, most browsers _don't support_ overriding form methods declared through [formmethod][] other than "GET" and "POST".

Rails works around this issue by emulating other methods over POST through a combination of [formmethod][], [value][button-value], and [name][button-name] attributes:

```html
<form accept-charset="UTF-8" action="/posts/1" method="post">
<input name="_method" type="hidden" value="patch" />
<input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
<!-- ... -->

<button type="submit" formmethod="post" name="_method" value="delete" data-confirm="Are you sure?">Delete</button>
<button type="submit" name="button">Update</button>
</form>
```

IMPORTANT: In Rails 6.0 and 5.2, all forms using `form_with` implement `remote: true` by default. These forms will submit data using an XHR (Ajax) request. To disable this include `local: true`. To dive deeper see [Working with JavaScript in Rails](working_with_javascript_in_rails.html#remote-elements) guide.

[formmethod]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formmethod
[button-name]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
[button-value]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-value

Making Select Boxes with Ease
-----------------------------

Expand Down

0 comments on commit b8c9c9d

Please sign in to comment.