Skip to content

Commit e0076e4

Browse files
Merge pull request rails#42051 from seanpdoyle/form-empty-action
Support `<form>` elements without `[action]`
2 parents a92db0d + 3dae446 commit e0076e4

File tree

8 files changed

+144
-15
lines changed

8 files changed

+144
-15
lines changed

actionview/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
* Support rendering `<form>` elements _without_ `[action]` attributes by:
2+
3+
* `form_with url: false` or `form_with ..., html: { action: false }`
4+
* `form_for ..., url: false` or `form_for ..., html: { action: false }`
5+
* `form_tag false` or `form_tag ..., action: false`
6+
* `button_to "...", false` or `button_to(false) { ... }`
7+
8+
*Sean Doyle*
9+
110
* Add `:day_format` option to `date_select`
211

312
date_select("article", "written_on", day_format: ->(day) { day.ordinalize })

actionview/lib/action_view/helpers/form_helper.rb

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ module FormHelper
282282
# ...
283283
# <% end %>
284284
#
285+
# You can omit the <tt>action</tt> attribute by passing <tt>url: false</tt>:
286+
#
287+
# <%= form_for(@post, url: false) do |f| %>
288+
# ...
289+
# <% end %>
290+
#
285291
# You can also set the answer format, like this:
286292
#
287293
# <%= form_for(@post, format: :json) do |f| %>
@@ -449,7 +455,7 @@ def form_for(record, options = {}, &block)
449455
output = capture(builder, &block)
450456
html_options[:multipart] ||= builder.multipart?
451457

452-
html_options = html_options_for_form(options[:url] || {}, html_options)
458+
html_options = html_options_for_form(options.fetch(:url, {}), html_options)
453459
form_tag_with_body(html_options, output)
454460
end
455461

@@ -465,10 +471,12 @@ def apply_form_for_options!(record, object, options) # :nodoc:
465471
method: method
466472
)
467473

468-
options[:url] ||= if options.key?(:format)
469-
polymorphic_path(record, format: options.delete(:format))
470-
else
471-
polymorphic_path(record, {})
474+
if options[:url] != false
475+
options[:url] ||= if options.key?(:format)
476+
polymorphic_path(record, format: options.delete(:format))
477+
else
478+
polymorphic_path(record, {})
479+
end
472480
end
473481
end
474482
private :apply_form_for_options!
@@ -487,6 +495,15 @@ def apply_form_for_options!(record, object, options) # :nodoc:
487495
# <form action="/posts" method="post">
488496
# <input type="text" name="title">
489497
# </form>
498+
499+
# # With an intentionally empty URL:
500+
# <%= form_with url: false do |form| %>
501+
# <%= form.text_field :title %>
502+
# <% end %>
503+
# # =>
504+
# <form method="post" data-remote="true">
505+
# <input type="text" name="title">
506+
# </form>
490507
#
491508
# # Adding a scope prefixes the input field names:
492509
# <%= form_with scope: :post, url: posts_path do |form| %>
@@ -744,7 +761,9 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
744761
options[:skip_default_ids] = !form_with_generates_ids
745762

746763
if model
747-
url ||= polymorphic_path(model, format: format)
764+
if url != false
765+
url ||= polymorphic_path(model, format: format)
766+
end
748767

749768
model = model.last if model.is_a?(Array)
750769
scope ||= model_name_from_record_or_class(model).param_key
@@ -1559,7 +1578,11 @@ def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, loc
15591578

15601579
# The following URL is unescaped, this is just a hash of options, and it is the
15611580
# responsibility of the caller to escape all the values.
1562-
html_options[:action] = url_for(url_for_options || {})
1581+
if url_for_options == false || html_options[:action] == false
1582+
html_options.delete(:action)
1583+
else
1584+
html_options[:action] = url_for(url_for_options || {})
1585+
end
15631586
html_options[:"accept-charset"] = "UTF-8"
15641587
html_options[:"data-remote"] = true unless local
15651588

actionview/lib/action_view/helpers/form_tag_helper.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ module FormTagHelper
6262
#
6363
# <%= form_tag('/posts', remote: true) %>
6464
# # => <form action="/posts" method="post" data-remote="true">
65+
66+
# form_tag(false, method: :get)
67+
# # => <form method="get">
6568
#
6669
# form_tag('http://far.away.com/form', authenticity_token: false)
6770
# # form without authenticity token
@@ -875,7 +878,11 @@ def html_options_for_form(url_for_options, options)
875878
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
876879
# The following URL is unescaped, this is just a hash of options, and it is the
877880
# responsibility of the caller to escape all the values.
878-
html_options["action"] = url_for(url_for_options)
881+
if url_for_options == false || html_options["action"] == false
882+
html_options.delete("action")
883+
else
884+
html_options["action"] = url_for(url_for_options)
885+
end
879886
html_options["accept-charset"] = "UTF-8"
880887

881888
html_options["data-remote"] = true if html_options.delete("remote")

actionview/lib/action_view/helpers/url_helper.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,15 @@ def link_to(name = nil, options = nil, html_options = nil, &block)
238238
# HTTP verb via the +:method+ option within +html_options+.
239239
#
240240
# ==== Options
241-
# The +options+ hash accepts the same options as +url_for+.
241+
# The +options+ hash accepts the same options as +url_for+. To generate a
242+
# <tt><form></tt> element without an <tt>[action]</tt> attribute, pass
243+
# <tt>false</tt>:
244+
#
245+
# <%= button_to "New", false %>
246+
# # => "<form method="post" class="button_to">
247+
# # <button type="submit">New</button>
248+
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
249+
# # </form>"
242250
#
243251
# Most values in +html_options+ are passed through to the button element,
244252
# but there are a few special options:
@@ -324,11 +332,15 @@ def link_to(name = nil, options = nil, html_options = nil, &block)
324332
# #
325333
def button_to(name = nil, options = nil, html_options = nil, &block)
326334
html_options, options = options, name if block_given?
327-
options ||= {}
328335
html_options ||= {}
329336
html_options = html_options.stringify_keys
330337

331-
url = options.is_a?(String) ? options : url_for(options)
338+
url =
339+
case options
340+
when FalseClass then nil
341+
else url_for(options)
342+
end
343+
332344
remote = html_options.delete("remote")
333345
params = html_options.delete("params")
334346

actionview/test/template/form_helper/form_with_test.rb

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def form_text(action = "http://www.example.com", local: false, **options)
5353

5454
method = method.to_s == "get" ? "get" : "post"
5555

56-
txt = +%{<form accept-charset="UTF-8" action="#{action}"}
56+
txt = +%{<form accept-charset="UTF-8"} + (action ? %{ action="#{action}"} : "")
5757
txt << %{ enctype="multipart/form-data"} if enctype
5858
txt << %{ data-remote="true"} unless local
5959
txt << %{ class="#{html_class}"} if html_class
@@ -107,6 +107,22 @@ def test_form_with_with_method_delete
107107
assert_dom_equal expected, actual
108108
end
109109

110+
def test_form_with_false_url
111+
actual = form_with(url: false)
112+
113+
expected = whole_form(false)
114+
115+
assert_dom_equal expected, actual
116+
end
117+
118+
def test_form_with_false_action
119+
actual = form_with(html: { action: false })
120+
121+
expected = whole_form(false)
122+
123+
assert_dom_equal expected, actual
124+
end
125+
110126
def test_form_with_with_local_true
111127
actual = form_with(local: true)
112128

@@ -445,6 +461,22 @@ def test_form_with_general_attributes
445461
assert_dom_equal expected, output_buffer
446462
end
447463

464+
def test_form_with_false_url
465+
form_with(url: false)
466+
467+
expected = whole_form(false)
468+
469+
assert_dom_equal expected, output_buffer
470+
end
471+
472+
def test_form_with_model_and_false_url
473+
form_with(model: Post.new, url: false)
474+
475+
expected = whole_form(false)
476+
477+
assert_dom_equal expected, output_buffer
478+
end
479+
448480
def test_form_with_attribute_not_on_model
449481
form_with(model: @post) do |f|
450482
concat f.text_field :this_dont_exist_on_post
@@ -2398,7 +2430,7 @@ def hidden_fields(options = {})
23982430
end
23992431

24002432
def form_text(action = "/", id = nil, html_class = nil, local = nil, multipart = nil, method = nil)
2401-
txt = +%{<form accept-charset="UTF-8" action="#{action}"}
2433+
txt = +%{<form accept-charset="UTF-8"} + (action ? %{ action="#{action}"} : "")
24022434
txt << %{ enctype="multipart/form-data"} if multipart
24032435
txt << %{ data-remote="true"} unless local
24042436
txt << %{ class="#{html_class}"} if html_class

actionview/test/template/form_helper_test.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,24 @@ def test_form_for_id
16171617
assert_dom_equal expected, output_buffer
16181618
end
16191619

1620+
def test_form_for_false_url
1621+
form_for(Post.new, url: false) do |form|
1622+
end
1623+
1624+
expected = whole_form(false, "new_post", "new_post")
1625+
1626+
assert_dom_equal expected, output_buffer
1627+
end
1628+
1629+
def test_form_for_false_action
1630+
form_for(Post.new, html: { action: false }) do |form|
1631+
end
1632+
1633+
expected = whole_form(false, "new_post", "new_post")
1634+
1635+
assert_dom_equal expected, output_buffer
1636+
end
1637+
16201638
def test_field_id_with_model
16211639
value = field_id(Post.new, :title)
16221640

@@ -3738,7 +3756,7 @@ def hidden_fields(options = {})
37383756
end
37393757

37403758
def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
3741-
txt = +%{<form accept-charset="UTF-8" action="#{action}"}
3759+
txt = +%{<form accept-charset="UTF-8"} + (action ? %{ action="#{action}"} : "")
37423760
txt << %{ enctype="multipart/form-data"} if multipart
37433761
txt << %{ data-remote="true"} if remote
37443762
txt << %{ class="#{html_class}"} if html_class

actionview/test/template/form_tag_helper_test.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def form_text(action = "http://www.example.com", options = {})
4242

4343
method = method.to_s == "get" ? "get" : "post"
4444

45-
txt = +%{<form accept-charset="UTF-8" action="#{action}"}
45+
txt = +%{<form accept-charset="UTF-8"} + (action ? %{ action="#{action}"} : "")
4646
txt << %{ enctype="multipart/form-data"} if enctype
4747
txt << %{ data-remote="true"} if remote
4848
txt << %{ class="#{html_class}"} if html_class
@@ -138,6 +138,20 @@ def test_form_tag_with_remote_false
138138
assert_dom_equal expected, actual
139139
end
140140

141+
def test_form_tag_with_false_url_for_options
142+
actual = form_tag(false)
143+
144+
expected = whole_form(false)
145+
assert_dom_equal expected, actual
146+
end
147+
148+
def test_form_tag_with_false_action
149+
actual = form_tag({}, action: false)
150+
151+
expected = whole_form(false)
152+
assert_dom_equal expected, actual
153+
end
154+
141155
def test_form_tag_enforce_utf8_true
142156
actual = form_tag({}, { enforce_utf8: true })
143157
expected = whole_form("http://www.example.com", enforce_utf8: true)

actionview/test/template/url_helper_test.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,20 @@ def test_button_to_with_path
161161
)
162162
end
163163

164+
def test_button_to_with_false_url
165+
assert_dom_equal(
166+
%{<form method="post" class="button_to"><button type="submit">Hello</button></form>},
167+
button_to("Hello", false)
168+
)
169+
end
170+
171+
def test_button_to_with_false_url_and_block
172+
assert_dom_equal(
173+
%{<form method="post" class="button_to"><button type="submit">Hello</button></form>},
174+
button_to(false) { "Hello" }
175+
)
176+
end
177+
164178
def test_button_to_with_straight_url_and_request_forgery
165179
self.request_forgery = true
166180

0 commit comments

Comments
 (0)