actionview 7.0.0.alpha2 → 7.0.0.rc1

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
 
@@ -484,7 +485,16 @@ module ActionView
484
485
  # <%= form.text_field :title %>
485
486
  # <% end %>
486
487
  # # =>
487
- # <form action="/posts" method="post" data-remote="true">
488
+ # <form action="/posts" method="post">
489
+ # <input type="text" name="title">
490
+ # </form>
491
+ #
492
+ # # With an intentionally empty URL:
493
+ # <%= form_with url: false do |form| %>
494
+ # <%= form.text_field :title %>
495
+ # <% end %>
496
+ # # =>
497
+ # <form method="post" data-remote="true">
488
498
  # <input type="text" name="title">
489
499
  # </form>
490
500
  #
@@ -493,7 +503,7 @@ module ActionView
493
503
  # <%= form.text_field :title %>
494
504
  # <% end %>
495
505
  # # =>
496
- # <form action="/posts" method="post" data-remote="true">
506
+ # <form action="/posts" method="post">
497
507
  # <input type="text" name="post[title]">
498
508
  # </form>
499
509
  #
@@ -502,7 +512,7 @@ module ActionView
502
512
  # <%= form.text_field :title %>
503
513
  # <% end %>
504
514
  # # =>
505
- # <form action="/posts" method="post" data-remote="true">
515
+ # <form action="/posts" method="post">
506
516
  # <input type="text" name="post[title]">
507
517
  # </form>
508
518
  #
@@ -511,7 +521,7 @@ module ActionView
511
521
  # <%= form.text_field :title %>
512
522
  # <% end %>
513
523
  # # =>
514
- # <form action="/posts/1" method="post" data-remote="true">
524
+ # <form action="/posts/1" method="post">
515
525
  # <input type="hidden" name="_method" value="patch">
516
526
  # <input type="text" name="post[title]" value="<the title of the post>">
517
527
  # </form>
@@ -522,7 +532,7 @@ module ActionView
522
532
  # <%= form.text_field :but_in_forms_they_can %>
523
533
  # <% end %>
524
534
  # # =>
525
- # <form action="/cats" method="post" data-remote="true">
535
+ # <form action="/cats" method="post">
526
536
  # <input type="text" name="cat[cats_dont_have_gills]">
527
537
  # <input type="text" name="cat[but_in_forms_they_can]">
528
538
  # </form>
@@ -740,13 +750,14 @@ module ActionView
740
750
  # form_with(**options.merge(builder: LabellingFormBuilder), &block)
741
751
  # end
742
752
  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
753
+ options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
745
754
 
746
755
  if model
747
- url ||= polymorphic_path(model, format: format)
756
+ if url != false
757
+ url ||= polymorphic_path(model, format: format)
758
+ end
748
759
 
749
- model = model.last if model.is_a?(Array)
760
+ model = _object_for_form_builder(model)
750
761
  scope ||= model_name_from_record_or_class(model).param_key
751
762
  end
752
763
 
@@ -1005,8 +1016,9 @@ module ActionView
1005
1016
  # hidden field is not needed and you can pass <tt>include_id: false</tt>
1006
1017
  # to prevent fields_for from rendering it automatically.
1007
1018
  def fields_for(record_name, record_object = nil, options = {}, &block)
1008
- builder = instantiate_builder(record_name, record_object, options)
1009
- capture(builder, &block)
1019
+ options = { model: record_object, allow_method_names_outside_object: false, skip_default_ids: false }.merge!(options)
1020
+
1021
+ fields(record_name, **options, &block)
1010
1022
  end
1011
1023
 
1012
1024
  # Scopes input fields with either an explicit scope or model.
@@ -1055,10 +1067,10 @@ module ActionView
1055
1067
  # to work with an object as a base, like
1056
1068
  # FormOptionsHelper#collection_select and DateHelper#datetime_select.
1057
1069
  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
1070
+ options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
1060
1071
 
1061
1072
  if model
1073
+ model = _object_for_form_builder(model)
1062
1074
  scope ||= model_name_from_record_or_class(model).param_key
1063
1075
  end
1064
1076
 
@@ -1220,7 +1232,7 @@ module ActionView
1220
1232
  # file_field(:attachment, :file, class: 'file_input')
1221
1233
  # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
1222
1234
  def file_field(object_name, method, options = {})
1223
- Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
1235
+ Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(method, options.dup)).render
1224
1236
  end
1225
1237
 
1226
1238
  # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
@@ -1257,6 +1269,12 @@ module ActionView
1257
1269
  # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
1258
1270
  # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
1259
1271
  #
1272
+ # ==== Options
1273
+ #
1274
+ # * Any standard HTML attributes for the tag can be passed in, for example +:class+.
1275
+ # * <tt>:checked</tt> - +true+ or +false+ forces the state of the checkbox to be checked or not.
1276
+ # * <tt>:include_hidden</tt> - If set to false, the auxiliary hidden field described below will not be generated.
1277
+ #
1260
1278
  # ==== Gotcha
1261
1279
  #
1262
1280
  # The HTML specification says unchecked check boxes are not successful, and
@@ -1270,7 +1288,7 @@ module ActionView
1270
1288
  # wouldn't update the flag.
1271
1289
  #
1272
1290
  # 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
1291
+ # every check box. The hidden field has the same name and its
1274
1292
  # attributes mimic an unchecked check box.
1275
1293
  #
1276
1294
  # This way, the client either sends only the hidden field (representing
@@ -1294,6 +1312,8 @@ module ActionView
1294
1312
  # In that case it is preferable to either use +check_box_tag+ or to use
1295
1313
  # hashes instead of arrays.
1296
1314
  #
1315
+ # ==== Examples
1316
+ #
1297
1317
  # # Let's say that @post.validated? is 1:
1298
1318
  # check_box("post", "validated")
1299
1319
  # # => <input name="post[validated]" type="hidden" value="0" />
@@ -1540,34 +1560,24 @@ module ActionView
1540
1560
  Tags::RangeField.new(object_name, method, self, options).render
1541
1561
  end
1542
1562
 
1563
+ def _object_for_form_builder(object) # :nodoc:
1564
+ object.is_a?(Array) ? object.last : object
1565
+ end
1566
+
1543
1567
  private
1544
1568
  def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
1545
1569
  skip_enforcing_utf8: nil, **options)
1546
- html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
1570
+ html_options = options.slice(:id, :class, :multipart, :method, :data, :authenticity_token).merge!(html)
1571
+ html_options[:remote] = html.delete(:remote) || !local
1547
1572
  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
1573
+ if skip_enforcing_utf8.nil?
1574
+ if options.key?(:enforce_utf8)
1575
+ html_options[:enforce_utf8] = options[:enforce_utf8]
1576
+ end
1577
+ else
1578
+ html_options[:enforce_utf8] = !skip_enforcing_utf8
1568
1579
  end
1569
-
1570
- html_options.stringify_keys!
1580
+ html_options_for_form(url_for_options.nil? ? {} : url_for_options, html_options)
1571
1581
  end
1572
1582
 
1573
1583
  def instantiate_builder(record_name, record_object, options)
@@ -1738,6 +1748,28 @@ module ActionView
1738
1748
  @template.field_id(@object_name, method, *suffixes, index: index)
1739
1749
  end
1740
1750
 
1751
+ # Generate an HTML <tt>name</tt> attribute value for the given name and
1752
+ # field combination
1753
+ #
1754
+ # Return the value generated by the <tt>FormBuilder</tt> for the given
1755
+ # attribute name.
1756
+ #
1757
+ # <%= form_for @post do |f| %>
1758
+ # <%= f.text_field :title, name: f.field_name(:title, :subtitle) %>
1759
+ # <%# => <input type="text" name="post[title][subtitle]">
1760
+ # <% end %>
1761
+ #
1762
+ # <%= form_for @post do |f| %>
1763
+ # <%= f.field_tag :tag, name: f.field_name(:tag, multiple: true) %>
1764
+ # <%# => <input type="text" name="post[tag][]">
1765
+ # <% end %>
1766
+ #
1767
+ def field_name(method, *methods, multiple: false, index: @index)
1768
+ object_name = @options.fetch(:as) { @object_name }
1769
+
1770
+ @template.field_name(object_name, method, *methods, index: index, multiple: multiple)
1771
+ end
1772
+
1741
1773
  ##
1742
1774
  # :method: text_field
1743
1775
  #
@@ -2224,7 +2256,7 @@ module ActionView
2224
2256
  return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
2225
2257
  end
2226
2258
  else
2227
- record_object = record_name.is_a?(Array) ? record_name.last : record_name
2259
+ record_object = @template._object_for_form_builder(record_name)
2228
2260
  record_name = model_name_from_record_or_class(record_object).param_key
2229
2261
  end
2230
2262
 
@@ -2334,6 +2366,12 @@ module ActionView
2334
2366
  # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
2335
2367
  # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
2336
2368
  #
2369
+ # ==== Options
2370
+ #
2371
+ # * Any standard HTML attributes for the tag can be passed in, for example +:class+.
2372
+ # * <tt>:checked</tt> - +true+ or +false+ forces the state of the checkbox to be checked or not.
2373
+ # * <tt>:include_hidden</tt> - If set to false, the auxiliary hidden field described below will not be generated.
2374
+ #
2337
2375
  # ==== Gotcha
2338
2376
  #
2339
2377
  # The HTML specification says unchecked check boxes are not successful, and
@@ -2347,7 +2385,7 @@ module ActionView
2347
2385
  # wouldn't update the flag.
2348
2386
  #
2349
2387
  # 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
2388
+ # every check box. The hidden field has the same name and its
2351
2389
  # attributes mimic an unchecked check box.
2352
2390
  #
2353
2391
  # This way, the client either sends only the hidden field (representing
@@ -2371,6 +2409,8 @@ module ActionView
2371
2409
  # In that case it is preferable to either use +check_box_tag+ or to use
2372
2410
  # hashes instead of arrays.
2373
2411
  #
2412
+ # ==== Examples
2413
+ #
2374
2414
  # # Let's say that @post.validated? is 1:
2375
2415
  # check_box("validated")
2376
2416
  # # => <input name="post[validated]" type="hidden" value="0" />
@@ -2536,6 +2576,9 @@ module ActionView
2536
2576
  # button("Create post")
2537
2577
  # # => <button name='button' type='submit'>Create post</button>
2538
2578
  #
2579
+ # button(:draft, value: true)
2580
+ # # => <button name="post[draft]" value="true" type="submit">Create post</button>
2581
+ #
2539
2582
  # button do
2540
2583
  # content_tag(:strong, 'Ask me!')
2541
2584
  # end
@@ -2550,8 +2593,20 @@ module ActionView
2550
2593
  # # <strong>Create post</strong>
2551
2594
  # # </button>
2552
2595
  #
2596
+ # button(:draft, value: true) do
2597
+ # content_tag(:strong, "Save as draft")
2598
+ # end
2599
+ # # => <button name="post[draft]" value="true" type="submit">
2600
+ # # <strong>Save as draft</strong>
2601
+ # # </button>
2602
+ #
2553
2603
  def button(value = nil, options = {}, &block)
2554
- value, options = nil, value if value.is_a?(Hash)
2604
+ case value
2605
+ when Hash
2606
+ value, options = nil, value
2607
+ when Symbol
2608
+ value, options[:name] = nil, field_name(value)
2609
+ end
2555
2610
  value ||= submit_default_value
2556
2611
 
2557
2612
  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
@@ -114,6 +117,32 @@ module ActionView
114
117
  end
115
118
  end
116
119
 
120
+ # Generate an HTML <tt>name</tt> attribute value for the given name and
121
+ # field combination
122
+ #
123
+ # Return the value generated by the <tt>FormBuilder</tt> for the given
124
+ # attribute name.
125
+ #
126
+ # <%= text_field_tag :post, :title, name: field_name(:post, :title, :subtitle) %>
127
+ # <%# => <input type="text" name="post[title][subtitle]">
128
+ #
129
+ # <%= text_field_tag :post, :tag, name: field_name(:post, :tag, multiple: true) %>
130
+ # <%# => <input type="text" name="post[tag][]">
131
+ #
132
+ def field_name(object_name, method_name, *method_names, multiple: false, index: nil)
133
+ names = method_names.map! { |name| "[#{name}]" }.join
134
+
135
+ # a little duplication to construct fewer strings
136
+ case
137
+ when object_name.empty?
138
+ "#{method_name}#{names}#{multiple ? "[]" : ""}"
139
+ when index
140
+ "#{object_name}[#{index}][#{method_name}]#{names}#{multiple ? "[]" : ""}"
141
+ else
142
+ "#{object_name}[#{method_name}]#{names}#{multiple ? "[]" : ""}"
143
+ end
144
+ end
145
+
117
146
  # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
118
147
  # choice selection box.
119
148
  #
@@ -277,7 +306,7 @@ module ActionView
277
306
  # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
278
307
  # # type="hidden" value="" />
279
308
  def hidden_field_tag(name, value = nil, options = {})
280
- text_field_tag(name, value, options.merge(type: :hidden))
309
+ text_field_tag(name, value, options.merge(type: :hidden, autocomplete: "off"))
281
310
  end
282
311
 
283
312
  # Creates a file upload field. If you are using file uploads then you will also need
@@ -316,7 +345,7 @@ module ActionView
316
345
  # file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
317
346
  # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
318
347
  def file_field_tag(name, options = {})
319
- text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file)))
348
+ text_field_tag(name, nil, convert_direct_upload_option_to_url(name, options.merge(type: :file)))
320
349
  end
321
350
 
322
351
  # Creates a password field, a masked text field that will hide the users input behind a mask character.
@@ -866,7 +895,7 @@ module ActionView
866
895
  # Use raw HTML to ensure the value is written as an HTML entity; it
867
896
  # needs to be the right character regardless of which encoding the
868
897
  # browser infers.
869
- '<input name="utf8" type="hidden" value="&#x2713;" />'.html_safe
898
+ '<input name="utf8" type="hidden" value="&#x2713;" autocomplete="off" />'.html_safe
870
899
  end
871
900
 
872
901
  private
@@ -875,13 +904,17 @@ module ActionView
875
904
  html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
876
905
  # The following URL is unescaped, this is just a hash of options, and it is the
877
906
  # responsibility of the caller to escape all the values.
878
- html_options["action"] = url_for(url_for_options)
907
+ if url_for_options == false || html_options["action"] == false
908
+ html_options.delete("action")
909
+ else
910
+ html_options["action"] = url_for(url_for_options)
911
+ end
879
912
  html_options["accept-charset"] = "UTF-8"
880
913
 
881
914
  html_options["data-remote"] = true if html_options.delete("remote")
882
915
 
883
916
  if html_options["data-remote"] &&
884
- !embed_authenticity_token_in_remote_forms &&
917
+ embed_authenticity_token_in_remote_forms == false &&
885
918
  html_options["authenticity_token"].blank?
886
919
  # The authenticity token is taken from the meta tag in this case
887
920
  html_options["authenticity_token"] = false
@@ -954,9 +987,23 @@ module ActionView
954
987
  tag_options.delete("data-disable-with")
955
988
  end
956
989
 
957
- def convert_direct_upload_option_to_url(options)
990
+ def convert_direct_upload_option_to_url(name, options)
958
991
  if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
959
992
  options["data-direct-upload-url"] = rails_direct_uploads_url
993
+
994
+ if options[:object] && options[:object].class.respond_to?(:reflect_on_attachment)
995
+ attachment_reflection = options[:object].class.reflect_on_attachment(name)
996
+
997
+ class_with_attachment = "#{options[:object].class.name.underscore}##{name}"
998
+ options["data-direct-upload-attachment-name"] = class_with_attachment
999
+
1000
+ service_name = attachment_reflection.options[:service_name] || ActiveStorage::Blob.service.name
1001
+ options["data-direct-upload-token"] = ActiveStorage::DirectUploadToken.generate_direct_upload_token(
1002
+ class_with_attachment,
1003
+ service_name,
1004
+ session
1005
+ )
1006
+ end
960
1007
  end
961
1008
  options
962
1009
  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.
@@ -105,15 +105,7 @@ 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
111
  def tag_id(index = nil)
@@ -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
@@ -4,6 +4,10 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class HiddenField < TextField # :nodoc:
7
+ def render
8
+ @options[:autocomplete] = "off"
9
+ super
10
+ end
7
11
  end
8
12
  end
9
13
  end
@@ -15,7 +15,8 @@ module ActionView
15
15
  weekday_options_for_select(
16
16
  value || @options[:selected],
17
17
  index_as_value: @options.fetch(:index_as_value, false),
18
- day_format: @options.fetch(:day_format, :day_names)
18
+ day_format: @options.fetch(:day_format, :day_names),
19
+ beginning_of_week: @options.fetch(:beginning_of_week, Date.beginning_of_week)
19
20
  ),
20
21
  @options,
21
22
  @html_options