actionview 6.1.7.2 → 7.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +265 -261
  3. data/MIT-LICENSE +1 -0
  4. data/lib/action_view/base.rb +4 -7
  5. data/lib/action_view/buffers.rb +2 -2
  6. data/lib/action_view/cache_expiry.rb +46 -32
  7. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  8. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  9. data/lib/action_view/dependency_tracker.rb +6 -147
  10. data/lib/action_view/digestor.rb +7 -4
  11. data/lib/action_view/flows.rb +4 -4
  12. data/lib/action_view/gem_version.rb +5 -5
  13. data/lib/action_view/helpers/active_model_helper.rb +2 -2
  14. data/lib/action_view/helpers/asset_tag_helper.rb +95 -39
  15. data/lib/action_view/helpers/asset_url_helper.rb +16 -16
  16. data/lib/action_view/helpers/atom_feed_helper.rb +3 -4
  17. data/lib/action_view/helpers/cache_helper.rb +52 -3
  18. data/lib/action_view/helpers/capture_helper.rb +4 -4
  19. data/lib/action_view/helpers/controller_helper.rb +2 -2
  20. data/lib/action_view/helpers/csp_helper.rb +1 -1
  21. data/lib/action_view/helpers/csrf_helper.rb +2 -2
  22. data/lib/action_view/helpers/date_helper.rb +111 -43
  23. data/lib/action_view/helpers/debug_helper.rb +3 -1
  24. data/lib/action_view/helpers/form_helper.rb +211 -85
  25. data/lib/action_view/helpers/form_options_helper.rb +70 -33
  26. data/lib/action_view/helpers/form_tag_helper.rb +150 -53
  27. data/lib/action_view/helpers/javascript_helper.rb +3 -5
  28. data/lib/action_view/helpers/number_helper.rb +17 -16
  29. data/lib/action_view/helpers/output_safety_helper.rb +4 -4
  30. data/lib/action_view/helpers/rendering_helper.rb +5 -6
  31. data/lib/action_view/helpers/sanitize_helper.rb +3 -3
  32. data/lib/action_view/helpers/tag_helper.rb +37 -8
  33. data/lib/action_view/helpers/tags/base.rb +5 -25
  34. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  35. data/lib/action_view/helpers/tags/collection_select.rb +1 -1
  36. data/lib/action_view/helpers/tags/file_field.rb +16 -0
  37. data/lib/action_view/helpers/tags/select.rb +1 -1
  38. data/lib/action_view/helpers/tags/time_field.rb +10 -1
  39. data/lib/action_view/helpers/tags/weekday_select.rb +28 -0
  40. data/lib/action_view/helpers/tags.rb +3 -2
  41. data/lib/action_view/helpers/text_helper.rb +25 -14
  42. data/lib/action_view/helpers/translation_helper.rb +12 -43
  43. data/lib/action_view/helpers/url_helper.rb +194 -123
  44. data/lib/action_view/helpers.rb +25 -25
  45. data/lib/action_view/layouts.rb +7 -4
  46. data/lib/action_view/lookup_context.rb +33 -52
  47. data/lib/action_view/model_naming.rb +2 -2
  48. data/lib/action_view/path_set.rb +16 -22
  49. data/lib/action_view/railtie.rb +19 -7
  50. data/lib/action_view/record_identifier.rb +1 -1
  51. data/lib/action_view/render_parser.rb +188 -0
  52. data/lib/action_view/renderer/abstract_renderer.rb +2 -2
  53. data/lib/action_view/renderer/partial_renderer.rb +1 -35
  54. data/lib/action_view/renderer/renderer.rb +4 -4
  55. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -3
  56. data/lib/action_view/renderer/template_renderer.rb +6 -2
  57. data/lib/action_view/rendering.rb +3 -3
  58. data/lib/action_view/ripper_ast_parser.rb +198 -0
  59. data/lib/action_view/routing_url_for.rb +8 -5
  60. data/lib/action_view/template/error.rb +108 -13
  61. data/lib/action_view/template/handlers/erb.rb +6 -0
  62. data/lib/action_view/template/handlers.rb +3 -3
  63. data/lib/action_view/template/html.rb +3 -3
  64. data/lib/action_view/template/inline.rb +3 -3
  65. data/lib/action_view/template/raw_file.rb +3 -3
  66. data/lib/action_view/template/resolver.rb +89 -314
  67. data/lib/action_view/template/text.rb +3 -3
  68. data/lib/action_view/template/types.rb +14 -12
  69. data/lib/action_view/template.rb +18 -2
  70. data/lib/action_view/template_details.rb +66 -0
  71. data/lib/action_view/template_path.rb +64 -0
  72. data/lib/action_view/test_case.rb +7 -3
  73. data/lib/action_view/testing/resolvers.rb +11 -12
  74. data/lib/action_view/unbound_template.rb +33 -7
  75. data/lib/action_view/version.rb +1 -1
  76. data/lib/action_view/view_paths.rb +4 -4
  77. data/lib/action_view.rb +2 -3
  78. data/lib/assets/compiled/rails-ujs.js +36 -5
  79. metadata +23 -16
@@ -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,10 @@ 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
16
  # = Action View Form Helpers
18
- module Helpers #:nodoc:
17
+ module Helpers # :nodoc:
19
18
  # Form helpers are designed to make working with resources much easier
20
19
  # compared to using vanilla HTML.
21
20
  #
@@ -283,6 +282,12 @@ module ActionView
283
282
  # ...
284
283
  # <% end %>
285
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
+ #
286
291
  # You can also set the answer format, like this:
287
292
  #
288
293
  # <%= form_for(@post, format: :json) do |f| %>
@@ -427,50 +432,45 @@ module ActionView
427
432
  # <% end %>
428
433
  def form_for(record, options = {}, &block)
429
434
  raise ArgumentError, "Missing block" unless block_given?
430
- html_options = options[:html] ||= {}
431
435
 
432
436
  case record
433
437
  when String, Symbol
438
+ model = nil
434
439
  object_name = record
435
- object = nil
436
440
  else
437
- object = record.is_a?(Array) ? record.last : record
441
+ model = convert_to_model(record)
442
+ object = _object_for_form_builder(record)
438
443
  raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
439
444
  object_name = options[:as] || model_name_from_record_or_class(object).param_key
440
- apply_form_for_options!(record, object, options)
445
+ apply_form_for_options!(object, options)
441
446
  end
442
447
 
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)
448
+ remote = options.delete(:remote)
449
+
450
+ if remote && !embed_authenticity_token_in_remote_forms && options[:authenticity_token].blank?
451
+ options[:authenticity_token] = false
452
+ end
448
453
 
449
- builder = instantiate_builder(object_name, object, options)
450
- output = capture(builder, &block)
451
- html_options[:multipart] ||= builder.multipart?
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)
452
459
 
453
- html_options = html_options_for_form(options[:url] || {}, html_options)
454
- form_tag_with_body(html_options, output)
460
+ form_with(**options, &block)
455
461
  end
456
462
 
457
- def apply_form_for_options!(record, object, options) #:nodoc:
463
+ def apply_form_for_options!(object, options) # :nodoc:
458
464
  object = convert_to_model(object)
459
465
 
460
466
  as = options[:as]
461
467
  namespace = options[:namespace]
462
- 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] ||= {}
463
470
  options[:html].reverse_merge!(
464
471
  class: as ? "#{action}_#{as}" : dom_class(object, action),
465
472
  id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
466
- method: method
467
473
  )
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
474
  end
475
475
  private :apply_form_for_options!
476
476
 
@@ -478,6 +478,8 @@ module ActionView
478
478
 
479
479
  mattr_accessor :form_with_generates_ids, default: false
480
480
 
481
+ mattr_accessor :multiple_file_field_include_hidden, default: false
482
+
481
483
  # Creates a form tag based on mixing URLs, scopes, or models.
482
484
  #
483
485
  # # Using just a URL:
@@ -485,7 +487,16 @@ module ActionView
485
487
  # <%= form.text_field :title %>
486
488
  # <% end %>
487
489
  # # =>
488
- # <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">
489
500
  # <input type="text" name="title">
490
501
  # </form>
491
502
  #
@@ -494,7 +505,7 @@ module ActionView
494
505
  # <%= form.text_field :title %>
495
506
  # <% end %>
496
507
  # # =>
497
- # <form action="/posts" method="post" data-remote="true">
508
+ # <form action="/posts" method="post">
498
509
  # <input type="text" name="post[title]">
499
510
  # </form>
500
511
  #
@@ -503,7 +514,7 @@ module ActionView
503
514
  # <%= form.text_field :title %>
504
515
  # <% end %>
505
516
  # # =>
506
- # <form action="/posts" method="post" data-remote="true">
517
+ # <form action="/posts" method="post">
507
518
  # <input type="text" name="post[title]">
508
519
  # </form>
509
520
  #
@@ -512,7 +523,7 @@ module ActionView
512
523
  # <%= form.text_field :title %>
513
524
  # <% end %>
514
525
  # # =>
515
- # <form action="/posts/1" method="post" data-remote="true">
526
+ # <form action="/posts/1" method="post">
516
527
  # <input type="hidden" name="_method" value="patch">
517
528
  # <input type="text" name="post[title]" value="<the title of the post>">
518
529
  # </form>
@@ -523,7 +534,7 @@ module ActionView
523
534
  # <%= form.text_field :but_in_forms_they_can %>
524
535
  # <% end %>
525
536
  # # =>
526
- # <form action="/cats" method="post" data-remote="true">
537
+ # <form action="/cats" method="post">
527
538
  # <input type="text" name="cat[cats_dont_have_gills]">
528
539
  # <input type="text" name="cat[but_in_forms_they_can]">
529
540
  # </form>
@@ -604,10 +615,16 @@ module ActionView
604
615
  # This is helpful when fragment-caching the form. Remote forms
605
616
  # get the authenticity token from the <tt>meta</tt> tag, so embedding is
606
617
  # 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>.
618
+ # * <tt>:local</tt> - Whether to use standard HTTP form submission.
619
+ # When set to <tt>true</tt>, the form is submitted via standard HTTP.
620
+ # When set to <tt>false</tt>, the form is submitted as a "remote form", which
621
+ # is handled by Rails UJS as an XHR. When unspecified, the behavior is derived
622
+ # from <tt>config.action_view.form_with_generates_remote_forms</tt> where the
623
+ # config's value is actually the inverse of what <tt>local</tt>'s value would be.
624
+ # As of Rails 6.1, that configuration option defaults to <tt>false</tt>
625
+ # (which has the equivalent effect of passing <tt>local: true</tt>).
626
+ # In previous versions of Rails, that configuration option defaults to
627
+ # <tt>true</tt> (the equivalent of passing <tt>local: false</tt>).
611
628
  # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name
612
629
  # utf8 is not output.
613
630
  # * <tt>:builder</tt> - Override the object used to build the form.
@@ -735,13 +752,14 @@ module ActionView
735
752
  # form_with(**options.merge(builder: LabellingFormBuilder), &block)
736
753
  # end
737
754
  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
755
+ options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
740
756
 
741
757
  if model
742
- url ||= polymorphic_path(model, format: format)
758
+ if url != false
759
+ url ||= polymorphic_path(model, format: format)
760
+ end
743
761
 
744
- model = model.last if model.is_a?(Array)
762
+ model = _object_for_form_builder(model)
745
763
  scope ||= model_name_from_record_or_class(model).param_key
746
764
  end
747
765
 
@@ -996,12 +1014,14 @@ module ActionView
996
1014
  # <% end %>
997
1015
  #
998
1016
  # 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.
1017
+ # to store the ID of the record if it responds to <tt>persisted?</tt>.
1018
+ # There are circumstances where this hidden field is not needed and you
1019
+ # can pass <tt>include_id: false</tt> to prevent fields_for from
1020
+ # rendering it automatically.
1002
1021
  def fields_for(record_name, record_object = nil, options = {}, &block)
1003
- builder = instantiate_builder(record_name, record_object, options)
1004
- capture(builder, &block)
1022
+ options = { model: record_object, allow_method_names_outside_object: false, skip_default_ids: false }.merge!(options)
1023
+
1024
+ fields(record_name, **options, &block)
1005
1025
  end
1006
1026
 
1007
1027
  # Scopes input fields with either an explicit scope or model.
@@ -1050,10 +1070,10 @@ module ActionView
1050
1070
  # to work with an object as a base, like
1051
1071
  # FormOptionsHelper#collection_select and DateHelper#datetime_select.
1052
1072
  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
1073
+ options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
1055
1074
 
1056
1075
  if model
1076
+ model = _object_for_form_builder(model)
1057
1077
  scope ||= model_name_from_record_or_class(model).param_key
1058
1078
  end
1059
1079
 
@@ -1063,7 +1083,7 @@ module ActionView
1063
1083
 
1064
1084
  # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
1065
1085
  # 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.
1086
+ # is found in the current I18n locale (through <tt>helpers.label.<modelname>.<attribute></tt>) or you specify it explicitly.
1067
1087
  # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
1068
1088
  # 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
1089
  # target labels for radio_button tags (where the value is used in the ID of the input tag).
@@ -1197,6 +1217,7 @@ module ActionView
1197
1217
  # * Creates standard HTML attributes for the tag.
1198
1218
  # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
1199
1219
  # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
1220
+ # * <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
1221
  # * <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
1222
  #
1202
1223
  # ==== Examples
@@ -1215,6 +1236,8 @@ module ActionView
1215
1236
  # file_field(:attachment, :file, class: 'file_input')
1216
1237
  # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
1217
1238
  def file_field(object_name, method, options = {})
1239
+ options = { include_hidden: multiple_file_field_include_hidden }.merge!(options)
1240
+
1218
1241
  Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
1219
1242
  end
1220
1243
 
@@ -1252,6 +1275,12 @@ module ActionView
1252
1275
  # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
1253
1276
  # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
1254
1277
  #
1278
+ # ==== Options
1279
+ #
1280
+ # * Any standard HTML attributes for the tag can be passed in, for example +:class+.
1281
+ # * <tt>:checked</tt> - +true+ or +false+ forces the state of the checkbox to be checked or not.
1282
+ # * <tt>:include_hidden</tt> - If set to false, the auxiliary hidden field described below will not be generated.
1283
+ #
1255
1284
  # ==== Gotcha
1256
1285
  #
1257
1286
  # The HTML specification says unchecked check boxes are not successful, and
@@ -1265,7 +1294,7 @@ module ActionView
1265
1294
  # wouldn't update the flag.
1266
1295
  #
1267
1296
  # 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
1297
+ # every check box. The hidden field has the same name and its
1269
1298
  # attributes mimic an unchecked check box.
1270
1299
  #
1271
1300
  # This way, the client either sends only the hidden field (representing
@@ -1289,6 +1318,8 @@ module ActionView
1289
1318
  # In that case it is preferable to either use +check_box_tag+ or to use
1290
1319
  # hashes instead of arrays.
1291
1320
  #
1321
+ # ==== Examples
1322
+ #
1292
1323
  # # Let's say that @post.validated? is 1:
1293
1324
  # check_box("post", "validated")
1294
1325
  # # => <input name="post[validated]" type="hidden" value="0" />
@@ -1403,13 +1434,16 @@ module ActionView
1403
1434
  # Returns a text_field of type "time".
1404
1435
  #
1405
1436
  # 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.
1437
+ # on the object's value. If you pass <tt>include_seconds: false</tt>, it will be
1438
+ # formatted by trying to call +strftime+ with "%H:%M" on the object's value.
1439
+ # It is also possible to override this by passing the "value" option.
1440
+ #
1441
+ # ==== Options
1442
+ #
1443
+ # Supports the same options as FormTagHelper#time_field_tag.
1408
1444
  #
1409
- # === Options
1410
- # * Accepts same options as time_field_tag
1445
+ # ==== Examples
1411
1446
  #
1412
- # === Example
1413
1447
  # time_field("task", "started_at")
1414
1448
  # # => <input id="task_started_at" name="task[started_at]" type="time" />
1415
1449
  #
@@ -1425,6 +1459,12 @@ module ActionView
1425
1459
  # time_field("task", "started_at", min: "01:00:00")
1426
1460
  # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
1427
1461
  #
1462
+ # By default, provided times will be formatted including seconds. You can render just the hour
1463
+ # and minute by passing <tt>include_seconds: false</tt>. Some browsers will render a simpler UI
1464
+ # if you exclude seconds in the timestamp format.
1465
+ #
1466
+ # time_field("task", "started_at", value: Time.now, include_seconds: false)
1467
+ # # => <input id="task_started_at" name="task[started_at]" type="time" value="01:00" />
1428
1468
  def time_field(object_name, method, options = {})
1429
1469
  Tags::TimeField.new(object_name, method, self, options).render
1430
1470
  end
@@ -1515,7 +1555,8 @@ module ActionView
1515
1555
  # Returns an input tag of type "number".
1516
1556
  #
1517
1557
  # ==== Options
1518
- # * Accepts same options as number_field_tag
1558
+ #
1559
+ # Supports the same options as FormTagHelper#number_field_tag.
1519
1560
  def number_field(object_name, method, options = {})
1520
1561
  Tags::NumberField.new(object_name, method, self, options).render
1521
1562
  end
@@ -1523,39 +1564,30 @@ module ActionView
1523
1564
  # Returns an input tag of type "range".
1524
1565
  #
1525
1566
  # ==== Options
1526
- # * Accepts same options as range_field_tag
1567
+ #
1568
+ # Supports the same options as FormTagHelper#range_field_tag.
1527
1569
  def range_field(object_name, method, options = {})
1528
1570
  Tags::RangeField.new(object_name, method, self, options).render
1529
1571
  end
1530
1572
 
1573
+ def _object_for_form_builder(object) # :nodoc:
1574
+ object.is_a?(Array) ? object.last : object
1575
+ end
1576
+
1531
1577
  private
1532
1578
  def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
1533
1579
  skip_enforcing_utf8: nil, **options)
1534
- html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
1580
+ html_options = options.slice(:id, :class, :multipart, :method, :data, :authenticity_token).merge!(html)
1581
+ html_options[:remote] = html.delete(:remote) || !local
1535
1582
  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
1583
+ if skip_enforcing_utf8.nil?
1584
+ if options.key?(:enforce_utf8)
1585
+ html_options[:enforce_utf8] = options[:enforce_utf8]
1586
+ end
1587
+ else
1588
+ html_options[:enforce_utf8] = !skip_enforcing_utf8
1556
1589
  end
1557
-
1558
- html_options.stringify_keys!
1590
+ html_options_for_form(url_for_options.nil? ? {} : url_for_options, html_options)
1559
1591
  end
1560
1592
 
1561
1593
  def instantiate_builder(record_name, record_object, options)
@@ -1685,6 +1717,69 @@ module ActionView
1685
1717
  @index = options[:index] || options[:child_index]
1686
1718
  end
1687
1719
 
1720
+ # Generate an HTML <tt>id</tt> attribute value.
1721
+ #
1722
+ # return the <tt><form></tt> element's <tt>id</tt> attribute.
1723
+ #
1724
+ # <%= form_for @post do |f| %>
1725
+ # <%# ... %>
1726
+ #
1727
+ # <% content_for :sticky_footer do %>
1728
+ # <%= form.button(form: f.id) %>
1729
+ # <% end %>
1730
+ # <% end %>
1731
+ #
1732
+ # In the example above, the <tt>:sticky_footer</tt> content area will
1733
+ # exist outside of the <tt><form></tt> element. By declaring the
1734
+ # <tt>form</tt> HTML attribute, we hint to the browser that the generated
1735
+ # <tt><button></tt> element should be treated as the <tt><form></tt>
1736
+ # element's submit button, regardless of where it exists in the DOM.
1737
+ def id
1738
+ options.dig(:html, :id) || options[:id]
1739
+ end
1740
+
1741
+ # Generate an HTML <tt>id</tt> attribute value for the given field
1742
+ #
1743
+ # Return the value generated by the <tt>FormBuilder</tt> for the given
1744
+ # attribute name.
1745
+ #
1746
+ # <%= form_for @post do |f| %>
1747
+ # <%= f.label :title %>
1748
+ # <%= f.text_field :title, aria: { describedby: f.field_id(:title, :error) } %>
1749
+ # <%= tag.span("is blank", id: f.field_id(:title, :error) %>
1750
+ # <% end %>
1751
+ #
1752
+ # In the example above, the <tt><input type="text"></tt> element built by
1753
+ # the call to <tt>FormBuilder#text_field</tt> declares an
1754
+ # <tt>aria-describedby</tt> attribute referencing the <tt><span></tt>
1755
+ # element, sharing a common <tt>id</tt> root (<tt>post_title</tt>, in this
1756
+ # case).
1757
+ def field_id(method, *suffixes, namespace: @options[:namespace], index: @index)
1758
+ @template.field_id(@object_name, method, *suffixes, namespace: namespace, index: index)
1759
+ end
1760
+
1761
+ # Generate an HTML <tt>name</tt> attribute value for the given name and
1762
+ # field combination
1763
+ #
1764
+ # Return the value generated by the <tt>FormBuilder</tt> for the given
1765
+ # attribute name.
1766
+ #
1767
+ # <%= form_for @post do |f| %>
1768
+ # <%= f.text_field :title, name: f.field_name(:title, :subtitle) %>
1769
+ # <%# => <input type="text" name="post[title][subtitle]">
1770
+ # <% end %>
1771
+ #
1772
+ # <%= form_for @post do |f| %>
1773
+ # <%= f.field_tag :tag, name: f.field_name(:tag, multiple: true) %>
1774
+ # <%# => <input type="text" name="post[tag][]">
1775
+ # <% end %>
1776
+ #
1777
+ def field_name(method, *methods, multiple: false, index: @index)
1778
+ object_name = @options.fetch(:as) { @object_name }
1779
+
1780
+ @template.field_name(object_name, method, *methods, index: index, multiple: multiple)
1781
+ end
1782
+
1688
1783
  ##
1689
1784
  # :method: text_field
1690
1785
  #
@@ -2171,7 +2266,7 @@ module ActionView
2171
2266
  return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
2172
2267
  end
2173
2268
  else
2174
- record_object = record_name.is_a?(Array) ? record_name.last : record_name
2269
+ record_object = @template._object_for_form_builder(record_name)
2175
2270
  record_name = model_name_from_record_or_class(record_object).param_key
2176
2271
  end
2177
2272
 
@@ -2195,7 +2290,7 @@ module ActionView
2195
2290
  @template.fields_for(record_name, record_object, fields_options, &block)
2196
2291
  end
2197
2292
 
2198
- # See the docs for the <tt>ActionView::FormHelper.fields</tt> helper method.
2293
+ # See the docs for the ActionView::Helpers::FormHelper#fields helper method.
2199
2294
  def fields(scope = nil, model: nil, **options, &block)
2200
2295
  options[:allow_method_names_outside_object] = true
2201
2296
  options[:skip_default_ids] = !FormHelper.form_with_generates_ids
@@ -2207,7 +2302,7 @@ module ActionView
2207
2302
 
2208
2303
  # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
2209
2304
  # 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.
2305
+ # is found in the current I18n locale (through <tt>helpers.label.<modelname>.<attribute></tt>) or you specify it explicitly.
2211
2306
  # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
2212
2307
  # 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
2308
  # target labels for radio_button tags (where the value is used in the ID of the input tag).
@@ -2281,6 +2376,12 @@ module ActionView
2281
2376
  # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
2282
2377
  # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
2283
2378
  #
2379
+ # ==== Options
2380
+ #
2381
+ # * Any standard HTML attributes for the tag can be passed in, for example +:class+.
2382
+ # * <tt>:checked</tt> - +true+ or +false+ forces the state of the checkbox to be checked or not.
2383
+ # * <tt>:include_hidden</tt> - If set to false, the auxiliary hidden field described below will not be generated.
2384
+ #
2284
2385
  # ==== Gotcha
2285
2386
  #
2286
2387
  # The HTML specification says unchecked check boxes are not successful, and
@@ -2294,7 +2395,7 @@ module ActionView
2294
2395
  # wouldn't update the flag.
2295
2396
  #
2296
2397
  # 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
2398
+ # every check box. The hidden field has the same name and its
2298
2399
  # attributes mimic an unchecked check box.
2299
2400
  #
2300
2401
  # This way, the client either sends only the hidden field (representing
@@ -2318,6 +2419,8 @@ module ActionView
2318
2419
  # In that case it is preferable to either use +check_box_tag+ or to use
2319
2420
  # hashes instead of arrays.
2320
2421
  #
2422
+ # ==== Examples
2423
+ #
2321
2424
  # # Let's say that @post.validated? is 1:
2322
2425
  # check_box("validated")
2323
2426
  # # => <input name="post[validated]" type="hidden" value="0" />
@@ -2386,12 +2489,13 @@ module ActionView
2386
2489
  # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
2387
2490
  # shown.
2388
2491
  #
2389
- # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
2492
+ # Using this method inside a +form_with+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
2390
2493
  #
2391
2494
  # ==== Options
2392
2495
  # * Creates standard HTML attributes for the tag.
2393
2496
  # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
2394
2497
  # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
2498
+ # * <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.
2395
2499
  # * <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
2500
  #
2397
2501
  # ==== Examples
@@ -2483,6 +2587,9 @@ module ActionView
2483
2587
  # button("Create post")
2484
2588
  # # => <button name='button' type='submit'>Create post</button>
2485
2589
  #
2590
+ # button(:draft, value: true)
2591
+ # # => <button id="post_draft" name="post[draft]" value="true" type="submit">Create post</button>
2592
+ #
2486
2593
  # button do
2487
2594
  # content_tag(:strong, 'Ask me!')
2488
2595
  # end
@@ -2497,14 +2604,31 @@ module ActionView
2497
2604
  # # <strong>Create post</strong>
2498
2605
  # # </button>
2499
2606
  #
2607
+ # button(:draft, value: true) do
2608
+ # content_tag(:strong, "Save as draft")
2609
+ # end
2610
+ # # => <button id="post_draft" name="post[draft]" value="true" type="submit">
2611
+ # # <strong>Save as draft</strong>
2612
+ # # </button>
2613
+ #
2500
2614
  def button(value = nil, options = {}, &block)
2501
- value, options = nil, value if value.is_a?(Hash)
2615
+ case value
2616
+ when Hash
2617
+ value, options = nil, value
2618
+ when Symbol
2619
+ value, options = nil, { name: field_name(value), id: field_id(value) }.merge!(options.to_h)
2620
+ end
2502
2621
  value ||= submit_default_value
2503
2622
 
2504
2623
  if block_given?
2505
2624
  value = @template.capture { yield(value) }
2506
2625
  end
2507
2626
 
2627
+ formmethod = options[:formmethod]
2628
+ if formmethod.present? && !/post|get/i.match?(formmethod) && !options.key?(:name) && !options.key?(:value)
2629
+ options.merge! formmethod: :post, name: "_method", value: formmethod
2630
+ end
2631
+
2508
2632
  @template.button_tag(value, options)
2509
2633
  end
2510
2634
 
@@ -2565,7 +2689,9 @@ module ActionView
2565
2689
  else
2566
2690
  options[:child_index] = nested_child_index(name)
2567
2691
  end
2568
- output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
2692
+ if content = fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
2693
+ output << content
2694
+ end
2569
2695
  end
2570
2696
  output
2571
2697
  elsif association