actionview 7.0.0.alpha2 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

@@ -282,6 +282,12 @@ module ActionView
282
282
  # ...
283
283
  # <% end %>
284
284
  #
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
+ #
285
291
  # You can also set the answer format, like this:
286
292
  #
287
293
  # <%= form_for(@post, format: :json) do |f| %>
@@ -426,50 +432,45 @@ module ActionView
426
432
  # <% end %>
427
433
  def form_for(record, options = {}, &block)
428
434
  raise ArgumentError, "Missing block" unless block_given?
429
- html_options = options[:html] ||= {}
430
435
 
431
436
  case record
432
437
  when String, Symbol
438
+ model = nil
433
439
  object_name = record
434
- object = nil
435
440
  else
436
- object = record.is_a?(Array) ? record.last : record
441
+ model = record
442
+ object = _object_for_form_builder(record)
437
443
  raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
438
444
  object_name = options[:as] || model_name_from_record_or_class(object).param_key
439
- apply_form_for_options!(record, object, options)
445
+ apply_form_for_options!(object, options)
440
446
  end
441
447
 
442
- html_options[:data] = options.delete(:data) if options.has_key?(:data)
443
- html_options[:remote] = options.delete(:remote) if options.has_key?(:remote)
444
- html_options[:method] = options.delete(:method) if options.has_key?(:method)
445
- html_options[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8)
446
- html_options[:authenticity_token] = options.delete(:authenticity_token)
448
+ remote = options.delete(:remote)
447
449
 
448
- builder = instantiate_builder(object_name, object, options)
449
- output = capture(builder, &block)
450
- html_options[:multipart] ||= builder.multipart?
450
+ if remote && !embed_authenticity_token_in_remote_forms && options[:authenticity_token].blank?
451
+ options[:authenticity_token] = false
452
+ end
453
+
454
+ options[:model] = model
455
+ options[:scope] = object_name
456
+ options[:local] = !remote
457
+ options[:skip_default_ids] = false
458
+ options[:allow_method_names_outside_object] = options.fetch(:allow_method_names_outside_object, false)
451
459
 
452
- html_options = html_options_for_form(options[:url] || {}, html_options)
453
- form_tag_with_body(html_options, output)
460
+ form_with(**options, &block)
454
461
  end
455
462
 
456
- def apply_form_for_options!(record, object, options) # :nodoc:
463
+ def apply_form_for_options!(object, options) # :nodoc:
457
464
  object = convert_to_model(object)
458
465
 
459
466
  as = options[:as]
460
467
  namespace = options[:namespace]
461
- action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
468
+ action = object.respond_to?(:persisted?) && object.persisted? ? :edit : :new
469
+ options[:html] ||= {}
462
470
  options[:html].reverse_merge!(
463
471
  class: as ? "#{action}_#{as}" : dom_class(object, action),
464
472
  id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
465
- method: method
466
473
  )
467
-
468
- options[:url] ||= if options.key?(:format)
469
- polymorphic_path(record, format: options.delete(:format))
470
- else
471
- polymorphic_path(record, {})
472
- end
473
474
  end
474
475
  private :apply_form_for_options!
475
476
 
@@ -477,6 +478,8 @@ module ActionView
477
478
 
478
479
  mattr_accessor :form_with_generates_ids, default: false
479
480
 
481
+ mattr_accessor :multiple_file_field_include_hidden, default: false
482
+
480
483
  # Creates a form tag based on mixing URLs, scopes, or models.
481
484
  #
482
485
  # # Using just a URL:
@@ -484,7 +487,16 @@ module ActionView
484
487
  # <%= form.text_field :title %>
485
488
  # <% end %>
486
489
  # # =>
487
- # <form action="/posts" method="post" data-remote="true">
490
+ # <form action="/posts" method="post">
491
+ # <input type="text" name="title">
492
+ # </form>
493
+ #
494
+ # # With an intentionally empty URL:
495
+ # <%= form_with url: false do |form| %>
496
+ # <%= form.text_field :title %>
497
+ # <% end %>
498
+ # # =>
499
+ # <form method="post" data-remote="true">
488
500
  # <input type="text" name="title">
489
501
  # </form>
490
502
  #
@@ -493,7 +505,7 @@ module ActionView
493
505
  # <%= form.text_field :title %>
494
506
  # <% end %>
495
507
  # # =>
496
- # <form action="/posts" method="post" data-remote="true">
508
+ # <form action="/posts" method="post">
497
509
  # <input type="text" name="post[title]">
498
510
  # </form>
499
511
  #
@@ -502,7 +514,7 @@ module ActionView
502
514
  # <%= form.text_field :title %>
503
515
  # <% end %>
504
516
  # # =>
505
- # <form action="/posts" method="post" data-remote="true">
517
+ # <form action="/posts" method="post">
506
518
  # <input type="text" name="post[title]">
507
519
  # </form>
508
520
  #
@@ -511,7 +523,7 @@ module ActionView
511
523
  # <%= form.text_field :title %>
512
524
  # <% end %>
513
525
  # # =>
514
- # <form action="/posts/1" method="post" data-remote="true">
526
+ # <form action="/posts/1" method="post">
515
527
  # <input type="hidden" name="_method" value="patch">
516
528
  # <input type="text" name="post[title]" value="<the title of the post>">
517
529
  # </form>
@@ -522,7 +534,7 @@ module ActionView
522
534
  # <%= form.text_field :but_in_forms_they_can %>
523
535
  # <% end %>
524
536
  # # =>
525
- # <form action="/cats" method="post" data-remote="true">
537
+ # <form action="/cats" method="post">
526
538
  # <input type="text" name="cat[cats_dont_have_gills]">
527
539
  # <input type="text" name="cat[but_in_forms_they_can]">
528
540
  # </form>
@@ -740,13 +752,14 @@ module ActionView
740
752
  # form_with(**options.merge(builder: LabellingFormBuilder), &block)
741
753
  # end
742
754
  def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
743
- options[:allow_method_names_outside_object] = true
744
- options[:skip_default_ids] = !form_with_generates_ids
755
+ options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
745
756
 
746
757
  if model
747
- url ||= polymorphic_path(model, format: format)
758
+ if url != false
759
+ url ||= polymorphic_path(model, format: format)
760
+ end
748
761
 
749
- model = model.last if model.is_a?(Array)
762
+ model = _object_for_form_builder(model)
750
763
  scope ||= model_name_from_record_or_class(model).param_key
751
764
  end
752
765
 
@@ -1005,8 +1018,9 @@ module ActionView
1005
1018
  # hidden field is not needed and you can pass <tt>include_id: false</tt>
1006
1019
  # to prevent fields_for from rendering it automatically.
1007
1020
  def fields_for(record_name, record_object = nil, options = {}, &block)
1008
- builder = instantiate_builder(record_name, record_object, options)
1009
- capture(builder, &block)
1021
+ options = { model: record_object, allow_method_names_outside_object: false, skip_default_ids: false }.merge!(options)
1022
+
1023
+ fields(record_name, **options, &block)
1010
1024
  end
1011
1025
 
1012
1026
  # Scopes input fields with either an explicit scope or model.
@@ -1055,10 +1069,10 @@ module ActionView
1055
1069
  # to work with an object as a base, like
1056
1070
  # FormOptionsHelper#collection_select and DateHelper#datetime_select.
1057
1071
  def fields(scope = nil, model: nil, **options, &block)
1058
- options[:allow_method_names_outside_object] = true
1059
- options[:skip_default_ids] = !form_with_generates_ids
1072
+ options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
1060
1073
 
1061
1074
  if model
1075
+ model = _object_for_form_builder(model)
1062
1076
  scope ||= model_name_from_record_or_class(model).param_key
1063
1077
  end
1064
1078
 
@@ -1202,6 +1216,7 @@ module ActionView
1202
1216
  # * Creates standard HTML attributes for the tag.
1203
1217
  # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
1204
1218
  # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
1219
+ # * <tt>:include_hidden</tt> - When <tt>multiple: true</tt> and <tt>include_hidden: true</tt>, the field will be prefixed with an <tt><input type="hidden"></tt> field with an empty value to support submitting an empty collection of files.
1205
1220
  # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
1206
1221
  #
1207
1222
  # ==== Examples
@@ -1220,7 +1235,9 @@ module ActionView
1220
1235
  # file_field(:attachment, :file, class: 'file_input')
1221
1236
  # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
1222
1237
  def file_field(object_name, method, options = {})
1223
- Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
1238
+ options = { include_hidden: multiple_file_field_include_hidden }.merge!(options)
1239
+
1240
+ Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(method, options)).render
1224
1241
  end
1225
1242
 
1226
1243
  # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
@@ -1257,6 +1274,12 @@ module ActionView
1257
1274
  # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
1258
1275
  # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
1259
1276
  #
1277
+ # ==== Options
1278
+ #
1279
+ # * Any standard HTML attributes for the tag can be passed in, for example +:class+.
1280
+ # * <tt>:checked</tt> - +true+ or +false+ forces the state of the checkbox to be checked or not.
1281
+ # * <tt>:include_hidden</tt> - If set to false, the auxiliary hidden field described below will not be generated.
1282
+ #
1260
1283
  # ==== Gotcha
1261
1284
  #
1262
1285
  # The HTML specification says unchecked check boxes are not successful, and
@@ -1270,7 +1293,7 @@ module ActionView
1270
1293
  # wouldn't update the flag.
1271
1294
  #
1272
1295
  # To prevent this the helper generates an auxiliary hidden field before
1273
- # the very check box. The hidden field has the same name and its
1296
+ # every check box. The hidden field has the same name and its
1274
1297
  # attributes mimic an unchecked check box.
1275
1298
  #
1276
1299
  # This way, the client either sends only the hidden field (representing
@@ -1294,6 +1317,8 @@ module ActionView
1294
1317
  # In that case it is preferable to either use +check_box_tag+ or to use
1295
1318
  # hashes instead of arrays.
1296
1319
  #
1320
+ # ==== Examples
1321
+ #
1297
1322
  # # Let's say that @post.validated? is 1:
1298
1323
  # check_box("post", "validated")
1299
1324
  # # => <input name="post[validated]" type="hidden" value="0" />
@@ -1540,34 +1565,24 @@ module ActionView
1540
1565
  Tags::RangeField.new(object_name, method, self, options).render
1541
1566
  end
1542
1567
 
1568
+ def _object_for_form_builder(object) # :nodoc:
1569
+ object.is_a?(Array) ? object.last : object
1570
+ end
1571
+
1543
1572
  private
1544
1573
  def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
1545
1574
  skip_enforcing_utf8: nil, **options)
1546
- html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
1575
+ html_options = options.slice(:id, :class, :multipart, :method, :data, :authenticity_token).merge!(html)
1576
+ html_options[:remote] = html.delete(:remote) || !local
1547
1577
  html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
1548
- html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil?
1549
-
1550
- html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart)
1551
-
1552
- # The following URL is unescaped, this is just a hash of options, and it is the
1553
- # responsibility of the caller to escape all the values.
1554
- html_options[:action] = url_for(url_for_options || {})
1555
- html_options[:"accept-charset"] = "UTF-8"
1556
- html_options[:"data-remote"] = true unless local
1557
-
1558
- html_options[:authenticity_token] = options.delete(:authenticity_token)
1559
-
1560
- if !local && html_options[:authenticity_token].blank?
1561
- html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms
1562
- end
1563
-
1564
- if html_options[:authenticity_token] == true
1565
- # Include the default authenticity_token, which is only generated when it's set to nil,
1566
- # but we needed the true value to override the default of no authenticity_token on data-remote.
1567
- html_options[:authenticity_token] = nil
1578
+ if skip_enforcing_utf8.nil?
1579
+ if options.key?(:enforce_utf8)
1580
+ html_options[:enforce_utf8] = options[:enforce_utf8]
1581
+ end
1582
+ else
1583
+ html_options[:enforce_utf8] = !skip_enforcing_utf8
1568
1584
  end
1569
-
1570
- html_options.stringify_keys!
1585
+ html_options_for_form(url_for_options.nil? ? {} : url_for_options, html_options)
1571
1586
  end
1572
1587
 
1573
1588
  def instantiate_builder(record_name, record_object, options)
@@ -1734,8 +1749,30 @@ module ActionView
1734
1749
  # <tt>aria-describedby</tt> attribute referencing the <tt><span></tt>
1735
1750
  # element, sharing a common <tt>id</tt> root (<tt>post_title</tt>, in this
1736
1751
  # case).
1737
- def field_id(method, *suffixes, index: @index)
1738
- @template.field_id(@object_name, method, *suffixes, index: index)
1752
+ def field_id(method, *suffixes, namespace: @options[:namespace], index: @index)
1753
+ @template.field_id(@object_name, method, *suffixes, namespace: namespace, index: index)
1754
+ end
1755
+
1756
+ # Generate an HTML <tt>name</tt> attribute value for the given name and
1757
+ # field combination
1758
+ #
1759
+ # Return the value generated by the <tt>FormBuilder</tt> for the given
1760
+ # attribute name.
1761
+ #
1762
+ # <%= form_for @post do |f| %>
1763
+ # <%= f.text_field :title, name: f.field_name(:title, :subtitle) %>
1764
+ # <%# => <input type="text" name="post[title][subtitle]">
1765
+ # <% end %>
1766
+ #
1767
+ # <%= form_for @post do |f| %>
1768
+ # <%= f.field_tag :tag, name: f.field_name(:tag, multiple: true) %>
1769
+ # <%# => <input type="text" name="post[tag][]">
1770
+ # <% end %>
1771
+ #
1772
+ def field_name(method, *methods, multiple: false, index: @index)
1773
+ object_name = @options.fetch(:as) { @object_name }
1774
+
1775
+ @template.field_name(object_name, method, *methods, index: index, multiple: multiple)
1739
1776
  end
1740
1777
 
1741
1778
  ##
@@ -2224,7 +2261,7 @@ module ActionView
2224
2261
  return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
2225
2262
  end
2226
2263
  else
2227
- record_object = record_name.is_a?(Array) ? record_name.last : record_name
2264
+ record_object = @template._object_for_form_builder(record_name)
2228
2265
  record_name = model_name_from_record_or_class(record_object).param_key
2229
2266
  end
2230
2267
 
@@ -2334,6 +2371,12 @@ module ActionView
2334
2371
  # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
2335
2372
  # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
2336
2373
  #
2374
+ # ==== Options
2375
+ #
2376
+ # * Any standard HTML attributes for the tag can be passed in, for example +:class+.
2377
+ # * <tt>:checked</tt> - +true+ or +false+ forces the state of the checkbox to be checked or not.
2378
+ # * <tt>:include_hidden</tt> - If set to false, the auxiliary hidden field described below will not be generated.
2379
+ #
2337
2380
  # ==== Gotcha
2338
2381
  #
2339
2382
  # The HTML specification says unchecked check boxes are not successful, and
@@ -2347,7 +2390,7 @@ module ActionView
2347
2390
  # wouldn't update the flag.
2348
2391
  #
2349
2392
  # To prevent this the helper generates an auxiliary hidden field before
2350
- # the very check box. The hidden field has the same name and its
2393
+ # every check box. The hidden field has the same name and its
2351
2394
  # attributes mimic an unchecked check box.
2352
2395
  #
2353
2396
  # This way, the client either sends only the hidden field (representing
@@ -2371,6 +2414,8 @@ module ActionView
2371
2414
  # In that case it is preferable to either use +check_box_tag+ or to use
2372
2415
  # hashes instead of arrays.
2373
2416
  #
2417
+ # ==== Examples
2418
+ #
2374
2419
  # # Let's say that @post.validated? is 1:
2375
2420
  # check_box("validated")
2376
2421
  # # => <input name="post[validated]" type="hidden" value="0" />
@@ -2445,6 +2490,7 @@ module ActionView
2445
2490
  # * Creates standard HTML attributes for the tag.
2446
2491
  # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
2447
2492
  # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
2493
+ # * <tt>:include_hidden</tt> - When <tt>multiple: true</tt> and <tt>include_hidden: true</tt>, the field will be prefixed with an <tt><input type="hidden"></tt> field with an empty value to support submitting an empty collection of files.
2448
2494
  # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
2449
2495
  #
2450
2496
  # ==== Examples
@@ -2536,6 +2582,9 @@ module ActionView
2536
2582
  # button("Create post")
2537
2583
  # # => <button name='button' type='submit'>Create post</button>
2538
2584
  #
2585
+ # button(:draft, value: true)
2586
+ # # => <button id="post_draft" name="post[draft]" value="true" type="submit">Create post</button>
2587
+ #
2539
2588
  # button do
2540
2589
  # content_tag(:strong, 'Ask me!')
2541
2590
  # end
@@ -2550,8 +2599,20 @@ module ActionView
2550
2599
  # # <strong>Create post</strong>
2551
2600
  # # </button>
2552
2601
  #
2602
+ # button(:draft, value: true) do
2603
+ # content_tag(:strong, "Save as draft")
2604
+ # end
2605
+ # # => <button id="post_draft" name="post[draft]" value="true" type="submit">
2606
+ # # <strong>Save as draft</strong>
2607
+ # # </button>
2608
+ #
2553
2609
  def button(value = nil, options = {}, &block)
2554
- value, options = nil, value if value.is_a?(Hash)
2610
+ case value
2611
+ when Hash
2612
+ value, options = nil, value
2613
+ when Symbol
2614
+ value, options = nil, { name: field_name(value), id: field_id(value) }.merge!(options.to_h)
2615
+ end
2555
2616
  value ||= submit_default_value
2556
2617
 
2557
2618
  if block_given?
@@ -597,15 +597,18 @@ module ActionView
597
597
  # Returns a string of option tags for the days of the week.
598
598
  #
599
599
  # Options:
600
- # * <tt>:index_as_value</tt> - Defaults to false, set to true to use the index of the weekday as the value.
600
+ # * <tt>:index_as_value</tt> - Defaults to false, set to true to use the indexes from
601
+ # `I18n.translate("date.day_names")` as the values. By default, Sunday is always 0.
601
602
  # * <tt>:day_format</tt> - The I18n key of the array to use for the weekday options.
602
603
  # Defaults to :day_names, set to :abbr_day_names for abbreviations.
604
+ # * <tt>:beginning_of_week</tt> - Defaults to Date.beginning_of_week.
603
605
  #
604
606
  # NOTE: Only the option tags are returned, you have to wrap this call in
605
607
  # a regular HTML select tag.
606
- def weekday_options_for_select(selected = nil, index_as_value: false, day_format: :day_names)
608
+ def weekday_options_for_select(selected = nil, index_as_value: false, day_format: :day_names, beginning_of_week: Date.beginning_of_week)
607
609
  day_names = I18n.translate("date.#{day_format}")
608
- day_names = day_names.map.with_index.to_h if index_as_value
610
+ day_names = day_names.map.with_index.to_a if index_as_value
611
+ day_names = day_names.rotate(Date::DAYS_INTO_WEEK.fetch(beginning_of_week))
609
612
 
610
613
  options_for_select(day_names, selected)
611
614
  end
@@ -62,6 +62,9 @@ module ActionView
62
62
  #
63
63
  # <%= form_tag('/posts', remote: true) %>
64
64
  # # => <form action="/posts" method="post" data-remote="true">
65
+
66
+ # form_tag(false, method: :get)
67
+ # # => <form method="get">
65
68
  #
66
69
  # form_tag('http://far.away.com/form', authenticity_token: false)
67
70
  # # form without authenticity token
@@ -93,7 +96,7 @@ module ActionView
93
96
  # <tt>aria-describedby</tt> attribute referencing the <tt><span></tt>
94
97
  # element, sharing a common <tt>id</tt> root (<tt>post_title</tt>, in this
95
98
  # case).
96
- def field_id(object_name, method_name, *suffixes, index: nil)
99
+ def field_id(object_name, method_name, *suffixes, index: nil, namespace: nil)
97
100
  if object_name.respond_to?(:model_name)
98
101
  object_name = object_name.model_name.singular
99
102
  end
@@ -102,15 +105,38 @@ module ActionView
102
105
 
103
106
  sanitized_method_name = method_name.to_s.delete_suffix("?")
104
107
 
108
+ [
109
+ namespace,
110
+ sanitized_object_name.presence,
111
+ (index unless sanitized_object_name.empty?),
112
+ sanitized_method_name,
113
+ *suffixes,
114
+ ].tap(&:compact!).join("_")
115
+ end
116
+
117
+ # Generate an HTML <tt>name</tt> attribute value for the given name and
118
+ # field combination
119
+ #
120
+ # Return the value generated by the <tt>FormBuilder</tt> for the given
121
+ # attribute name.
122
+ #
123
+ # <%= text_field_tag :post, :title, name: field_name(:post, :title, :subtitle) %>
124
+ # <%# => <input type="text" name="post[title][subtitle]">
125
+ #
126
+ # <%= text_field_tag :post, :tag, name: field_name(:post, :tag, multiple: true) %>
127
+ # <%# => <input type="text" name="post[tag][]">
128
+ #
129
+ def field_name(object_name, method_name, *method_names, multiple: false, index: nil)
130
+ names = method_names.map! { |name| "[#{name}]" }.join
131
+
105
132
  # a little duplication to construct fewer strings
106
- if sanitized_object_name.empty?
107
- sanitized_method_name
108
- elsif suffixes.any?
109
- [sanitized_object_name, index, sanitized_method_name, *suffixes].compact.join("_")
110
- elsif index
111
- "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
133
+ case
134
+ when object_name.empty?
135
+ "#{method_name}#{names}#{multiple ? "[]" : ""}"
136
+ when index
137
+ "#{object_name}[#{index}][#{method_name}]#{names}#{multiple ? "[]" : ""}"
112
138
  else
113
- "#{sanitized_object_name}_#{sanitized_method_name}"
139
+ "#{object_name}[#{method_name}]#{names}#{multiple ? "[]" : ""}"
114
140
  end
115
141
  end
116
142
 
@@ -277,7 +303,7 @@ module ActionView
277
303
  # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
278
304
  # # type="hidden" value="" />
279
305
  def hidden_field_tag(name, value = nil, options = {})
280
- text_field_tag(name, value, options.merge(type: :hidden))
306
+ text_field_tag(name, value, options.merge(type: :hidden, autocomplete: "off"))
281
307
  end
282
308
 
283
309
  # Creates a file upload field. If you are using file uploads then you will also need
@@ -316,7 +342,7 @@ module ActionView
316
342
  # file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
317
343
  # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
318
344
  def file_field_tag(name, options = {})
319
- text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file)))
345
+ text_field_tag(name, nil, convert_direct_upload_option_to_url(name, options.merge(type: :file)))
320
346
  end
321
347
 
322
348
  # Creates a password field, a masked text field that will hide the users input behind a mask character.
@@ -866,7 +892,7 @@ module ActionView
866
892
  # Use raw HTML to ensure the value is written as an HTML entity; it
867
893
  # needs to be the right character regardless of which encoding the
868
894
  # browser infers.
869
- '<input name="utf8" type="hidden" value="&#x2713;" />'.html_safe
895
+ '<input name="utf8" type="hidden" value="&#x2713;" autocomplete="off" />'.html_safe
870
896
  end
871
897
 
872
898
  private
@@ -875,13 +901,17 @@ module ActionView
875
901
  html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
876
902
  # The following URL is unescaped, this is just a hash of options, and it is the
877
903
  # responsibility of the caller to escape all the values.
878
- html_options["action"] = url_for(url_for_options)
904
+ if url_for_options == false || html_options["action"] == false
905
+ html_options.delete("action")
906
+ else
907
+ html_options["action"] = url_for(url_for_options)
908
+ end
879
909
  html_options["accept-charset"] = "UTF-8"
880
910
 
881
911
  html_options["data-remote"] = true if html_options.delete("remote")
882
912
 
883
913
  if html_options["data-remote"] &&
884
- !embed_authenticity_token_in_remote_forms &&
914
+ embed_authenticity_token_in_remote_forms == false &&
885
915
  html_options["authenticity_token"].blank?
886
916
  # The authenticity token is taken from the meta tag in this case
887
917
  html_options["authenticity_token"] = false
@@ -954,9 +984,23 @@ module ActionView
954
984
  tag_options.delete("data-disable-with")
955
985
  end
956
986
 
957
- def convert_direct_upload_option_to_url(options)
987
+ def convert_direct_upload_option_to_url(name, options)
958
988
  if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
959
989
  options["data-direct-upload-url"] = rails_direct_uploads_url
990
+
991
+ if options[:object] && options[:object].class.respond_to?(:reflect_on_attachment)
992
+ attachment_reflection = options[:object].class.reflect_on_attachment(name)
993
+
994
+ class_with_attachment = "#{options[:object].class.name.underscore}##{name}"
995
+ options["data-direct-upload-attachment-name"] = class_with_attachment
996
+
997
+ service_name = attachment_reflection.options[:service_name] || ActiveStorage::Blob.service.name
998
+ options["data-direct-upload-token"] = ActiveStorage::DirectUploadToken.generate_direct_upload_token(
999
+ class_with_attachment,
1000
+ service_name,
1001
+ session
1002
+ )
1003
+ end
960
1004
  end
961
1005
  options
962
1006
  end
@@ -450,6 +450,7 @@ module ActionView
450
450
  def parse_float(number, raise_error)
451
451
  result = Float(number, exception: false)
452
452
  raise InvalidNumberError, number if result.nil? && raise_error
453
+ result
453
454
  end
454
455
  end
455
456
  end
@@ -45,7 +45,8 @@ module ActionView
45
45
  include CaptureHelper
46
46
  include OutputSafetyHelper
47
47
 
48
- VOID_ELEMENTS = %i(area base br col embed hr img input keygen link meta param source track wbr).to_set
48
+ HTML_VOID_ELEMENTS = %i(area base br col circle embed hr img input keygen link meta param source track wbr).to_set
49
+ SVG_VOID_ELEMENTS = %i(animate animateMotion animateTransform circle ellipse line path polygon polyline rect set stop use view).to_set
49
50
 
50
51
  def initialize(view_context)
51
52
  @view_context = view_context
@@ -66,7 +67,7 @@ module ActionView
66
67
 
67
68
  def tag_string(name, content = nil, escape_attributes: true, **options, &block)
68
69
  content = @view_context.capture(self, &block) if block_given?
69
- if VOID_ELEMENTS.include?(name) && content.nil?
70
+ if (HTML_VOID_ELEMENTS.include?(name) || SVG_VOID_ELEMENTS.include?(name)) && content.nil?
70
71
  "<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe
71
72
  else
72
73
  content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes)
@@ -234,6 +235,20 @@ module ActionView
234
235
  # # A void element:
235
236
  # tag.br # => <br>
236
237
  #
238
+ # === Building HTML attributes
239
+ #
240
+ # Transforms a Hash into HTML attributes, ready to be interpolated into
241
+ # ERB. Includes or omits boolean attributes based on their truthiness.
242
+ # Transforms keys nested within
243
+ # <tt>aria:</tt> or <tt>data:</tt> objects into `aria-` and `data-`
244
+ # prefixed attributes:
245
+ #
246
+ # <input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>
247
+ # # => <input type="text" aria-label="Search">
248
+ #
249
+ # <button <%= tag.attributes id: "call-to-action", disabled: false, aria: { expanded: false } %> class="primary">Get Started!</button>
250
+ # # => <button id="call-to-action" aria-expanded="false" class="primary">Get Started!</button>
251
+ #
237
252
  # === Legacy syntax
238
253
  #
239
254
  # The following format is for legacy syntax support. It will be deprecated in future versions of Rails.
@@ -97,7 +97,7 @@ module ActionView
97
97
  options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
98
98
 
99
99
  if generate_ids?
100
- options["id"] = options.fetch("id") { tag_id(index) }
100
+ options["id"] = options.fetch("id") { tag_id(index, options.delete("namespace")) }
101
101
  if namespace = options.delete("namespace")
102
102
  options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
103
103
  end
@@ -105,19 +105,11 @@ module ActionView
105
105
  end
106
106
 
107
107
  def tag_name(multiple = false, index = nil)
108
- # a little duplication to construct fewer strings
109
- case
110
- when @object_name.empty?
111
- "#{sanitized_method_name}#{multiple ? "[]" : ""}"
112
- when index
113
- "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}"
114
- else
115
- "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}"
116
- end
108
+ @template_object.field_name(@object_name, sanitized_method_name, multiple: multiple, index: index)
117
109
  end
118
110
 
119
- def tag_id(index = nil)
120
- @template_object.field_id(@object_name, @method_name, index: index)
111
+ def tag_id(index = nil, namespace = nil)
112
+ @template_object.field_id(@object_name, @method_name, index: index, namespace: namespace)
121
113
  end
122
114
 
123
115
  def sanitized_method_name
@@ -141,7 +133,7 @@ module ActionView
141
133
  select = content_tag("select", add_options(option_tags, options, value), html_options)
142
134
 
143
135
  if html_options["multiple"] && options.fetch(:include_hidden, true)
144
- tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select
136
+ tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "", autocomplete: "off") + select
145
137
  else
146
138
  select
147
139
  end
@@ -57,7 +57,7 @@ module ActionView
57
57
  end
58
58
 
59
59
  def hidden_field_for_checkbox(options)
60
- @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe
60
+ @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value, "autocomplete" => "off")) : "".html_safe
61
61
  end
62
62
  end
63
63
  end