1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Consistently render button_to as <button>

Prior to this commit, the
[ActionView::Helpers::UrlHelper#button_to][button_to] helper rendered
`<input type="submit">` elements when passed its contents as a String
argument, and rendered `<button type="submit">` elements when passed its
contents as a block.

This difference is subtle, and might lead to surprises.

Additionally, a `<form>` element's submitter can encode a `name`/`value`
pairing, which will be submitted as part of the request. When
`button_to` renders an `<input type="submit">` element, the "button"
content is rendered as a `[value]` attribute, which prevents any
meaningful data from being encoded.

Since it's a single `<button>` or `<input type="submit">` within a
`<form>`, missing out on that opportunity to encode information might
not be a show stopper, but ensuring that a `<button>` element is
rendered _without_ a default `[value]` attribute enables applications to
encode additional information that can be accessed JavaScript as
`element.value`, instead of a workaround like
`element.getAttribute("data-value")`.

Support rendering `input` elements with button_to
---

To support the original behavior of `button_to` rendering `<input
type="submit">` elements when invoked _without_ a block, expose the
`app.config.button_to_generates_button_tag` configuration flag.

By default, it's set to `true` and ensures that all `button_to` calls
render `<button>` elements. To revert to the original behavior, set it
to `false`.

[button_to]: https://api.rubyonrails.org/v6.0/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to

Co-authored-by: Dusan Orlovic <duleorlovic@gmail.com>
This commit is contained in:
Sean Doyle 2020-12-03 21:01:39 -05:00
parent dbf6dbb147
commit 9af9458396
6 changed files with 80 additions and 32 deletions

View file

@ -1,7 +1,21 @@
* 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>
<%= 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>
*Sean Doyle, Dusan Orlovic*
* Add `config.action_view.preload_links_header` to allow disabling of * Add `config.action_view.preload_links_header` to allow disabling of
the `Link` header being added by default when using `stylesheet_link_tag` the `Link` header being added by default when using `stylesheet_link_tag`
and `javascript_include_tag`. and `javascript_include_tag`.
*Andrew White* *Andrew White*
* The `translate` helper now resolves `default` values when a `nil` key is * The `translate` helper now resolves `default` values when a `nil` key is

View file

@ -29,6 +29,8 @@ module ActionView
end end
end end
mattr_accessor :button_to_generates_button_tag, default: false
# Basic implementation of url_for to allow use helpers without routes existence # Basic implementation of url_for to allow use helpers without routes existence
def url_for(options = nil) # :nodoc: def url_for(options = nil) # :nodoc:
case options case options
@ -250,12 +252,12 @@ module ActionView
# ==== Examples # ==== Examples
# <%= button_to "New", action: "new" %> # <%= button_to "New", action: "new" %>
# # => "<form method="post" action="/controller/new" class="button_to"> # # => "<form method="post" action="/controller/new" class="button_to">
# # <input value="New" type="submit" /> # # <button type="submit">New</button>
# # </form>" # # </form>"
# #
# <%= button_to "New", new_article_path %> # <%= button_to "New", new_article_path %>
# # => "<form method="post" action="/articles/new" class="button_to"> # # => "<form method="post" action="/articles/new" class="button_to">
# # <input value="New" type="submit" /> # # <button type="submit">New</button>
# # </form>" # # </form>"
# #
# <%= button_to [:make_happy, @user] do %> # <%= button_to [:make_happy, @user] do %>
@ -269,13 +271,13 @@ module ActionView
# #
# <%= button_to "New", { action: "new" }, form_class: "new-thing" %> # <%= button_to "New", { action: "new" }, form_class: "new-thing" %>
# # => "<form method="post" action="/controller/new" class="new-thing"> # # => "<form method="post" action="/controller/new" class="new-thing">
# # <input value="New" type="submit" /> # # <button type="submit">New</button>
# # </form>" # # </form>"
# #
# #
# <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %> # <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
# # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json"> # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
# # <input value="Create" type="submit" /> # # <button type="submit">Create</button>
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>" # # </form>"
# #
@ -284,7 +286,7 @@ module ActionView
# method: :delete, data: { confirm: "Are you sure?" } %> # method: :delete, data: { confirm: "Are you sure?" } %>
# # => "<form method="post" action="/images/delete/1" class="button_to"> # # => "<form method="post" action="/images/delete/1" class="button_to">
# # <input type="hidden" name="_method" value="delete" /> # # <input type="hidden" name="_method" value="delete" />
# # <input data-confirm='Are you sure?' value="Delete Image" type="submit" /> # # <button data-confirm='Are you sure?' type="submit">Delete Image</button>
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>" # # </form>"
# #
@ -293,7 +295,7 @@ module ActionView
# method: :delete, remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %> # method: :delete, remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
# # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'> # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
# # <input name='_method' value='delete' type='hidden' /> # # <input name='_method' value='delete' type='hidden' />
# # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' /> # # <button type='submit' data-disable-with='loading...' data-confirm='Are you sure?'>Destroy</button>
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>" # # </form>"
# # # #
@ -327,8 +329,8 @@ module ActionView
html_options = convert_options_to_data_attributes(options, html_options) html_options = convert_options_to_data_attributes(options, html_options)
html_options["type"] = "submit" html_options["type"] = "submit"
button = if block_given? button = if block_given? || button_to_generates_button_tag
content_tag("button", html_options, &block) content_tag("button", name || url, html_options, &block)
else else
html_options["value"] = name || url html_options["value"] = name || url
tag("input", html_options) tag("input", html_options)

View file

@ -81,6 +81,13 @@ module ActionView
PartialRenderer.collection_cache = app.config.action_controller.cache_store PartialRenderer.collection_cache = app.config.action_controller.cache_store
end end
initializer "action_view.button_to_generates_button_tag" do |app|
ActiveSupport.on_load(:action_view) do
ActionView::Helpers::UrlHelper.button_to_generates_button_tag =
app.config.action_view.delete(:button_to_generates_button_tag)
end
end
config.after_initialize do |app| config.after_initialize do |app|
enable_caching = if app.config.action_view.cache_template_loading.nil? enable_caching = if app.config.action_view.cache_template_loading.nil?
app.config.cache_classes app.config.cache_classes

View file

@ -50,6 +50,8 @@ class UrlHelperTest < ActiveSupport::TestCase
include RenderERBUtils include RenderERBUtils
setup :_prepare_context setup :_prepare_context
setup { ActionView::Helpers::UrlHelper.button_to_generates_button_tag = @button_to_generates_button_tag = true }
teardown { ActionView::Helpers::UrlHelper.button_to_generates_button_tag = @button_to_generates_button_tag }
def hash_for(options = {}) def hash_for(options = {})
{ controller: "foo", action: "bar" }.merge!(options) { controller: "foo", action: "bar" }.merge!(options)
@ -140,7 +142,7 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_button_to_without_protect_against_forgery_method def test_button_to_without_protect_against_forgery_method
self.class.undef_method(:protect_against_forgery?) self.class.undef_method(:protect_against_forgery?)
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com") button_to("Hello", "http://www.example.com")
) )
ensure ensure
@ -148,12 +150,12 @@ class UrlHelperTest < ActiveSupport::TestCase
end end
def test_button_to_with_straight_url def test_button_to_with_straight_url
assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com") assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit">Hello</button></form>}, button_to("Hello", "http://www.example.com")
end end
def test_button_to_with_path def test_button_to_with_path
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="/article/Hello" class="button_to"><input type="submit" value="Hello" /></form>}, %{<form method="post" action="/article/Hello" class="button_to"><button type="submit">Hello</button></form>},
button_to("Hello", article_path("Hello")) button_to("Hello", article_path("Hello"))
) )
end end
@ -162,7 +164,7 @@ class UrlHelperTest < ActiveSupport::TestCase
self.request_forgery = true self.request_forgery = true
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit">Hello</button><input name="form_token" type="hidden" value="secret" /></form>},
button_to("Hello", "http://www.example.com") button_to("Hello", "http://www.example.com")
) )
ensure ensure
@ -170,88 +172,92 @@ class UrlHelperTest < ActiveSupport::TestCase
end end
def test_button_to_with_form_class def test_button_to_with_form_class
assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: "custom-class") assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><button type="submit">Hello</button></form>}, button_to("Hello", "http://www.example.com", form_class: "custom-class")
end end
def test_button_to_with_form_class_escapes def test_button_to_with_form_class_escapes
assert_dom_equal %{<form method="post" action="http://www.example.com" class="&lt;script&gt;evil_js&lt;/script&gt;"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: "<script>evil_js</script>") assert_dom_equal %{<form method="post" action="http://www.example.com" class="&lt;script&gt;evil_js&lt;/script&gt;"><button type="submit">Hello</button></form>}, button_to("Hello", "http://www.example.com", form_class: "<script>evil_js</script>")
end end
def test_button_to_with_query def test_button_to_with_query
assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2") assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><button type="submit">Hello</button></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2")
end
def test_button_to_with_value
assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit" name="key" value="value">Hello</button></form>}, button_to("Hello", "http://www.example.com", name: "key", value: "value")
end end
def test_button_to_with_html_safe_URL def test_button_to_with_html_safe_URL
assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", raw("http://www.example.com/q1=v1&amp;q2=v2")) assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><button type="submit">Hello</button></form>}, button_to("Hello", raw("http://www.example.com/q1=v1&amp;q2=v2"))
end end
def test_button_to_with_query_and_no_name def test_button_to_with_query_and_no_name
assert_dom_equal %{<form method="post" action="http://www.example.com?q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="http://www.example.com?q1=v1&amp;q2=v2" /></form>}, button_to(nil, "http://www.example.com?q1=v1&q2=v2") assert_dom_equal %{<form method="post" action="http://www.example.com?q1=v1&amp;q2=v2" class="button_to"><button type="submit">http://www.example.com?q1=v1&amp;q2=v2</button></form>}, button_to(nil, "http://www.example.com?q1=v1&q2=v2")
end end
def test_button_to_with_javascript_confirm def test_button_to_with_javascript_confirm
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input data-confirm="Are you sure?" type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to"><button data-confirm="Are you sure?" type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" }) button_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" })
) )
end end
def test_button_to_with_javascript_disable_with def test_button_to_with_javascript_disable_with
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input data-disable-with="Greeting..." type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to"><button data-disable-with="Greeting..." type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", data: { disable_with: "Greeting..." }) button_to("Hello", "http://www.example.com", data: { disable_with: "Greeting..." })
) )
end end
def test_button_to_with_remote_and_form_options def test_button_to_with_remote_and_form_options
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="custom-class" data-remote="true" data-type="json"><input type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="custom-class" data-remote="true" data-type="json"><button type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", remote: true, form: { class: "custom-class", "data-type" => "json" }) button_to("Hello", "http://www.example.com", remote: true, form: { class: "custom-class", "data-type" => "json" })
) )
end end
def test_button_to_with_remote_and_javascript_confirm def test_button_to_with_remote_and_javascript_confirm
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><input data-confirm="Are you sure?" type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><button data-confirm="Are you sure?" type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", remote: true, data: { confirm: "Are you sure?" }) button_to("Hello", "http://www.example.com", remote: true, data: { confirm: "Are you sure?" })
) )
end end
def test_button_to_with_remote_and_javascript_disable_with def test_button_to_with_remote_and_javascript_disable_with
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><input data-disable-with="Greeting..." type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><button data-disable-with="Greeting..." type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", remote: true, data: { disable_with: "Greeting..." }) button_to("Hello", "http://www.example.com", remote: true, data: { disable_with: "Greeting..." })
) )
end end
def test_button_to_with_remote_false def test_button_to_with_remote_false
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", remote: false) button_to("Hello", "http://www.example.com", remote: false)
) )
end end
def test_button_to_enabled_disabled def test_button_to_enabled_disabled
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", disabled: false) button_to("Hello", "http://www.example.com", disabled: false)
) )
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input disabled="disabled" type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to"><button disabled="disabled" type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", disabled: true) button_to("Hello", "http://www.example.com", disabled: true)
) )
end end
def test_button_to_with_method_delete def test_button_to_with_method_delete
assert_dom_equal( assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input type="hidden" name="_method" value="delete" /><input type="submit" value="Hello" /></form>}, %{<form method="post" action="http://www.example.com" class="button_to"><input type="hidden" name="_method" value="delete" /><button type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", method: :delete) button_to("Hello", "http://www.example.com", method: :delete)
) )
end end
def test_button_to_with_method_get def test_button_to_with_method_get
assert_dom_equal( assert_dom_equal(
%{<form method="get" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, %{<form method="get" action="http://www.example.com" class="button_to"><button type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com", method: :get) button_to("Hello", "http://www.example.com", method: :get)
) )
end end
@ -265,11 +271,23 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_button_to_with_params def test_button_to_with_params
assert_dom_equal( assert_dom_equal(
%{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>}, %{<form action="http://www.example.com" class="button_to" method="post"><button type="submit">Hello</button><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>},
button_to("Hello", "http://www.example.com", params: { foo: :bar, baz: "quux" }) button_to("Hello", "http://www.example.com", params: { foo: :bar, baz: "quux" })
) )
end end
def test_button_to_generates_input_when_button_to_generates_button_tag_false
old_value = ActionView::Helpers::UrlHelper.button_to_generates_button_tag
ActionView::Helpers::UrlHelper.button_to_generates_button_tag = false
assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Save"/></form>},
button_to("Save", "http://www.example.com")
)
ensure
ActionView::Helpers::UrlHelper.button_to_generates_button_tag = old_value
end
class FakeParams class FakeParams
def initialize(permitted = true) def initialize(permitted = true)
@permitted = permitted @permitted = permitted
@ -290,7 +308,7 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_button_to_with_permitted_strong_params def test_button_to_with_permitted_strong_params
assert_dom_equal( assert_dom_equal(
%{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>}, %{<form action="http://www.example.com" class="button_to" method="post"><button type="submit">Hello</button><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>},
button_to("Hello", "http://www.example.com", params: FakeParams.new) button_to("Hello", "http://www.example.com", params: FakeParams.new)
) )
end end
@ -303,14 +321,14 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_button_to_with_nested_hash_params def test_button_to_with_nested_hash_params
assert_dom_equal( assert_dom_equal(
%{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[bar]" value="baz" /></form>}, %{<form action="http://www.example.com" class="button_to" method="post"><button type="submit">Hello</button><input type="hidden" name="foo[bar]" value="baz" /></form>},
button_to("Hello", "http://www.example.com", params: { foo: { bar: "baz" } }) button_to("Hello", "http://www.example.com", params: { foo: { bar: "baz" } })
) )
end end
def test_button_to_with_nested_array_params def test_button_to_with_nested_array_params
assert_dom_equal( assert_dom_equal(
%{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[]" value="bar" /></form>}, %{<form action="http://www.example.com" class="button_to" method="post"><button type="submit">Hello</button><input type="hidden" name="foo[]" value="bar" /></form>},
button_to("Hello", "http://www.example.com", params: { foo: ["bar"] }) button_to("Hello", "http://www.example.com", params: { foo: ["bar"] })
) )
end end

View file

@ -203,6 +203,10 @@ module Rails
ActiveSupport.utc_to_local_returns_utc_offset_times = true ActiveSupport.utc_to_local_returns_utc_offset_times = true
when "6.2" when "6.2"
load_defaults "6.1" load_defaults "6.1"
if respond_to?(:action_view)
action_view.button_to_generates_button_tag = true
end
else else
raise "Unknown version #{target_version.to_s.inspect}" raise "Unknown version #{target_version.to_s.inspect}"
end end

View file

@ -5,3 +5,6 @@
# Once upgraded flip defaults one by one to migrate to the new default. # Once upgraded flip defaults one by one to migrate to the new default.
# #
# Read the Guide for Upgrading Ruby on Rails for more info on each option. # Read the Guide for Upgrading Ruby on Rails for more info on each option.
# button_to view helpers consistently render <button> elements.
# Rails.application.config.action_view.button_to_generates_button_tag = false