actionview 6.1.7.2 → 7.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -277
  3. data/MIT-LICENSE +2 -1
  4. data/README.rdoc +3 -3
  5. data/app/assets/javascripts/rails-ujs.esm.js +686 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +37 -19
  8. data/lib/action_view/buffers.rb +107 -9
  9. data/lib/action_view/cache_expiry.rb +48 -37
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  12. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  13. data/lib/action_view/dependency_tracker.rb +6 -147
  14. data/lib/action_view/deprecator.rb +7 -0
  15. data/lib/action_view/digestor.rb +8 -5
  16. data/lib/action_view/flows.rb +4 -4
  17. data/lib/action_view/gem_version.rb +4 -4
  18. data/lib/action_view/helpers/active_model_helper.rb +3 -3
  19. data/lib/action_view/helpers/asset_tag_helper.rb +200 -60
  20. data/lib/action_view/helpers/asset_url_helper.rb +22 -21
  21. data/lib/action_view/helpers/atom_feed_helper.rb +8 -9
  22. data/lib/action_view/helpers/cache_helper.rb +55 -12
  23. data/lib/action_view/helpers/capture_helper.rb +34 -14
  24. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  25. data/lib/action_view/helpers/controller_helper.rb +8 -2
  26. data/lib/action_view/helpers/csp_helper.rb +3 -3
  27. data/lib/action_view/helpers/csrf_helper.rb +4 -4
  28. data/lib/action_view/helpers/date_helper.rb +123 -57
  29. data/lib/action_view/helpers/debug_helper.rb +6 -4
  30. data/lib/action_view/helpers/form_helper.rb +253 -97
  31. data/lib/action_view/helpers/form_options_helper.rb +72 -34
  32. data/lib/action_view/helpers/form_tag_helper.rb +189 -58
  33. data/lib/action_view/helpers/javascript_helper.rb +4 -5
  34. data/lib/action_view/helpers/number_helper.rb +43 -335
  35. data/lib/action_view/helpers/output_safety_helper.rb +6 -6
  36. data/lib/action_view/helpers/rendering_helper.rb +6 -7
  37. data/lib/action_view/helpers/sanitize_helper.rb +54 -24
  38. data/lib/action_view/helpers/tag_helper.rb +42 -35
  39. data/lib/action_view/helpers/tags/base.rb +16 -77
  40. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  41. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  42. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  43. data/lib/action_view/helpers/tags/collection_select.rb +4 -1
  44. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  45. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  48. data/lib/action_view/helpers/tags/file_field.rb +16 -0
  49. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  50. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  51. data/lib/action_view/helpers/tags/select.rb +4 -1
  52. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  53. data/lib/action_view/helpers/tags/time_field.rb +11 -2
  54. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  55. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  56. data/lib/action_view/helpers/tags/weekday_select.rb +31 -0
  57. data/lib/action_view/helpers/tags.rb +5 -2
  58. data/lib/action_view/helpers/text_helper.rb +180 -97
  59. data/lib/action_view/helpers/translation_helper.rb +14 -45
  60. data/lib/action_view/helpers/url_helper.rb +230 -132
  61. data/lib/action_view/helpers.rb +27 -25
  62. data/lib/action_view/layouts.rb +15 -10
  63. data/lib/action_view/log_subscriber.rb +49 -32
  64. data/lib/action_view/lookup_context.rb +58 -61
  65. data/lib/action_view/model_naming.rb +2 -2
  66. data/lib/action_view/path_registry.rb +57 -0
  67. data/lib/action_view/path_set.rb +28 -35
  68. data/lib/action_view/railtie.rb +44 -9
  69. data/lib/action_view/record_identifier.rb +16 -9
  70. data/lib/action_view/render_parser.rb +188 -0
  71. data/lib/action_view/renderer/abstract_renderer.rb +3 -3
  72. data/lib/action_view/renderer/collection_renderer.rb +10 -2
  73. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +21 -3
  74. data/lib/action_view/renderer/partial_renderer.rb +3 -36
  75. data/lib/action_view/renderer/renderer.rb +6 -4
  76. data/lib/action_view/renderer/streaming_template_renderer.rb +6 -5
  77. data/lib/action_view/renderer/template_renderer.rb +9 -4
  78. data/lib/action_view/rendering.rb +25 -7
  79. data/lib/action_view/ripper_ast_parser.rb +198 -0
  80. data/lib/action_view/routing_url_for.rb +8 -5
  81. data/lib/action_view/template/error.rb +122 -14
  82. data/lib/action_view/template/handlers/builder.rb +4 -4
  83. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  84. data/lib/action_view/template/handlers/erb.rb +79 -1
  85. data/lib/action_view/template/handlers.rb +4 -4
  86. data/lib/action_view/template/html.rb +4 -4
  87. data/lib/action_view/template/inline.rb +3 -3
  88. data/lib/action_view/template/raw_file.rb +4 -4
  89. data/lib/action_view/template/renderable.rb +1 -1
  90. data/lib/action_view/template/resolver.rb +96 -313
  91. data/lib/action_view/template/text.rb +4 -4
  92. data/lib/action_view/template/types.rb +25 -32
  93. data/lib/action_view/template.rb +245 -41
  94. data/lib/action_view/template_details.rb +66 -0
  95. data/lib/action_view/template_path.rb +66 -0
  96. data/lib/action_view/test_case.rb +182 -23
  97. data/lib/action_view/testing/resolvers.rb +11 -12
  98. data/lib/action_view/unbound_template.rb +43 -7
  99. data/lib/action_view/version.rb +1 -1
  100. data/lib/action_view/view_paths.rb +19 -28
  101. data/lib/action_view.rb +6 -4
  102. data/lib/assets/compiled/rails-ujs.js +36 -5
  103. metadata +32 -25
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "cgi"
4
4
  require "action_view/helpers/date_helper"
5
- require "action_view/helpers/tag_helper"
5
+ require "action_view/helpers/url_helper"
6
6
  require "action_view/helpers/form_tag_helper"
7
7
  require "action_view/helpers/active_model_helper"
8
8
  require "action_view/model_naming"
@@ -11,11 +11,11 @@ require "active_support/core_ext/module/attribute_accessors"
11
11
  require "active_support/core_ext/hash/slice"
12
12
  require "active_support/core_ext/string/output_safety"
13
13
  require "active_support/core_ext/string/inflections"
14
- require "active_support/core_ext/symbol/starts_ends_with"
15
14
 
16
15
  module ActionView
17
- # = Action View Form Helpers
18
- module Helpers #:nodoc:
16
+ module Helpers # :nodoc:
17
+ # = Action View Form \Helpers
18
+ #
19
19
  # Form helpers are designed to make working with resources much easier
20
20
  # compared to using vanilla HTML.
21
21
  #
@@ -29,7 +29,7 @@ module ActionView
29
29
  # when the form is initially displayed, input fields corresponding to attributes
30
30
  # of the resource should show the current values of those attributes.
31
31
  #
32
- # In Rails, this is usually achieved by creating the form using +form_for+ and
32
+ # In \Rails, this is usually achieved by creating the form using +form_for+ and
33
33
  # a number of related helper methods. +form_for+ generates an appropriate <tt>form</tt>
34
34
  # tag and yields a form builder object that knows the model the form is about.
35
35
  # Input fields are created by calling methods defined on the form builder, which
@@ -123,7 +123,7 @@ module ActionView
123
123
  # of a specific model object.
124
124
  #
125
125
  # The method can be used in several slightly different ways, depending on
126
- # how much you wish to rely on Rails to infer automatically from the model
126
+ # how much you wish to rely on \Rails to infer automatically from the model
127
127
  # how the form should be constructed. For a generic model object, a form
128
128
  # can be created by passing +form_for+ a string or symbol representing
129
129
  # the object we are concerned with:
@@ -252,7 +252,7 @@ module ActionView
252
252
  # form is going to be sent. However, further simplification is possible
253
253
  # if the record passed to +form_for+ is a _resource_, i.e. it corresponds
254
254
  # to a set of RESTful routes, e.g. defined using the +resources+ method
255
- # in <tt>config/routes.rb</tt>. In this case Rails will simply infer the
255
+ # in <tt>config/routes.rb</tt>. In this case \Rails will simply infer the
256
256
  # appropriate URL from the record itself. For example,
257
257
  #
258
258
  # <%= form_for @post do |f| %>
@@ -283,6 +283,12 @@ module ActionView
283
283
  # ...
284
284
  # <% end %>
285
285
  #
286
+ # You can omit the <tt>action</tt> attribute by passing <tt>url: false</tt>:
287
+ #
288
+ # <%= form_for(@post, url: false) do |f| %>
289
+ # ...
290
+ # <% end %>
291
+ #
286
292
  # You can also set the answer format, like this:
287
293
  #
288
294
  # <%= form_for(@post, format: :json) do |f| %>
@@ -427,50 +433,45 @@ module ActionView
427
433
  # <% end %>
428
434
  def form_for(record, options = {}, &block)
429
435
  raise ArgumentError, "Missing block" unless block_given?
430
- html_options = options[:html] ||= {}
431
436
 
432
437
  case record
433
438
  when String, Symbol
439
+ model = nil
434
440
  object_name = record
435
- object = nil
436
441
  else
437
- object = record.is_a?(Array) ? record.last : record
442
+ model = record
443
+ object = _object_for_form_builder(record)
438
444
  raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
439
445
  object_name = options[:as] || model_name_from_record_or_class(object).param_key
440
- apply_form_for_options!(record, object, options)
446
+ apply_form_for_options!(object, options)
441
447
  end
442
448
 
443
- html_options[:data] = options.delete(:data) if options.has_key?(:data)
444
- html_options[:remote] = options.delete(:remote) if options.has_key?(:remote)
445
- html_options[:method] = options.delete(:method) if options.has_key?(:method)
446
- html_options[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8)
447
- html_options[:authenticity_token] = options.delete(:authenticity_token)
449
+ remote = options.delete(:remote)
450
+
451
+ if remote && !embed_authenticity_token_in_remote_forms && options[:authenticity_token].blank?
452
+ options[:authenticity_token] = false
453
+ end
448
454
 
449
- builder = instantiate_builder(object_name, object, options)
450
- output = capture(builder, &block)
451
- html_options[:multipart] ||= builder.multipart?
455
+ options[:model] = model
456
+ options[:scope] = object_name
457
+ options[:local] = !remote
458
+ options[:skip_default_ids] = false
459
+ options[:allow_method_names_outside_object] = options.fetch(:allow_method_names_outside_object, false)
452
460
 
453
- html_options = html_options_for_form(options[:url] || {}, html_options)
454
- form_tag_with_body(html_options, output)
461
+ form_with(**options, &block)
455
462
  end
456
463
 
457
- def apply_form_for_options!(record, object, options) #:nodoc:
464
+ def apply_form_for_options!(object, options) # :nodoc:
458
465
  object = convert_to_model(object)
459
466
 
460
467
  as = options[:as]
461
468
  namespace = options[:namespace]
462
- action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
469
+ action = object.respond_to?(:persisted?) && object.persisted? ? :edit : :new
470
+ options[:html] ||= {}
463
471
  options[:html].reverse_merge!(
464
472
  class: as ? "#{action}_#{as}" : dom_class(object, action),
465
473
  id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
466
- method: method
467
474
  )
468
-
469
- options[:url] ||= if options.key?(:format)
470
- polymorphic_path(record, format: options.delete(:format))
471
- else
472
- polymorphic_path(record, {})
473
- end
474
475
  end
475
476
  private :apply_form_for_options!
476
477
 
@@ -478,6 +479,8 @@ module ActionView
478
479
 
479
480
  mattr_accessor :form_with_generates_ids, default: false
480
481
 
482
+ mattr_accessor :multiple_file_field_include_hidden, default: false
483
+
481
484
  # Creates a form tag based on mixing URLs, scopes, or models.
482
485
  #
483
486
  # # Using just a URL:
@@ -485,7 +488,16 @@ module ActionView
485
488
  # <%= form.text_field :title %>
486
489
  # <% end %>
487
490
  # # =>
488
- # <form action="/posts" method="post" data-remote="true">
491
+ # <form action="/posts" method="post">
492
+ # <input type="text" name="title">
493
+ # </form>
494
+ #
495
+ # # With an intentionally empty URL:
496
+ # <%= form_with url: false do |form| %>
497
+ # <%= form.text_field :title %>
498
+ # <% end %>
499
+ # # =>
500
+ # <form method="post">
489
501
  # <input type="text" name="title">
490
502
  # </form>
491
503
  #
@@ -494,7 +506,7 @@ module ActionView
494
506
  # <%= form.text_field :title %>
495
507
  # <% end %>
496
508
  # # =>
497
- # <form action="/posts" method="post" data-remote="true">
509
+ # <form action="/posts" method="post">
498
510
  # <input type="text" name="post[title]">
499
511
  # </form>
500
512
  #
@@ -503,7 +515,7 @@ module ActionView
503
515
  # <%= form.text_field :title %>
504
516
  # <% end %>
505
517
  # # =>
506
- # <form action="/posts" method="post" data-remote="true">
518
+ # <form action="/posts" method="post">
507
519
  # <input type="text" name="post[title]">
508
520
  # </form>
509
521
  #
@@ -512,7 +524,7 @@ module ActionView
512
524
  # <%= form.text_field :title %>
513
525
  # <% end %>
514
526
  # # =>
515
- # <form action="/posts/1" method="post" data-remote="true">
527
+ # <form action="/posts/1" method="post">
516
528
  # <input type="hidden" name="_method" value="patch">
517
529
  # <input type="text" name="post[title]" value="<the title of the post>">
518
530
  # </form>
@@ -523,7 +535,7 @@ module ActionView
523
535
  # <%= form.text_field :but_in_forms_they_can %>
524
536
  # <% end %>
525
537
  # # =>
526
- # <form action="/cats" method="post" data-remote="true">
538
+ # <form action="/cats" method="post">
527
539
  # <input type="text" name="cat[cats_dont_have_gills]">
528
540
  # <input type="text" name="cat[but_in_forms_they_can]">
529
541
  # </form>
@@ -544,7 +556,7 @@ module ActionView
544
556
  # is a _resource_. It corresponds to a set of RESTful routes, most likely
545
557
  # defined via +resources+ in <tt>config/routes.rb</tt>.
546
558
  #
547
- # So when passing such a model record, Rails infers the URL and method.
559
+ # So when passing such a model record, \Rails infers the URL and method.
548
560
  #
549
561
  # <%= form_with model: @post do |form| %>
550
562
  # ...
@@ -604,10 +616,16 @@ module ActionView
604
616
  # This is helpful when fragment-caching the form. Remote forms
605
617
  # get the authenticity token from the <tt>meta</tt> tag, so embedding is
606
618
  # unnecessary unless you support browsers without JavaScript.
607
- # * <tt>:local</tt> - By default form submits via typical HTTP requests.
608
- # Enable remote and unobtrusive XHRs submits with <tt>local: false</tt>.
609
- # Remote forms may be enabled by default by setting
610
- # <tt>config.action_view.form_with_generates_remote_forms = true</tt>.
619
+ # * <tt>:local</tt> - Whether to use standard HTTP form submission.
620
+ # When set to <tt>true</tt>, the form is submitted via standard HTTP.
621
+ # When set to <tt>false</tt>, the form is submitted as a "remote form", which
622
+ # is handled by \Rails UJS as an XHR. When unspecified, the behavior is derived
623
+ # from <tt>config.action_view.form_with_generates_remote_forms</tt> where the
624
+ # config's value is actually the inverse of what <tt>local</tt>'s value would be.
625
+ # As of \Rails 6.1, that configuration option defaults to <tt>false</tt>
626
+ # (which has the equivalent effect of passing <tt>local: true</tt>).
627
+ # In previous versions of \Rails, that configuration option defaults to
628
+ # <tt>true</tt> (the equivalent of passing <tt>local: false</tt>).
611
629
  # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name
612
630
  # utf8 is not output.
613
631
  # * <tt>:builder</tt> - Override the object used to build the form.
@@ -735,13 +753,18 @@ module ActionView
735
753
  # form_with(**options.merge(builder: LabellingFormBuilder), &block)
736
754
  # end
737
755
  def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
738
- options[:allow_method_names_outside_object] = true
739
- options[:skip_default_ids] = !form_with_generates_ids
756
+ options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
740
757
 
741
758
  if model
742
- url ||= polymorphic_path(model, format: format)
759
+ if url != false
760
+ url ||= if format.nil?
761
+ polymorphic_path(model, {})
762
+ else
763
+ polymorphic_path(model, format: format)
764
+ end
765
+ end
743
766
 
744
- model = model.last if model.is_a?(Array)
767
+ model = convert_to_model(_object_for_form_builder(model))
745
768
  scope ||= model_name_from_record_or_class(model).param_key
746
769
  end
747
770
 
@@ -983,8 +1006,8 @@ module ActionView
983
1006
  # <% end %>
984
1007
  #
985
1008
  # When a collection is used you might want to know the index of each
986
- # object into the array. For this purpose, the <tt>index</tt> method
987
- # is available in the FormBuilder object.
1009
+ # object in the array. For this purpose, the <tt>index</tt> method is
1010
+ # available in the FormBuilder object.
988
1011
  #
989
1012
  # <%= form_for @person do |person_form| %>
990
1013
  # ...
@@ -996,12 +1019,14 @@ module ActionView
996
1019
  # <% end %>
997
1020
  #
998
1021
  # Note that fields_for will automatically generate a hidden field
999
- # to store the ID of the record. There are circumstances where this
1000
- # hidden field is not needed and you can pass <tt>include_id: false</tt>
1001
- # to prevent fields_for from rendering it automatically.
1022
+ # to store the ID of the record if it responds to <tt>persisted?</tt>.
1023
+ # There are circumstances where this hidden field is not needed and you
1024
+ # can pass <tt>include_id: false</tt> to prevent fields_for from
1025
+ # rendering it automatically.
1002
1026
  def fields_for(record_name, record_object = nil, options = {}, &block)
1003
- builder = instantiate_builder(record_name, record_object, options)
1004
- capture(builder, &block)
1027
+ options = { model: record_object, allow_method_names_outside_object: false, skip_default_ids: false }.merge!(options)
1028
+
1029
+ fields(record_name, **options, &block)
1005
1030
  end
1006
1031
 
1007
1032
  # Scopes input fields with either an explicit scope or model.
@@ -1050,10 +1075,10 @@ module ActionView
1050
1075
  # to work with an object as a base, like
1051
1076
  # FormOptionsHelper#collection_select and DateHelper#datetime_select.
1052
1077
  def fields(scope = nil, model: nil, **options, &block)
1053
- options[:allow_method_names_outside_object] = true
1054
- options[:skip_default_ids] = !form_with_generates_ids
1078
+ options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
1055
1079
 
1056
1080
  if model
1081
+ model = _object_for_form_builder(model)
1057
1082
  scope ||= model_name_from_record_or_class(model).param_key
1058
1083
  end
1059
1084
 
@@ -1063,7 +1088,7 @@ module ActionView
1063
1088
 
1064
1089
  # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
1065
1090
  # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
1066
- # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
1091
+ # is found in the current I18n locale (through <tt>helpers.label.<modelname>.<attribute></tt>) or you specify it explicitly.
1067
1092
  # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
1068
1093
  # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
1069
1094
  # target labels for radio_button tags (where the value is used in the ID of the input tag).
@@ -1093,6 +1118,8 @@ module ActionView
1093
1118
  # post:
1094
1119
  # cost: "Total cost"
1095
1120
  #
1121
+ # <code></code>
1122
+ #
1096
1123
  # label(:post, :cost)
1097
1124
  # # => <label for="post_cost">Total cost</label>
1098
1125
  #
@@ -1197,6 +1224,7 @@ module ActionView
1197
1224
  # * Creates standard HTML attributes for the tag.
1198
1225
  # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
1199
1226
  # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
1227
+ # * <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.
1200
1228
  # * <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.
1201
1229
  #
1202
1230
  # ==== Examples
@@ -1215,6 +1243,8 @@ module ActionView
1215
1243
  # file_field(:attachment, :file, class: 'file_input')
1216
1244
  # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
1217
1245
  def file_field(object_name, method, options = {})
1246
+ options = { include_hidden: multiple_file_field_include_hidden }.merge!(options)
1247
+
1218
1248
  Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
1219
1249
  end
1220
1250
 
@@ -1252,6 +1282,12 @@ module ActionView
1252
1282
  # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
1253
1283
  # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
1254
1284
  #
1285
+ # ==== Options
1286
+ #
1287
+ # * Any standard HTML attributes for the tag can be passed in, for example +:class+.
1288
+ # * <tt>:checked</tt> - +true+ or +false+ forces the state of the checkbox to be checked or not.
1289
+ # * <tt>:include_hidden</tt> - If set to false, the auxiliary hidden field described below will not be generated.
1290
+ #
1255
1291
  # ==== Gotcha
1256
1292
  #
1257
1293
  # The HTML specification says unchecked check boxes are not successful, and
@@ -1265,7 +1301,7 @@ module ActionView
1265
1301
  # wouldn't update the flag.
1266
1302
  #
1267
1303
  # To prevent this the helper generates an auxiliary hidden field before
1268
- # the very check box. The hidden field has the same name and its
1304
+ # every check box. The hidden field has the same name and its
1269
1305
  # attributes mimic an unchecked check box.
1270
1306
  #
1271
1307
  # This way, the client either sends only the hidden field (representing
@@ -1282,13 +1318,15 @@ module ActionView
1282
1318
  # ...
1283
1319
  # <% end %>
1284
1320
  #
1285
- # because parameter name repetition is precisely what Rails seeks to distinguish
1321
+ # because parameter name repetition is precisely what \Rails seeks to distinguish
1286
1322
  # the elements of the array. For each item with a checked check box you
1287
1323
  # get an extra ghost item with only that attribute, assigned to "0".
1288
1324
  #
1289
1325
  # In that case it is preferable to either use +check_box_tag+ or to use
1290
1326
  # hashes instead of arrays.
1291
1327
  #
1328
+ # ==== Examples
1329
+ #
1292
1330
  # # Let's say that @post.validated? is 1:
1293
1331
  # check_box("post", "validated")
1294
1332
  # # => <input name="post[validated]" type="hidden" value="0" />
@@ -1403,13 +1441,16 @@ module ActionView
1403
1441
  # Returns a text_field of type "time".
1404
1442
  #
1405
1443
  # The default value is generated by trying to call +strftime+ with "%T.%L"
1406
- # on the object's value. It is still possible to override that
1407
- # by passing the "value" option.
1444
+ # on the object's value. If you pass <tt>include_seconds: false</tt>, it will be
1445
+ # formatted by trying to call +strftime+ with "%H:%M" on the object's value.
1446
+ # It is also possible to override this by passing the "value" option.
1447
+ #
1448
+ # ==== Options
1408
1449
  #
1409
- # === Options
1410
- # * Accepts same options as time_field_tag
1450
+ # Supports the same options as FormTagHelper#time_field_tag.
1451
+ #
1452
+ # ==== Examples
1411
1453
  #
1412
- # === Example
1413
1454
  # time_field("task", "started_at")
1414
1455
  # # => <input id="task_started_at" name="task[started_at]" type="time" />
1415
1456
  #
@@ -1425,6 +1466,12 @@ module ActionView
1425
1466
  # time_field("task", "started_at", min: "01:00:00")
1426
1467
  # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
1427
1468
  #
1469
+ # By default, provided times will be formatted including seconds. You can render just the hour
1470
+ # and minute by passing <tt>include_seconds: false</tt>. Some browsers will render a simpler UI
1471
+ # if you exclude seconds in the timestamp format.
1472
+ #
1473
+ # time_field("task", "started_at", value: Time.now, include_seconds: false)
1474
+ # # => <input id="task_started_at" name="task[started_at]" type="time" value="01:00" />
1428
1475
  def time_field(object_name, method, options = {})
1429
1476
  Tags::TimeField.new(object_name, method, self, options).render
1430
1477
  end
@@ -1454,6 +1501,12 @@ module ActionView
1454
1501
  # datetime_field("user", "born_on", min: "2014-05-20T00:00:00")
1455
1502
  # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
1456
1503
  #
1504
+ # By default, provided datetimes will be formatted including seconds. You can render just the date, hour,
1505
+ # and minute by passing <tt>include_seconds: false</tt>.
1506
+ #
1507
+ # @user.born_on = Time.current
1508
+ # datetime_field("user", "born_on", include_seconds: false)
1509
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="2014-05-20T14:35" />
1457
1510
  def datetime_field(object_name, method, options = {})
1458
1511
  Tags::DatetimeLocalField.new(object_name, method, self, options).render
1459
1512
  end
@@ -1515,7 +1568,8 @@ module ActionView
1515
1568
  # Returns an input tag of type "number".
1516
1569
  #
1517
1570
  # ==== Options
1518
- # * Accepts same options as number_field_tag
1571
+ #
1572
+ # Supports the same options as FormTagHelper#number_field_tag.
1519
1573
  def number_field(object_name, method, options = {})
1520
1574
  Tags::NumberField.new(object_name, method, self, options).render
1521
1575
  end
@@ -1523,39 +1577,30 @@ module ActionView
1523
1577
  # Returns an input tag of type "range".
1524
1578
  #
1525
1579
  # ==== Options
1526
- # * Accepts same options as range_field_tag
1580
+ #
1581
+ # Supports the same options as FormTagHelper#range_field_tag.
1527
1582
  def range_field(object_name, method, options = {})
1528
1583
  Tags::RangeField.new(object_name, method, self, options).render
1529
1584
  end
1530
1585
 
1586
+ def _object_for_form_builder(object) # :nodoc:
1587
+ object.is_a?(Array) ? object.last : object
1588
+ end
1589
+
1531
1590
  private
1532
1591
  def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
1533
1592
  skip_enforcing_utf8: nil, **options)
1534
- html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
1593
+ html_options = options.slice(:id, :class, :multipart, :method, :data, :authenticity_token).merge!(html)
1594
+ html_options[:remote] = html.delete(:remote) || !local
1535
1595
  html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
1536
- html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil?
1537
-
1538
- html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart)
1539
-
1540
- # The following URL is unescaped, this is just a hash of options, and it is the
1541
- # responsibility of the caller to escape all the values.
1542
- html_options[:action] = url_for(url_for_options || {})
1543
- html_options[:"accept-charset"] = "UTF-8"
1544
- html_options[:"data-remote"] = true unless local
1545
-
1546
- html_options[:authenticity_token] = options.delete(:authenticity_token)
1547
-
1548
- if !local && html_options[:authenticity_token].blank?
1549
- html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms
1550
- end
1551
-
1552
- if html_options[:authenticity_token] == true
1553
- # Include the default authenticity_token, which is only generated when it's set to nil,
1554
- # but we needed the true value to override the default of no authenticity_token on data-remote.
1555
- html_options[:authenticity_token] = nil
1596
+ if skip_enforcing_utf8.nil?
1597
+ if options.key?(:enforce_utf8)
1598
+ html_options[:enforce_utf8] = options[:enforce_utf8]
1599
+ end
1600
+ else
1601
+ html_options[:enforce_utf8] = !skip_enforcing_utf8
1556
1602
  end
1557
-
1558
- html_options.stringify_keys!
1603
+ html_options_for_form(url_for_options.nil? ? {} : url_for_options, html_options)
1559
1604
  end
1560
1605
 
1561
1606
  def instantiate_builder(record_name, record_object, options)
@@ -1578,6 +1623,8 @@ module ActionView
1578
1623
  end
1579
1624
  end
1580
1625
 
1626
+ # = Action View Form Builder
1627
+ #
1581
1628
  # A +FormBuilder+ object is associated with a particular model object and
1582
1629
  # allows you to generate fields associated with the model object. The
1583
1630
  # +FormBuilder+ object is yielded when using +form_for+ or +fields_for+.
@@ -1685,6 +1732,69 @@ module ActionView
1685
1732
  @index = options[:index] || options[:child_index]
1686
1733
  end
1687
1734
 
1735
+ # Generate an HTML <tt>id</tt> attribute value.
1736
+ #
1737
+ # return the <tt><form></tt> element's <tt>id</tt> attribute.
1738
+ #
1739
+ # <%= form_for @post do |f| %>
1740
+ # <%# ... %>
1741
+ #
1742
+ # <% content_for :sticky_footer do %>
1743
+ # <%= form.button(form: f.id) %>
1744
+ # <% end %>
1745
+ # <% end %>
1746
+ #
1747
+ # In the example above, the <tt>:sticky_footer</tt> content area will
1748
+ # exist outside of the <tt><form></tt> element. By declaring the
1749
+ # <tt>form</tt> HTML attribute, we hint to the browser that the generated
1750
+ # <tt><button></tt> element should be treated as the <tt><form></tt>
1751
+ # element's submit button, regardless of where it exists in the DOM.
1752
+ def id
1753
+ options.dig(:html, :id) || options[:id]
1754
+ end
1755
+
1756
+ # Generate an HTML <tt>id</tt> attribute value for the given field
1757
+ #
1758
+ # Return the value generated by the <tt>FormBuilder</tt> for the given
1759
+ # attribute name.
1760
+ #
1761
+ # <%= form_for @post do |f| %>
1762
+ # <%= f.label :title %>
1763
+ # <%= f.text_field :title, aria: { describedby: f.field_id(:title, :error) } %>
1764
+ # <%= tag.span("is blank", id: f.field_id(:title, :error) %>
1765
+ # <% end %>
1766
+ #
1767
+ # In the example above, the <tt><input type="text"></tt> element built by
1768
+ # the call to <tt>FormBuilder#text_field</tt> declares an
1769
+ # <tt>aria-describedby</tt> attribute referencing the <tt><span></tt>
1770
+ # element, sharing a common <tt>id</tt> root (<tt>post_title</tt>, in this
1771
+ # case).
1772
+ def field_id(method, *suffixes, namespace: @options[:namespace], index: @options[:index])
1773
+ @template.field_id(@object_name, method, *suffixes, namespace: namespace, index: index)
1774
+ end
1775
+
1776
+ # Generate an HTML <tt>name</tt> attribute value for the given name and
1777
+ # field combination
1778
+ #
1779
+ # Return the value generated by the <tt>FormBuilder</tt> for the given
1780
+ # attribute name.
1781
+ #
1782
+ # <%= form_for @post do |f| %>
1783
+ # <%= f.text_field :title, name: f.field_name(:title, :subtitle) %>
1784
+ # <%# => <input type="text" name="post[title][subtitle]"> %>
1785
+ # <% end %>
1786
+ #
1787
+ # <%= form_for @post do |f| %>
1788
+ # <%= f.text_field :tag, name: f.field_name(:tag, multiple: true) %>
1789
+ # <%# => <input type="text" name="post[tag][]"> %>
1790
+ # <% end %>
1791
+ #
1792
+ def field_name(method, *methods, multiple: false, index: @options[:index])
1793
+ object_name = @options.fetch(:as) { @object_name }
1794
+
1795
+ @template.field_name(object_name, method, *methods, index: index, multiple: multiple)
1796
+ end
1797
+
1688
1798
  ##
1689
1799
  # :method: text_field
1690
1800
  #
@@ -1979,6 +2089,18 @@ module ActionView
1979
2089
  # DateHelper that are designed to work with an object as base, like
1980
2090
  # FormOptionsHelper#collection_select and DateHelper#datetime_select.
1981
2091
  #
2092
+ # +fields_for+ tries to be smart about parameters, but it can be confused if both
2093
+ # name and value parameters are provided and the provided value has the shape of an
2094
+ # option Hash. To remove the ambiguity, explicitly pass an option Hash, even if empty.
2095
+ #
2096
+ # <%= form_for @person do |person_form| %>
2097
+ # ...
2098
+ # <%= fields_for :permission, @person.permission, {} do |permission_fields| %>
2099
+ # Admin?: <%= check_box_tag permission_fields.field_name(:admin), @person.permission[:admin] %>
2100
+ # <% end %>
2101
+ # ...
2102
+ # <% end %>
2103
+ #
1982
2104
  # === Nested Attributes Examples
1983
2105
  #
1984
2106
  # When the object belonging to the current scope has a nested attribute
@@ -2143,7 +2265,7 @@ module ActionView
2143
2265
  # <% end %>
2144
2266
  #
2145
2267
  # When a collection is used you might want to know the index of each
2146
- # object into the array. For this purpose, the <tt>index</tt> method
2268
+ # object in the array. For this purpose, the <tt>index</tt> method
2147
2269
  # is available in the FormBuilder object.
2148
2270
  #
2149
2271
  # <%= form_for @person do |person_form| %>
@@ -2159,8 +2281,9 @@ module ActionView
2159
2281
  # to store the ID of the record. There are circumstances where this
2160
2282
  # hidden field is not needed and you can pass <tt>include_id: false</tt>
2161
2283
  # to prevent fields_for from rendering it automatically.
2162
- def fields_for(record_name, record_object = nil, fields_options = {}, &block)
2163
- fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
2284
+ def fields_for(record_name, record_object = nil, fields_options = nil, &block)
2285
+ fields_options, record_object = record_object, nil if fields_options.nil? && record_object.is_a?(Hash) && record_object.extractable_options?
2286
+ fields_options ||= {}
2164
2287
  fields_options[:builder] ||= options[:builder]
2165
2288
  fields_options[:namespace] = options[:namespace]
2166
2289
  fields_options[:parent_builder] = self
@@ -2171,7 +2294,7 @@ module ActionView
2171
2294
  return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
2172
2295
  end
2173
2296
  else
2174
- record_object = record_name.is_a?(Array) ? record_name.last : record_name
2297
+ record_object = @template._object_for_form_builder(record_name)
2175
2298
  record_name = model_name_from_record_or_class(record_object).param_key
2176
2299
  end
2177
2300
 
@@ -2195,7 +2318,7 @@ module ActionView
2195
2318
  @template.fields_for(record_name, record_object, fields_options, &block)
2196
2319
  end
2197
2320
 
2198
- # See the docs for the <tt>ActionView::FormHelper.fields</tt> helper method.
2321
+ # See the docs for the ActionView::Helpers::FormHelper#fields helper method.
2199
2322
  def fields(scope = nil, model: nil, **options, &block)
2200
2323
  options[:allow_method_names_outside_object] = true
2201
2324
  options[:skip_default_ids] = !FormHelper.form_with_generates_ids
@@ -2207,7 +2330,7 @@ module ActionView
2207
2330
 
2208
2331
  # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
2209
2332
  # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
2210
- # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
2333
+ # is found in the current I18n locale (through <tt>helpers.label.<modelname>.<attribute></tt>) or you specify it explicitly.
2211
2334
  # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
2212
2335
  # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
2213
2336
  # target labels for radio_button tags (where the value is used in the ID of the input tag).
@@ -2237,6 +2360,8 @@ module ActionView
2237
2360
  # post:
2238
2361
  # cost: "Total cost"
2239
2362
  #
2363
+ # <code></code>
2364
+ #
2240
2365
  # label(:cost)
2241
2366
  # # => <label for="post_cost">Total cost</label>
2242
2367
  #
@@ -2281,6 +2406,12 @@ module ActionView
2281
2406
  # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
2282
2407
  # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
2283
2408
  #
2409
+ # ==== Options
2410
+ #
2411
+ # * Any standard HTML attributes for the tag can be passed in, for example +:class+.
2412
+ # * <tt>:checked</tt> - +true+ or +false+ forces the state of the checkbox to be checked or not.
2413
+ # * <tt>:include_hidden</tt> - If set to false, the auxiliary hidden field described below will not be generated.
2414
+ #
2284
2415
  # ==== Gotcha
2285
2416
  #
2286
2417
  # The HTML specification says unchecked check boxes are not successful, and
@@ -2294,7 +2425,7 @@ module ActionView
2294
2425
  # wouldn't update the flag.
2295
2426
  #
2296
2427
  # To prevent this the helper generates an auxiliary hidden field before
2297
- # the very check box. The hidden field has the same name and its
2428
+ # every check box. The hidden field has the same name and its
2298
2429
  # attributes mimic an unchecked check box.
2299
2430
  #
2300
2431
  # This way, the client either sends only the hidden field (representing
@@ -2311,13 +2442,15 @@ module ActionView
2311
2442
  # ...
2312
2443
  # <% end %>
2313
2444
  #
2314
- # because parameter name repetition is precisely what Rails seeks to distinguish
2445
+ # because parameter name repetition is precisely what \Rails seeks to distinguish
2315
2446
  # the elements of the array. For each item with a checked check box you
2316
2447
  # get an extra ghost item with only that attribute, assigned to "0".
2317
2448
  #
2318
2449
  # In that case it is preferable to either use +check_box_tag+ or to use
2319
2450
  # hashes instead of arrays.
2320
2451
  #
2452
+ # ==== Examples
2453
+ #
2321
2454
  # # Let's say that @post.validated? is 1:
2322
2455
  # check_box("validated")
2323
2456
  # # => <input name="post[validated]" type="hidden" value="0" />
@@ -2386,12 +2519,13 @@ module ActionView
2386
2519
  # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
2387
2520
  # shown.
2388
2521
  #
2389
- # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
2522
+ # Using this method inside a +form_with+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
2390
2523
  #
2391
2524
  # ==== Options
2392
2525
  # * Creates standard HTML attributes for the tag.
2393
2526
  # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
2394
2527
  # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
2528
+ # * <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. Since <tt>include_hidden</tt> will default to <tt>config.active_storage.multiple_file_field_include_hidden</tt> if you don't specify <tt>include_hidden</tt>, you will need to pass <tt>include_hidden: false</tt> to prevent submitting an empty collection of files when passing <tt>multiple: true</tt>.
2395
2529
  # * <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.
2396
2530
  #
2397
2531
  # ==== Examples
@@ -2483,6 +2617,9 @@ module ActionView
2483
2617
  # button("Create post")
2484
2618
  # # => <button name='button' type='submit'>Create post</button>
2485
2619
  #
2620
+ # button(:draft, value: true)
2621
+ # # => <button id="post_draft" name="post[draft]" value="true" type="submit">Create post</button>
2622
+ #
2486
2623
  # button do
2487
2624
  # content_tag(:strong, 'Ask me!')
2488
2625
  # end
@@ -2497,14 +2634,31 @@ module ActionView
2497
2634
  # # <strong>Create post</strong>
2498
2635
  # # </button>
2499
2636
  #
2637
+ # button(:draft, value: true) do
2638
+ # content_tag(:strong, "Save as draft")
2639
+ # end
2640
+ # # => <button id="post_draft" name="post[draft]" value="true" type="submit">
2641
+ # # <strong>Save as draft</strong>
2642
+ # # </button>
2643
+ #
2500
2644
  def button(value = nil, options = {}, &block)
2501
- value, options = nil, value if value.is_a?(Hash)
2645
+ case value
2646
+ when Hash
2647
+ value, options = nil, value
2648
+ when Symbol
2649
+ value, options = nil, { name: field_name(value), id: field_id(value) }.merge!(options.to_h)
2650
+ end
2502
2651
  value ||= submit_default_value
2503
2652
 
2504
2653
  if block_given?
2505
2654
  value = @template.capture { yield(value) }
2506
2655
  end
2507
2656
 
2657
+ formmethod = options[:formmethod]
2658
+ if formmethod.present? && !/post|get/i.match?(formmethod) && !options.key?(:name) && !options.key?(:value)
2659
+ options.merge! formmethod: :post, name: "_method", value: formmethod
2660
+ end
2661
+
2508
2662
  @template.button_tag(value, options)
2509
2663
  end
2510
2664
 
@@ -2565,7 +2719,9 @@ module ActionView
2565
2719
  else
2566
2720
  options[:child_index] = nested_child_index(name)
2567
2721
  end
2568
- output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
2722
+ if content = fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
2723
+ output << content
2724
+ end
2569
2725
  end
2570
2726
  output
2571
2727
  elsif association