actionview 4.1.13 → 6.1.3.1

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.

Files changed (124) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +181 -359
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +12 -6
  5. data/lib/action_view/base.rb +115 -43
  6. data/lib/action_view/buffers.rb +22 -4
  7. data/lib/action_view/cache_expiry.rb +52 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker.rb +61 -21
  10. data/lib/action_view/digestor.rb +89 -84
  11. data/lib/action_view/flows.rb +12 -13
  12. data/lib/action_view/gem_version.rb +6 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +16 -11
  14. data/lib/action_view/helpers/asset_tag_helper.rb +311 -105
  15. data/lib/action_view/helpers/asset_url_helper.rb +197 -80
  16. data/lib/action_view/helpers/atom_feed_helper.rb +20 -17
  17. data/lib/action_view/helpers/cache_helper.rb +109 -45
  18. data/lib/action_view/helpers/capture_helper.rb +20 -22
  19. data/lib/action_view/helpers/controller_helper.rb +15 -4
  20. data/lib/action_view/helpers/csp_helper.rb +26 -0
  21. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  22. data/lib/action_view/helpers/date_helper.rb +245 -140
  23. data/lib/action_view/helpers/debug_helper.rb +14 -17
  24. data/lib/action_view/helpers/form_helper.rb +875 -148
  25. data/lib/action_view/helpers/form_options_helper.rb +128 -82
  26. data/lib/action_view/helpers/form_tag_helper.rb +253 -91
  27. data/lib/action_view/helpers/javascript_helper.rb +37 -15
  28. data/lib/action_view/helpers/number_helper.rb +100 -77
  29. data/lib/action_view/helpers/output_safety_helper.rb +42 -10
  30. data/lib/action_view/helpers/rendering_helper.rb +26 -15
  31. data/lib/action_view/helpers/sanitize_helper.rb +79 -164
  32. data/lib/action_view/helpers/tag_helper.rb +277 -64
  33. data/lib/action_view/helpers/tags/base.rb +143 -92
  34. data/lib/action_view/helpers/tags/check_box.rb +20 -19
  35. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -30
  37. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  38. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  39. data/lib/action_view/helpers/tags/collection_select.rb +4 -2
  40. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  41. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  42. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  43. data/lib/action_view/helpers/tags/datetime_field.rb +14 -5
  44. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  47. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  48. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  49. data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
  50. data/lib/action_view/helpers/tags/label.rb +41 -22
  51. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  52. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  54. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  55. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  56. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/search_field.rb +3 -0
  58. data/lib/action_view/helpers/tags/select.rb +11 -10
  59. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/text_area.rb +7 -1
  61. data/lib/action_view/helpers/tags/text_field.rb +11 -7
  62. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  63. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  64. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  65. data/lib/action_view/helpers/tags/translator.rb +39 -0
  66. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  67. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  68. data/lib/action_view/helpers/tags.rb +4 -1
  69. data/lib/action_view/helpers/text_helper.rb +80 -45
  70. data/lib/action_view/helpers/translation_helper.rb +148 -67
  71. data/lib/action_view/helpers/url_helper.rb +289 -147
  72. data/lib/action_view/helpers.rb +5 -3
  73. data/lib/action_view/layouts.rb +68 -63
  74. data/lib/action_view/log_subscriber.rb +80 -13
  75. data/lib/action_view/lookup_context.rb +137 -92
  76. data/lib/action_view/model_naming.rb +4 -2
  77. data/lib/action_view/path_set.rb +30 -16
  78. data/lib/action_view/railtie.rb +62 -13
  79. data/lib/action_view/record_identifier.rb +53 -26
  80. data/lib/action_view/renderer/abstract_renderer.rb +152 -13
  81. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  82. data/lib/action_view/renderer/object_renderer.rb +34 -0
  83. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +61 -261
  85. data/lib/action_view/renderer/renderer.rb +67 -6
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +58 -54
  87. data/lib/action_view/renderer/template_renderer.rb +83 -75
  88. data/lib/action_view/rendering.rb +73 -46
  89. data/lib/action_view/routing_url_for.rb +54 -17
  90. data/lib/action_view/tasks/cache_digests.rake +25 -0
  91. data/lib/action_view/template/error.rb +44 -29
  92. data/lib/action_view/template/handlers/builder.rb +12 -13
  93. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  94. data/lib/action_view/template/handlers/erb.rb +23 -89
  95. data/lib/action_view/template/handlers/html.rb +11 -0
  96. data/lib/action_view/template/handlers/raw.rb +4 -4
  97. data/lib/action_view/template/handlers.rb +22 -9
  98. data/lib/action_view/template/html.rb +10 -11
  99. data/lib/action_view/template/inline.rb +22 -0
  100. data/lib/action_view/template/raw_file.rb +25 -0
  101. data/lib/action_view/template/renderable.rb +24 -0
  102. data/lib/action_view/template/resolver.rb +267 -181
  103. data/lib/action_view/template/sources/file.rb +17 -0
  104. data/lib/action_view/template/sources.rb +13 -0
  105. data/lib/action_view/template/text.rb +8 -10
  106. data/lib/action_view/template/types.rb +18 -18
  107. data/lib/action_view/template.rb +109 -99
  108. data/lib/action_view/test_case.rb +73 -53
  109. data/lib/action_view/testing/resolvers.rb +24 -33
  110. data/lib/action_view/unbound_template.rb +31 -0
  111. data/lib/action_view/version.rb +3 -1
  112. data/lib/action_view/view_paths.rb +74 -44
  113. data/lib/action_view.rb +14 -9
  114. data/lib/assets/compiled/rails-ujs.js +746 -0
  115. metadata +71 -26
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  117. data/lib/action_view/tasks/dependencies.rake +0 -23
  118. data/lib/action_view/vendor/html-scanner/html/document.rb +0 -68
  119. data/lib/action_view/vendor/html-scanner/html/node.rb +0 -532
  120. data/lib/action_view/vendor/html-scanner/html/sanitizer.rb +0 -188
  121. data/lib/action_view/vendor/html-scanner/html/selector.rb +0 -830
  122. data/lib/action_view/vendor/html-scanner/html/tokenizer.rb +0 -107
  123. data/lib/action_view/vendor/html-scanner/html/version.rb +0 -11
  124. data/lib/action_view/vendor/html-scanner.rb +0 -20
@@ -1,98 +1,140 @@
1
- require 'action_view/helpers/tag_helper'
2
- require 'i18n/exceptions'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/helpers/tag_helper"
4
+ require "active_support/core_ext/symbol/starts_ends_with"
3
5
 
4
6
  module ActionView
5
7
  # = Action View Translation Helpers
6
- module Helpers
8
+ module Helpers #:nodoc:
7
9
  module TranslationHelper
10
+ extend ActiveSupport::Concern
11
+
8
12
  include TagHelper
9
- # Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
13
+
14
+ included do
15
+ mattr_accessor :debug_missing_translation, default: true
16
+ end
17
+
18
+ # Delegates to <tt>I18n#translate</tt> but also performs three additional
19
+ # functions.
10
20
  #
11
- # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
12
- # into inline spans that:
21
+ # First, it will ensure that any thrown +MissingTranslation+ messages will
22
+ # be rendered as inline spans that:
13
23
  #
14
- # * have a "translation-missing" class set,
15
- # * contain the missing key as a title attribute and
16
- # * a titleized version of the last key segment as a text.
24
+ # * Have a <tt>translation-missing</tt> class applied
25
+ # * Contain the missing key as the value of the +title+ attribute
26
+ # * Have a titleized version of the last key segment as text
17
27
  #
18
- # E.g. the value returned for a missing translation key :"blog.post.title" will be
19
- # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>.
20
- # This way your views will display rather reasonable strings but it will still
21
- # be easy to spot missing translations.
28
+ # For example, the value returned for the missing translation key
29
+ # <tt>"blog.post.title"</tt> will be:
22
30
  #
23
- # Second, it'll scope the key by the current partial if the key starts
24
- # with a period. So if you call <tt>translate(".foo")</tt> from the
25
- # <tt>people/index.html.erb</tt> template, you'll actually be calling
26
- # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
27
- # to translate many keys within the same partials and gives you a simple framework
28
- # for scoping them consistently. If you don't prepend the key with a period,
29
- # nothing is converted.
31
+ # <span
32
+ # class="translation_missing"
33
+ # title="translation missing: en.blog.post.title">Title</span>
30
34
  #
31
- # Third, it'll mark the translation as safe HTML if the key has the suffix
32
- # "_html" or the last element of the key is the word "html". For example,
33
- # calling translate("footer_html") or translate("footer.html") will return
34
- # a safe HTML string that won't be escaped by other HTML helper methods. This
35
- # naming convention helps to identify translations that include HTML tags so that
36
- # you know what kind of output to expect when you call translate in a template.
37
- def translate(key, options = {})
38
- options = options.dup
39
- has_default = options.has_key?(:default)
40
- remaining_defaults = Array(options.delete(:default)).compact
41
-
42
- if has_default && !remaining_defaults.first.kind_of?(Symbol)
43
- options[:default] = remaining_defaults.shift
44
- end
35
+ # This allows for views to display rather reasonable strings while still
36
+ # giving developers a way to find missing translations.
37
+ #
38
+ # If you would prefer missing translations to raise an error, you can
39
+ # opt out of span-wrapping behavior globally by setting
40
+ # <tt>ActionView::Base.raise_on_missing_translations = true</tt> or
41
+ # individually by passing <tt>raise: true</tt> as an option to
42
+ # <tt>translate</tt>.
43
+ #
44
+ # Second, if the key starts with a period <tt>translate</tt> will scope
45
+ # the key by the current partial. Calling <tt>translate(".foo")</tt> from
46
+ # the <tt>people/index.html.erb</tt> template is equivalent to calling
47
+ # <tt>translate("people.index.foo")</tt>. This makes it less
48
+ # repetitive to translate many keys within the same partial and provides
49
+ # a convention to scope keys consistently.
50
+ #
51
+ # Third, the translation will be marked as <tt>html_safe</tt> if the key
52
+ # has the suffix "_html" or the last element of the key is "html". Calling
53
+ # <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt>
54
+ # will return an HTML safe string that won't be escaped by other HTML
55
+ # helper methods. This naming convention helps to identify translations
56
+ # that include HTML tags so that you know what kind of output to expect
57
+ # when you call translate in a template and translators know which keys
58
+ # they can provide HTML values for.
59
+ #
60
+ # To access the translated text along with the fully resolved
61
+ # translation key, <tt>translate</tt> accepts a block:
62
+ #
63
+ # <%= translate(".relative_key") do |translation, resolved_key| %>
64
+ # <span title="<%= resolved_key %>"><%= translation %></span>
65
+ # <% end %>
66
+ #
67
+ # This enables annotate translated text to be aware of the scope it was
68
+ # resolved against.
69
+ #
70
+ def translate(key, **options)
71
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
72
+ key = key&.to_s unless key.is_a?(Symbol)
45
73
 
46
- # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
47
- # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
48
- # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
49
- if options[:raise] == false || (options.key?(:rescue_format) && options[:rescue_format].nil?)
50
- raise_error = false
51
- i18n_raise = false
52
- else
53
- raise_error = options[:raise] || options[:rescue_format] || ActionView::Base.raise_on_missing_translations
54
- i18n_raise = true
74
+ alternatives = if options.key?(:default)
75
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
55
76
  end
56
77
 
57
- if html_safe_translation_key?(key)
58
- html_safe_options = options.dup
59
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
60
- unless name == :count && value.is_a?(Numeric)
61
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
62
- end
78
+ options[:raise] = true if options[:raise].nil? && ActionView::Base.raise_on_missing_translations
79
+ default = MISSING_TRANSLATION
80
+
81
+ translation = while key || alternatives.present?
82
+ if alternatives.blank? && !options[:raise].nil?
83
+ default = NO_DEFAULT # let I18n handle missing translation
84
+ end
85
+
86
+ key = scope_key_by_partial(key)
87
+
88
+ if html_safe_translation_key?(key)
89
+ html_safe_options ||= html_escape_translation_options(options)
90
+ translated = I18n.translate(key, **html_safe_options, default: default)
91
+ break html_safe_translation(translated) unless translated.equal?(MISSING_TRANSLATION)
92
+ else
93
+ translated = I18n.translate(key, **options, default: default)
94
+ break translated unless translated.equal?(MISSING_TRANSLATION)
63
95
  end
64
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
65
96
 
66
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
67
- else
68
- I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
97
+ break alternatives.first if alternatives.present? && !alternatives.first.is_a?(Symbol)
98
+
99
+ first_key ||= key
100
+ key = alternatives&.shift
69
101
  end
70
- rescue I18n::MissingTranslationData => e
71
- if remaining_defaults.present?
72
- translate remaining_defaults.shift, options.merge(default: remaining_defaults)
73
- else
74
- raise e if raise_error
75
-
76
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
77
- content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}")
102
+
103
+ if key.nil? && !first_key.nil?
104
+ translation = missing_translation(first_key, options)
105
+ key = first_key
78
106
  end
107
+
108
+ block_given? ? yield(translation, key) : translation
79
109
  end
80
110
  alias :t :translate
81
111
 
82
112
  # Delegates to <tt>I18n.localize</tt> with no additional functionality.
83
113
  #
84
- # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
114
+ # See https://www.rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
85
115
  # for more information.
86
- def localize(*args)
87
- I18n.localize(*args)
116
+ def localize(object, **options)
117
+ I18n.localize(object, **options)
88
118
  end
89
119
  alias :l :localize
90
120
 
91
121
  private
122
+ MISSING_TRANSLATION = Object.new
123
+ private_constant :MISSING_TRANSLATION
124
+
125
+ NO_DEFAULT = [].freeze
126
+ private_constant :NO_DEFAULT
127
+
128
+ def self.i18n_option?(name)
129
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
130
+ end
131
+
92
132
  def scope_key_by_partial(key)
93
- if key.to_s.first == "."
133
+ if key&.start_with?(".")
94
134
  if @virtual_path
95
- @virtual_path.gsub(%r{/_?}, ".") + key.to_s
135
+ @_scope_key_by_partial_cache ||= {}
136
+ @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
137
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{key}"
96
138
  else
97
139
  raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
98
140
  end
@@ -101,8 +143,47 @@ module ActionView
101
143
  end
102
144
  end
103
145
 
146
+ def html_escape_translation_options(options)
147
+ return options if options.empty?
148
+ html_safe_options = options.dup
149
+
150
+ options.each do |name, value|
151
+ unless TranslationHelper.i18n_option?(name) || (name == :count && value.is_a?(Numeric))
152
+ html_safe_options[name] = ERB::Util.html_escape(value.to_s)
153
+ end
154
+ end
155
+
156
+ html_safe_options
157
+ end
158
+
104
159
  def html_safe_translation_key?(key)
105
- key.to_s =~ /(\b|_|\.)html$/
160
+ /(?:_|\b)html\z/.match?(key)
161
+ end
162
+
163
+ def html_safe_translation(translation)
164
+ if translation.respond_to?(:map)
165
+ translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
166
+ else
167
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
168
+ end
169
+ end
170
+
171
+ def missing_translation(key, options)
172
+ keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
173
+
174
+ title = +"translation missing: #{keys.join(".")}"
175
+
176
+ options.each do |name, value|
177
+ unless name == :scope
178
+ title << ", " << name.to_s << ": " << ERB::Util.html_escape(value)
179
+ end
180
+ end
181
+
182
+ if ActionView::Base.debug_missing_translation
183
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
184
+ else
185
+ title
186
+ end
106
187
  end
107
188
  end
108
189
  end
@@ -1,7 +1,9 @@
1
- require 'action_view/helpers/javascript_helper'
2
- require 'active_support/core_ext/array/access'
3
- require 'active_support/core_ext/hash/keys'
4
- require 'active_support/core_ext/string/output_safety'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/helpers/javascript_helper"
4
+ require "active_support/core_ext/array/access"
5
+ require "active_support/core_ext/hash/keys"
6
+ require "active_support/core_ext/string/output_safety"
5
7
 
6
8
  module ActionView
7
9
  # = Action View URL Helpers
@@ -35,21 +37,31 @@ module ActionView
35
37
  when :back
36
38
  _back_url
37
39
  else
38
- raise ArgumentError, "arguments passed to url_for can't be handled. Please require " +
40
+ raise ArgumentError, "arguments passed to url_for can't be handled. Please require " \
39
41
  "routes or provide your own implementation"
40
42
  end
41
43
  end
42
44
 
43
45
  def _back_url # :nodoc:
44
- referrer = controller.respond_to?(:request) && controller.request.env["HTTP_REFERER"]
45
- referrer || 'javascript:history.back()'
46
+ _filtered_referrer || "javascript:history.back()"
47
+ end
48
+ private :_back_url
49
+
50
+ def _filtered_referrer # :nodoc:
51
+ if controller.respond_to?(:request)
52
+ referrer = controller.request.env["HTTP_REFERER"]
53
+ if referrer && URI(referrer).scheme != "javascript"
54
+ referrer
55
+ end
56
+ end
57
+ rescue URI::InvalidURIError
46
58
  end
47
- protected :_back_url
59
+ private :_filtered_referrer
48
60
 
49
- # Creates a link tag of the given +name+ using a URL created by the set of +options+.
61
+ # Creates an anchor element of the given +name+ using a URL created by the set of +options+.
50
62
  # See the valid options in the documentation for +url_for+. It's also possible to
51
- # pass a String instead of an options hash, which generates a link tag that uses the
52
- # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead
63
+ # pass a \String instead of an options hash, which generates an anchor element that uses the
64
+ # value of the \String as the href for the link. Using a <tt>:back</tt> \Symbol instead
53
65
  # of an options hash will generate a link to the referrer (a JavaScript back link
54
66
  # will be used in place of a referrer if none exists). If +nil+ is passed as the name
55
67
  # the value of the link itself will become the name.
@@ -95,10 +107,9 @@ module ActionView
95
107
  # driver to prompt with the question specified (in this case, the
96
108
  # resulting text would be <tt>question?</tt>. If the user accepts, the
97
109
  # link is processed normally, otherwise no action is taken.
98
- # * <tt>:disable_with</tt> - Value of this parameter will be
99
- # used as the value for a disabled version of the submit
100
- # button when the form is submitted. This feature is provided
101
- # by the unobtrusive JavaScript driver.
110
+ # * <tt>:disable_with</tt> - Value of this parameter will be used as the
111
+ # name for a disabled version of the link. This feature is provided by
112
+ # the unobtrusive JavaScript driver.
102
113
  #
103
114
  # ==== Examples
104
115
  # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments
@@ -128,6 +139,11 @@ module ActionView
128
139
  # link_to "Profiles", controller: "profiles"
129
140
  # # => <a href="/profiles">Profiles</a>
130
141
  #
142
+ # When name is +nil+ the href is presented instead
143
+ #
144
+ # link_to nil, "http://example.com"
145
+ # # => <a href="http://www.example.com">http://www.example.com</a>
146
+ #
131
147
  # You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
132
148
  #
133
149
  # <%= link_to(@profile) do %>
@@ -161,7 +177,7 @@ module ActionView
161
177
  # # => <a href="/searches?query=ruby+on+rails">Ruby on Rails search</a>
162
178
  #
163
179
  # link_to "Nonsense search", searches_path(foo: "bar", baz: "quux")
164
- # # => <a href="/searches?foo=bar&amp;baz=quux">Nonsense search</a>
180
+ # # => <a href="/searches?foo=bar&baz=quux">Nonsense search</a>
165
181
  #
166
182
  # The only option specific to +link_to+ (<tt>:method</tt>) is used as follows:
167
183
  #
@@ -172,6 +188,11 @@ module ActionView
172
188
  #
173
189
  # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
174
190
  # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
191
+ #
192
+ # Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>:
193
+ #
194
+ # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow"
195
+ # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a>
175
196
  def link_to(name = nil, options = nil, html_options = nil, &block)
176
197
  html_options, options, name = options, name, block if block_given?
177
198
  options ||= {}
@@ -179,9 +200,9 @@ module ActionView
179
200
  html_options = convert_options_to_data_attributes(options, html_options)
180
201
 
181
202
  url = url_for(options)
182
- html_options['href'] ||= url
203
+ html_options["href"] ||= url
183
204
 
184
- content_tag(:a, name || url, html_options, &block)
205
+ content_tag("a", name || url, html_options, &block)
185
206
  end
186
207
 
187
208
  # Generates a form containing a single button that submits to the URL created
@@ -205,7 +226,7 @@ module ActionView
205
226
  # The +options+ hash accepts the same options as +url_for+.
206
227
  #
207
228
  # There are a few special +html_options+:
208
- # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
229
+ # * <tt>:method</tt> - \Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
209
230
  # <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>.
210
231
  # * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
211
232
  # * <tt>:data</tt> - This option can be used to add custom data attributes.
@@ -214,7 +235,7 @@ module ActionView
214
235
  # * <tt>:form</tt> - This hash will be form attributes
215
236
  # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
216
237
  # be placed
217
- # * <tt>:params</tt> - Hash of parameters to be rendered as hidden fields within the form.
238
+ # * <tt>:params</tt> - \Hash of parameters to be rendered as hidden fields within the form.
218
239
  #
219
240
  # ==== Data attributes
220
241
  #
@@ -229,101 +250,97 @@ module ActionView
229
250
  # ==== Examples
230
251
  # <%= button_to "New", action: "new" %>
231
252
  # # => "<form method="post" action="/controller/new" class="button_to">
232
- # # <div><input value="New" type="submit" /></div>
253
+ # # <input value="New" type="submit" />
233
254
  # # </form>"
234
255
  #
235
- # <%= button_to "New", new_articles_path %>
256
+ # <%= button_to "New", new_article_path %>
236
257
  # # => "<form method="post" action="/articles/new" class="button_to">
237
- # # <div><input value="New" type="submit" /></div>
258
+ # # <input value="New" type="submit" />
238
259
  # # </form>"
239
260
  #
240
261
  # <%= button_to [:make_happy, @user] do %>
241
262
  # Make happy <strong><%= @user.name %></strong>
242
263
  # <% end %>
243
264
  # # => "<form method="post" action="/users/1/make_happy" class="button_to">
244
- # # <div>
245
- # # <button type="submit">
246
- # # Make happy <strong><%= @user.name %></strong>
247
- # # </button>
248
- # # </div>
265
+ # # <button type="submit">
266
+ # # Make happy <strong><%= @user.name %></strong>
267
+ # # </button>
249
268
  # # </form>"
250
269
  #
251
270
  # <%= button_to "New", { action: "new" }, form_class: "new-thing" %>
252
271
  # # => "<form method="post" action="/controller/new" class="new-thing">
253
- # # <div><input value="New" type="submit" /></div>
272
+ # # <input value="New" type="submit" />
254
273
  # # </form>"
255
274
  #
256
275
  #
257
276
  # <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
258
277
  # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
259
- # # <div>
260
- # # <input value="Create" type="submit" />
261
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
262
- # # </div>
278
+ # # <input value="Create" type="submit" />
279
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
263
280
  # # </form>"
264
281
  #
265
282
  #
266
283
  # <%= button_to "Delete Image", { action: "delete", id: @image.id },
267
284
  # method: :delete, data: { confirm: "Are you sure?" } %>
268
285
  # # => "<form method="post" action="/images/delete/1" class="button_to">
269
- # # <div>
270
- # # <input type="hidden" name="_method" value="delete" />
271
- # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" />
272
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
273
- # # </div>
286
+ # # <input type="hidden" name="_method" value="delete" />
287
+ # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" />
288
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
274
289
  # # </form>"
275
290
  #
276
291
  #
277
292
  # <%= button_to('Destroy', 'http://www.example.com',
278
- # method: "delete", remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
293
+ # method: :delete, remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
279
294
  # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
280
- # # <div>
281
- # # <input name='_method' value='delete' type='hidden' />
282
- # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
283
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
284
- # # </div>
295
+ # # <input name='_method' value='delete' type='hidden' />
296
+ # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
297
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
285
298
  # # </form>"
286
299
  # #
287
300
  def button_to(name = nil, options = nil, html_options = nil, &block)
288
301
  html_options, options = options, name if block_given?
289
302
  options ||= {}
290
303
  html_options ||= {}
291
-
292
304
  html_options = html_options.stringify_keys
293
- convert_boolean_attributes!(html_options, %w(disabled))
294
305
 
295
306
  url = options.is_a?(String) ? options : url_for(options)
296
- remote = html_options.delete('remote')
297
- params = html_options.delete('params')
298
-
299
- method = html_options.delete('method').to_s
300
- method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe
301
-
302
- form_method = method == 'get' ? 'get' : 'post'
303
- form_options = html_options.delete('form') || {}
304
- form_options[:class] ||= html_options.delete('form_class') || 'button_to'
305
- form_options.merge!(method: form_method, action: url)
306
- form_options.merge!("data-remote" => "true") if remote
307
-
308
- request_token_tag = form_method == 'post' ? token_tag : ''
307
+ remote = html_options.delete("remote")
308
+ params = html_options.delete("params")
309
+
310
+ method = html_options.delete("method").to_s
311
+ method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe
312
+
313
+ form_method = method == "get" ? "get" : "post"
314
+ form_options = html_options.delete("form") || {}
315
+ form_options[:class] ||= html_options.delete("form_class") || "button_to"
316
+ form_options[:method] = form_method
317
+ form_options[:action] = url
318
+ form_options[:'data-remote'] = true if remote
319
+
320
+ request_token_tag = if form_method == "post"
321
+ request_method = method.empty? ? "post" : method
322
+ token_tag(nil, form_options: { action: url, method: request_method })
323
+ else
324
+ ""
325
+ end
309
326
 
310
327
  html_options = convert_options_to_data_attributes(options, html_options)
311
- html_options['type'] = 'submit'
328
+ html_options["type"] = "submit"
312
329
 
313
330
  button = if block_given?
314
- content_tag('button', html_options, &block)
331
+ content_tag("button", html_options, &block)
315
332
  else
316
- html_options['value'] = name || url
317
- tag('input', html_options)
333
+ html_options["value"] = name || url
334
+ tag("input", html_options)
318
335
  end
319
336
 
320
337
  inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
321
338
  if params
322
- params.each do |param_name, value|
323
- inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param)
339
+ to_form_params(params).each do |param|
340
+ inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value])
324
341
  end
325
342
  end
326
- content_tag('form', content_tag('div', inner_tags), form_options)
343
+ content_tag("form", inner_tags, form_options)
327
344
  end
328
345
 
329
346
  # Creates a link tag of the given +name+ using a URL created by the set of
@@ -389,22 +406,13 @@ module ActionView
389
406
  # # If not...
390
407
  # # => <a href="/accounts/signup">Reply</a>
391
408
  def link_to_unless(condition, name, options = {}, html_options = {}, &block)
392
- if condition
393
- if block_given?
394
- block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block)
395
- else
396
- ERB::Util.html_escape(name)
397
- end
398
- else
399
- link_to(name, options, html_options)
400
- end
409
+ link_to_if !condition, name, options, html_options, &block
401
410
  end
402
411
 
403
412
  # Creates a link tag of the given +name+ using a URL created by the set of
404
413
  # +options+ if +condition+ is true, otherwise only the name is
405
414
  # returned. To specialize the default behavior, you can pass a block that
406
- # accepts the name or the full argument list for +link_to_unless+ (see the examples
407
- # in +link_to_unless+).
415
+ # accepts the name or the full argument list for +link_to_if+.
408
416
  #
409
417
  # ==== Examples
410
418
  # <%= link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) %>
@@ -421,7 +429,15 @@ module ActionView
421
429
  # # If they are logged in...
422
430
  # # => <a href="/accounts/show/3">my_username</a>
423
431
  def link_to_if(condition, name, options = {}, html_options = {}, &block)
424
- link_to_unless !condition, name, options, html_options, &block
432
+ if condition
433
+ link_to(name, options, html_options)
434
+ else
435
+ if block_given?
436
+ block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block)
437
+ else
438
+ ERB::Util.html_escape(name)
439
+ end
440
+ end
425
441
  end
426
442
 
427
443
  # Creates a mailto link tag to the specified +email_address+, which is
@@ -436,6 +452,7 @@ module ActionView
436
452
  # * <tt>:body</tt> - Preset the body of the email.
437
453
  # * <tt>:cc</tt> - Carbon Copy additional recipients on the email.
438
454
  # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
455
+ # * <tt>:reply_to</tt> - Preset the Reply-To field of the email.
439
456
  #
440
457
  # ==== Obfuscation
441
458
  # Prior to Rails 4.0, +mail_to+ provided options for encoding the address
@@ -465,73 +482,64 @@ module ActionView
465
482
  html_options, name = name, nil if block_given?
466
483
  html_options = (html_options || {}).stringify_keys
467
484
 
468
- extras = %w{ cc bcc body subject }.map! { |item|
469
- option = html_options.delete(item) || next
470
- "#{item}=#{Rack::Utils.escape_path(option)}"
485
+ extras = %w{ cc bcc body subject reply_to }.map! { |item|
486
+ option = html_options.delete(item).presence || next
487
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
471
488
  }.compact
472
- extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&'))
489
+ extras = extras.empty? ? "" : "?" + extras.join("&")
473
490
 
474
491
  encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
475
- html_options["href"] = "mailto:#{encoded_email_address}#{extras}".html_safe
492
+ html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
476
493
 
477
- content_tag(:a, name || email_address, html_options, &block)
494
+ content_tag("a", name || email_address, html_options, &block)
478
495
  end
479
496
 
480
497
  # True if the current request URI was generated by the given +options+.
481
498
  #
482
499
  # ==== Examples
483
- # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc</tt> action.
500
+ # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action.
484
501
  #
485
502
  # current_page?(action: 'process')
486
503
  # # => false
487
504
  #
488
- # current_page?(controller: 'shop', action: 'checkout')
489
- # # => true
490
- #
491
- # current_page?(controller: 'shop', action: 'checkout', order: 'asc')
492
- # # => false
493
- #
494
505
  # current_page?(action: 'checkout')
495
506
  # # => true
496
507
  #
497
508
  # current_page?(controller: 'library', action: 'checkout')
498
509
  # # => false
499
510
  #
500
- # current_page?('http://www.example.com/shop/checkout')
501
- # # => true
502
- #
503
- # current_page?('/shop/checkout')
511
+ # current_page?(controller: 'shop', action: 'checkout')
504
512
  # # => true
505
513
  #
506
- # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action.
507
- #
508
- # current_page?(action: 'process')
514
+ # current_page?(controller: 'shop', action: 'checkout', order: 'asc')
509
515
  # # => false
510
516
  #
511
- # current_page?(controller: 'shop', action: 'checkout')
512
- # # => true
513
- #
514
517
  # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1')
515
518
  # # => true
516
519
  #
517
520
  # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2')
518
521
  # # => false
519
522
  #
520
- # current_page?(controller: 'shop', action: 'checkout', order: 'desc')
523
+ # current_page?('http://www.example.com/shop/checkout')
524
+ # # => true
525
+ #
526
+ # current_page?('http://www.example.com/shop/checkout', check_parameters: true)
521
527
  # # => false
522
528
  #
523
- # current_page?(action: 'checkout')
529
+ # current_page?('/shop/checkout')
524
530
  # # => true
525
531
  #
526
- # current_page?(controller: 'library', action: 'checkout')
527
- # # => false
532
+ # current_page?('http://www.example.com/shop/checkout?order=desc&page=1')
533
+ # # => true
528
534
  #
529
535
  # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product.
530
536
  #
531
537
  # current_page?(controller: 'product', action: 'index')
532
538
  # # => false
533
539
  #
534
- def current_page?(options)
540
+ # We can also pass in the symbol arguments instead of strings.
541
+ #
542
+ def current_page?(options = nil, check_parameters: false, **options_as_kwargs)
535
543
  unless request
536
544
  raise "You cannot use helpers that need to determine the current " \
537
545
  "page unless your view context provides a Request object " \
@@ -540,89 +548,223 @@ module ActionView
540
548
 
541
549
  return false unless request.get? || request.head?
542
550
 
543
- url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
551
+ options ||= options_as_kwargs
552
+ check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
553
+ url_string = URI::DEFAULT_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
544
554
 
545
555
  # We ignore any extra parameters in the request_uri if the
546
- # submitted url doesn't have any either. This lets the function
556
+ # submitted URL doesn't have any either. This lets the function
547
557
  # work with things like ?order=asc
548
- request_uri = url_string.index("?") ? request.fullpath : request.path
549
- request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY)
558
+ # the behaviour can be disabled with check_parameters: true
559
+ request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
560
+ request_uri = URI::DEFAULT_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
561
+
562
+ if url_string.start_with?("/") && url_string != "/"
563
+ url_string.chomp!("/")
564
+ request_uri.chomp!("/")
565
+ end
550
566
 
551
- if url_string =~ /^\w+:\/\//
567
+ if %r{^\w+://}.match?(url_string)
552
568
  url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
553
569
  else
554
570
  url_string == request_uri
555
571
  end
556
572
  end
557
573
 
574
+ # Creates an SMS anchor link tag to the specified +phone_number+, which is
575
+ # also used as the name of the link unless +name+ is specified. Additional
576
+ # HTML attributes for the link can be passed in +html_options+.
577
+ #
578
+ # When clicked, an SMS message is prepopulated with the passed phone number
579
+ # and optional +body+ value.
580
+ #
581
+ # +sms_to+ has a +body+ option for customizing the SMS message itself by
582
+ # passing special keys to +html_options+.
583
+ #
584
+ # ==== Options
585
+ # * <tt>:body</tt> - Preset the body of the message.
586
+ #
587
+ # ==== Examples
588
+ # sms_to "5155555785"
589
+ # # => <a href="sms:5155555785;">5155555785</a>
590
+ #
591
+ # sms_to "5155555785", "Text me"
592
+ # # => <a href="sms:5155555785;">Text me</a>
593
+ #
594
+ # sms_to "5155555785", "Text me",
595
+ # body: "Hello Jim I have a question about your product."
596
+ # # => <a href="sms:5155555785;?body=Hello%20Jim%20I%20have%20a%20question%20about%20your%20product">Text me</a>
597
+ #
598
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
599
+ #
600
+ # <%= sms_to "5155555785" do %>
601
+ # <strong>Text me:</strong>
602
+ # <% end %>
603
+ # # => <a href="sms:5155555785;">
604
+ # <strong>Text me:</strong>
605
+ # </a>
606
+ def sms_to(phone_number, name = nil, html_options = {}, &block)
607
+ html_options, name = name, nil if block_given?
608
+ html_options = (html_options || {}).stringify_keys
609
+
610
+ extras = %w{ body }.map! { |item|
611
+ option = html_options.delete(item).presence || next
612
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
613
+ }.compact
614
+ extras = extras.empty? ? "" : "?&" + extras.join("&")
615
+
616
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
617
+ html_options["href"] = "sms:#{encoded_phone_number};#{extras}"
618
+
619
+ content_tag("a", name || phone_number, html_options, &block)
620
+ end
621
+
622
+ # Creates a TEL anchor link tag to the specified +phone_number+, which is
623
+ # also used as the name of the link unless +name+ is specified. Additional
624
+ # HTML attributes for the link can be passed in +html_options+.
625
+ #
626
+ # When clicked, the default app to make calls is opened, and it
627
+ # is prepopulated with the passed phone number and optional
628
+ # +country_code+ value.
629
+ #
630
+ # +phone_to+ has an optional +country_code+ option which automatically adds the country
631
+ # code as well as the + sign in the phone numer that gets prepopulated,
632
+ # for example if +country_code: "01"+ +\+01+ will be prepended to the
633
+ # phone numer, by passing special keys to +html_options+.
634
+ #
635
+ # ==== Options
636
+ # * <tt>:country_code</tt> - Prepends the country code to the number
637
+ #
638
+ # ==== Examples
639
+ # phone_to "1234567890"
640
+ # # => <a href="tel:1234567890">1234567890</a>
641
+ #
642
+ # phone_to "1234567890", "Phone me"
643
+ # # => <a href="tel:134567890">Phone me</a>
644
+ #
645
+ # phone_to "1234567890", "Phone me", country_code: "01"
646
+ # # => <a href="tel:+015155555785">Phone me</a>
647
+ #
648
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
649
+ #
650
+ # <%= phone_to "1234567890" do %>
651
+ # <strong>Phone me:</strong>
652
+ # <% end %>
653
+ # # => <a href="tel:1234567890">
654
+ # <strong>Phone me:</strong>
655
+ # </a>
656
+ def phone_to(phone_number, name = nil, html_options = {}, &block)
657
+ html_options, name = name, nil if block_given?
658
+ html_options = (html_options || {}).stringify_keys
659
+
660
+ country_code = html_options.delete("country_code").presence
661
+ country_code = country_code.nil? ? "" : "+#{ERB::Util.url_encode(country_code)}"
662
+
663
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
664
+ html_options["href"] = "tel:#{country_code}#{encoded_phone_number}"
665
+
666
+ content_tag("a", name || phone_number, html_options, &block)
667
+ end
668
+
558
669
  private
559
670
  def convert_options_to_data_attributes(options, html_options)
560
671
  if html_options
561
672
  html_options = html_options.stringify_keys
562
- html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options)
673
+ html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)
563
674
 
564
- method = html_options.delete('method')
675
+ method = html_options.delete("method")
565
676
 
566
677
  add_method_to_attributes!(html_options, method) if method
567
678
 
568
679
  html_options
569
680
  else
570
- link_to_remote_options?(options) ? {'data-remote' => 'true'} : {}
681
+ link_to_remote_options?(options) ? { "data-remote" => "true" } : {}
571
682
  end
572
683
  end
573
684
 
574
685
  def link_to_remote_options?(options)
575
686
  if options.is_a?(Hash)
576
- options.delete('remote') || options.delete(:remote)
687
+ options.delete("remote") || options.delete(:remote)
577
688
  end
578
689
  end
579
690
 
580
691
  def add_method_to_attributes!(html_options, method)
581
- if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/
582
- html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip
692
+ if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
693
+ if html_options["rel"].blank?
694
+ html_options["rel"] = "nofollow"
695
+ else
696
+ html_options["rel"] = "#{html_options["rel"]} nofollow"
697
+ end
583
698
  end
584
699
  html_options["data-method"] = method
585
700
  end
586
701
 
587
- # Processes the +html_options+ hash, converting the boolean
588
- # attributes from true/false form into the form required by
589
- # HTML/XHTML. (An attribute is considered to be boolean if
590
- # its name is listed in the given +bool_attrs+ array.)
591
- #
592
- # More specifically, for each boolean attribute in +html_options+
593
- # given as:
702
+ STRINGIFIED_COMMON_METHODS = {
703
+ get: "get",
704
+ delete: "delete",
705
+ patch: "patch",
706
+ post: "post",
707
+ put: "put",
708
+ }.freeze
709
+
710
+ def method_not_get_method?(method)
711
+ return false unless method
712
+ (STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get"
713
+ end
714
+
715
+ def token_tag(token = nil, form_options: {})
716
+ if token != false && defined?(protect_against_forgery?) && protect_against_forgery?
717
+ token ||= form_authenticity_token(form_options: form_options)
718
+ tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
719
+ else
720
+ ""
721
+ end
722
+ end
723
+
724
+ def method_tag(method)
725
+ tag("input", type: "hidden", name: "_method", value: method.to_s)
726
+ end
727
+
728
+ # Returns an array of hashes each containing :name and :value keys
729
+ # suitable for use as the names and values of form input fields:
594
730
  #
595
- # "attr" => bool_value
731
+ # to_form_params(name: 'David', nationality: 'Danish')
732
+ # # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}]
596
733
  #
597
- # if the associated +bool_value+ evaluates to true, it is
598
- # replaced with the attribute's name; otherwise the attribute is
599
- # removed from the +html_options+ hash. (See the XHTML 1.0 spec,
600
- # section 4.5 "Attribute Minimization" for more:
601
- # http://www.w3.org/TR/xhtml1/#h-4.5)
734
+ # to_form_params(country: { name: 'Denmark' })
735
+ # # => [{name: 'country[name]', value: 'Denmark'}]
602
736
  #
603
- # Returns the updated +html_options+ hash, which is also modified
604
- # in place.
737
+ # to_form_params(countries: ['Denmark', 'Sweden']})
738
+ # # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}]
605
739
  #
606
- # Example:
740
+ # An optional namespace can be passed to enclose key names:
607
741
  #
608
- # convert_boolean_attributes!( html_options,
609
- # %w( checked disabled readonly ) )
610
- def convert_boolean_attributes!(html_options, bool_attrs)
611
- bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) }
612
- html_options
613
- end
742
+ # to_form_params({ name: 'Denmark' }, 'country')
743
+ # # => [{name: 'country[name]', value: 'Denmark'}]
744
+ def to_form_params(attribute, namespace = nil)
745
+ attribute = if attribute.respond_to?(:permitted?)
746
+ attribute.to_h
747
+ else
748
+ attribute
749
+ end
614
750
 
615
- def token_tag(token=nil)
616
- if token != false && protect_against_forgery?
617
- token ||= form_authenticity_token
618
- tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
751
+ params = []
752
+ case attribute
753
+ when Hash
754
+ attribute.each do |key, value|
755
+ prefix = namespace ? "#{namespace}[#{key}]" : key
756
+ params.push(*to_form_params(value, prefix))
757
+ end
758
+ when Array
759
+ array_prefix = "#{namespace}[]"
760
+ attribute.each do |value|
761
+ params.push(*to_form_params(value, array_prefix))
762
+ end
619
763
  else
620
- ''
764
+ params << { name: namespace.to_s, value: attribute.to_param }
621
765
  end
622
- end
623
766
 
624
- def method_tag(method)
625
- tag('input', type: 'hidden', name: '_method', value: method.to_s)
767
+ params.sort_by { |pair| pair[:name] }
626
768
  end
627
769
  end
628
770
  end