hanami 2.0.3 → 2.1.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -2
- data/LICENSE.md +1 -1
- data/README.md +26 -10
- data/hanami.gemspec +2 -2
- data/lib/hanami/app.rb +5 -0
- data/lib/hanami/config/actions.rb +4 -11
- data/lib/hanami/config/assets.rb +84 -0
- data/lib/hanami/config/null_config.rb +3 -0
- data/lib/hanami/config/views.rb +0 -4
- data/lib/hanami/config.rb +71 -5
- data/lib/hanami/extensions/action/slice_configured_action.rb +15 -7
- data/lib/hanami/extensions/action.rb +8 -6
- data/lib/hanami/extensions/router/errors.rb +58 -0
- data/lib/hanami/extensions/view/context.rb +129 -60
- data/lib/hanami/extensions/view/part.rb +26 -0
- data/lib/hanami/extensions/view/scope.rb +26 -0
- data/lib/hanami/extensions/view/slice_configured_context.rb +0 -2
- data/lib/hanami/extensions/view/slice_configured_helpers.rb +44 -0
- data/lib/hanami/extensions/view/slice_configured_view.rb +106 -21
- data/lib/hanami/extensions/view/standard_helpers.rb +18 -0
- data/lib/hanami/extensions.rb +10 -3
- data/lib/hanami/helpers/assets_helper.rb +752 -0
- data/lib/hanami/helpers/form_helper/form_builder.rb +1391 -0
- data/lib/hanami/helpers/form_helper/values.rb +75 -0
- data/lib/hanami/helpers/form_helper.rb +213 -0
- data/lib/hanami/middleware/assets.rb +21 -0
- data/lib/hanami/middleware/public_errors_app.rb +75 -0
- data/lib/hanami/middleware/render_errors.rb +90 -0
- data/lib/hanami/providers/assets.rb +44 -0
- data/lib/hanami/rake_tasks.rb +19 -18
- data/lib/hanami/settings.rb +1 -1
- data/lib/hanami/slice.rb +48 -2
- data/lib/hanami/slice_configurable.rb +3 -2
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami/web/rack_logger.rb +1 -1
- data/lib/hanami.rb +3 -3
- data/spec/integration/action/view_rendering/view_context_spec.rb +221 -0
- data/spec/integration/action/view_rendering_spec.rb +0 -18
- data/spec/integration/assets/assets_spec.rb +101 -0
- data/spec/integration/assets/serve_static_assets_spec.rb +152 -0
- data/spec/integration/logging/exception_logging_spec.rb +115 -0
- data/spec/integration/logging/notifications_spec.rb +68 -0
- data/spec/integration/logging/request_logging_spec.rb +128 -0
- data/spec/integration/rack_app/middleware_spec.rb +22 -22
- data/spec/integration/rack_app/rack_app_spec.rb +3 -220
- data/spec/integration/rake_tasks_spec.rb +107 -0
- data/spec/integration/view/config/default_context_spec.rb +149 -0
- data/spec/integration/view/{inflector_spec.rb → config/inflector_spec.rb} +1 -1
- data/spec/integration/view/config/part_class_spec.rb +147 -0
- data/spec/integration/view/config/part_namespace_spec.rb +103 -0
- data/spec/integration/view/config/paths_spec.rb +119 -0
- data/spec/integration/view/config/scope_class_spec.rb +147 -0
- data/spec/integration/view/config/scope_namespace_spec.rb +103 -0
- data/spec/integration/view/config/template_spec.rb +38 -0
- data/spec/integration/view/context/assets_spec.rb +3 -9
- data/spec/integration/view/context/request_spec.rb +3 -7
- data/spec/integration/view/helpers/form_helper_spec.rb +174 -0
- data/spec/integration/view/helpers/part_helpers_spec.rb +124 -0
- data/spec/integration/view/helpers/scope_helpers_spec.rb +84 -0
- data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +162 -0
- data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +119 -0
- data/spec/integration/view/slice_configuration_spec.rb +9 -9
- data/spec/integration/web/render_detailed_errors_spec.rb +107 -0
- data/spec/integration/web/render_errors_spec.rb +242 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/app_integration.rb +46 -2
- data/spec/support/matchers.rb +32 -0
- data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +24 -36
- data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +4 -3
- data/spec/unit/hanami/config/actions/default_values_spec.rb +3 -6
- data/spec/unit/hanami/config/render_detailed_errors_spec.rb +25 -0
- data/spec/unit/hanami/config/render_errors_spec.rb +25 -0
- data/spec/unit/hanami/config/views_spec.rb +0 -18
- data/spec/unit/hanami/env_spec.rb +11 -25
- data/spec/unit/hanami/extensions/view/context_spec.rb +59 -0
- data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +109 -0
- data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +132 -0
- data/spec/unit/hanami/helpers/assets_helper/favicon_link_tag_spec.rb +91 -0
- data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +92 -0
- data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +143 -0
- data/spec/unit/hanami/helpers/assets_helper/stylesheet_link_tag_spec.rb +126 -0
- data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +132 -0
- data/spec/unit/hanami/helpers/form_helper_spec.rb +2826 -0
- data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +27 -0
- data/spec/unit/hanami/router/errors/not_found_error_spec.rb +22 -0
- data/spec/unit/hanami/slice_configurable_spec.rb +18 -0
- data/spec/unit/hanami/version_spec.rb +1 -1
- data/spec/unit/hanami/web/rack_logger_spec.rb +1 -1
- metadata +95 -35
- data/lib/hanami/assets/app_config.rb +0 -61
- data/lib/hanami/assets/config.rb +0 -53
- data/spec/integration/action/view_integration_spec.rb +0 -165
- data/spec/integration/view/part_namespace_spec.rb +0 -96
- data/spec/integration/view/path_spec.rb +0 -56
- data/spec/integration/view/template_spec.rb +0 -68
- data/spec/isolation/hanami/application/already_configured_spec.rb +0 -19
- data/spec/isolation/hanami/application/inherit_anonymous_class_spec.rb +0 -10
- data/spec/isolation/hanami/application/inherit_concrete_class_spec.rb +0 -14
- data/spec/isolation/hanami/application/not_configured_spec.rb +0 -9
- data/spec/isolation/hanami/application/routes/configured_spec.rb +0 -44
- data/spec/isolation/hanami/application/routes/not_configured_spec.rb +0 -16
- data/spec/isolation/hanami/boot/success_spec.rb +0 -50
@@ -0,0 +1,1391 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hanami/view"
|
4
|
+
require_relative "values"
|
5
|
+
|
6
|
+
module Hanami
|
7
|
+
module Helpers
|
8
|
+
module FormHelper
|
9
|
+
# A range of convenient methods for building the fields within an HTML form, integrating with
|
10
|
+
# request params and template locals to populate the fields with appropriate values.
|
11
|
+
#
|
12
|
+
# @see FormHelper#form_for
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
# @since 2.0.0
|
16
|
+
class FormBuilder
|
17
|
+
# Set of HTTP methods that are understood by web browsers
|
18
|
+
#
|
19
|
+
# @since 2.0.0
|
20
|
+
# @api private
|
21
|
+
BROWSER_METHODS = %w[GET POST].freeze
|
22
|
+
private_constant :BROWSER_METHODS
|
23
|
+
|
24
|
+
# Set of HTTP methods that should NOT generate CSRF token
|
25
|
+
#
|
26
|
+
# @since 2.0.0
|
27
|
+
# @api private
|
28
|
+
EXCLUDED_CSRF_METHODS = %w[GET].freeze
|
29
|
+
private_constant :EXCLUDED_CSRF_METHODS
|
30
|
+
|
31
|
+
# Separator for accept attribute of file input
|
32
|
+
#
|
33
|
+
# @since 2.0.0
|
34
|
+
# @api private
|
35
|
+
#
|
36
|
+
# @see #file_input
|
37
|
+
ACCEPT_SEPARATOR = ","
|
38
|
+
private_constant :ACCEPT_SEPARATOR
|
39
|
+
|
40
|
+
# Default value for unchecked check box
|
41
|
+
#
|
42
|
+
# @since 2.0.0
|
43
|
+
# @api private
|
44
|
+
#
|
45
|
+
# @see #check_box
|
46
|
+
DEFAULT_UNCHECKED_VALUE = "0"
|
47
|
+
private_constant :DEFAULT_UNCHECKED_VALUE
|
48
|
+
|
49
|
+
# Default value for checked check box
|
50
|
+
#
|
51
|
+
# @since 2.0.0
|
52
|
+
# @api private
|
53
|
+
#
|
54
|
+
# @see #check_box
|
55
|
+
DEFAULT_CHECKED_VALUE = "1"
|
56
|
+
private_constant :DEFAULT_CHECKED_VALUE
|
57
|
+
|
58
|
+
# Input name separator
|
59
|
+
#
|
60
|
+
# @since 2.0.0
|
61
|
+
# @api private
|
62
|
+
INPUT_NAME_SEPARATOR = "."
|
63
|
+
private_constant :INPUT_NAME_SEPARATOR
|
64
|
+
|
65
|
+
# Empty string
|
66
|
+
#
|
67
|
+
# @since 2.0.0
|
68
|
+
# @api private
|
69
|
+
#
|
70
|
+
# @see #password_field
|
71
|
+
EMPTY_STRING = ""
|
72
|
+
private_constant :EMPTY_STRING
|
73
|
+
|
74
|
+
include Hanami::View::Helpers::EscapeHelper
|
75
|
+
include Hanami::View::Helpers::TagHelper
|
76
|
+
|
77
|
+
# @api private
|
78
|
+
# @since 2.0.0
|
79
|
+
attr_reader :base_name
|
80
|
+
private :base_name
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
# @since 2.0.0
|
84
|
+
attr_reader :values
|
85
|
+
private :values
|
86
|
+
|
87
|
+
# @api private
|
88
|
+
# @since 2.0.0
|
89
|
+
attr_reader :inflector
|
90
|
+
private :inflector
|
91
|
+
|
92
|
+
# @api private
|
93
|
+
# @since 2.0.0
|
94
|
+
attr_reader :form_attributes
|
95
|
+
private :form_attributes
|
96
|
+
|
97
|
+
# Returns a new form builder.
|
98
|
+
#
|
99
|
+
# @param inflector [Dry::Inflector] the app inflector
|
100
|
+
# @param base_name [String, nil] the base name to use for all fields in the form
|
101
|
+
# @param values [Hanami::Helpers::FormHelper::Values] the values for the form
|
102
|
+
#
|
103
|
+
# @return [self]
|
104
|
+
#
|
105
|
+
# @see Hanami::Helpers::FormHelper#form_for
|
106
|
+
#
|
107
|
+
# @api private
|
108
|
+
# @since 2.0.0
|
109
|
+
def initialize(inflector:, form_attributes:, base_name: nil, values: Values.new)
|
110
|
+
@base_name = base_name
|
111
|
+
@values = values
|
112
|
+
@form_attributes = form_attributes
|
113
|
+
@inflector = inflector
|
114
|
+
end
|
115
|
+
|
116
|
+
# @api private
|
117
|
+
# @since 2.0.0
|
118
|
+
def call(content, **attributes)
|
119
|
+
attributes["accept-charset"] ||= DEFAULT_CHARSET
|
120
|
+
|
121
|
+
method_override, original_form_method = _form_method(attributes)
|
122
|
+
csrf_token, token = _csrf_token(values, attributes)
|
123
|
+
|
124
|
+
tag.form(**attributes) do
|
125
|
+
(+"").tap { |inner|
|
126
|
+
inner << input(type: "hidden", name: "_method", value: original_form_method) if method_override
|
127
|
+
inner << input(type: "hidden", name: "_csrf_token", value: token) if csrf_token
|
128
|
+
inner << content
|
129
|
+
}.html_safe
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Applies the base input name to all fields within the given block.
|
134
|
+
#
|
135
|
+
# This can be helpful when generating a set of nested fields.
|
136
|
+
#
|
137
|
+
# This is a convenience only. You can achieve the same result by including the base name at
|
138
|
+
# the beginning of each input name.
|
139
|
+
#
|
140
|
+
# @param name [String] the base name to be used for all fields in the block
|
141
|
+
# @yieldparam [FormBuilder] the form builder for the nested fields
|
142
|
+
#
|
143
|
+
# @example Basic usage
|
144
|
+
# <% f.fields_for "address" do |fa| %>
|
145
|
+
# <%= fa.text_field "street" %>
|
146
|
+
# <%= fa.text_field "suburb" %>
|
147
|
+
# <% end %>
|
148
|
+
#
|
149
|
+
# # A convenience for:
|
150
|
+
# # <%= f.text_field "address.street" %>
|
151
|
+
# # <%= f.text_field "address.suburb" %>
|
152
|
+
#
|
153
|
+
# =>
|
154
|
+
# <input type="text" name="delivery[customer_name]" id="delivery-customer-name" value="">
|
155
|
+
# <input type="text" name="delivery[address][street]" id="delivery-address-street" value="">
|
156
|
+
#
|
157
|
+
# @example Multiple levels of nesting
|
158
|
+
# <% f.fields_for "address" do |fa| %>
|
159
|
+
# <%= fa.text_field "street" %>
|
160
|
+
#
|
161
|
+
# <% fa.fields_for "location" do |fl| %>
|
162
|
+
# <%= fl.text_field "city" %>
|
163
|
+
# <% end %>
|
164
|
+
# <% end %>
|
165
|
+
#
|
166
|
+
# =>
|
167
|
+
# <input type="text" name="delivery[address][street]" id="delivery-address-street" value="">
|
168
|
+
# <input type="text" name="delivery[address][location][city]" id="delivery-address-location-city" value="">
|
169
|
+
#
|
170
|
+
# @api public
|
171
|
+
# @since 2.0.0
|
172
|
+
def fields_for(name, *yield_args)
|
173
|
+
new_base_name = [base_name, name.to_s].compact.join(INPUT_NAME_SEPARATOR)
|
174
|
+
|
175
|
+
builder = self.class.new(
|
176
|
+
base_name: new_base_name,
|
177
|
+
values: values,
|
178
|
+
form_attributes: form_attributes,
|
179
|
+
inflector: inflector
|
180
|
+
)
|
181
|
+
|
182
|
+
yield(builder, *yield_args)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Yields to the given block for each element in the matching collection value, and applies
|
186
|
+
# the base input name to all fields within the block.
|
187
|
+
#
|
188
|
+
# Use this whenever generating form fields for an collection of nested fields.
|
189
|
+
#
|
190
|
+
# @param name [String] the input name, also used as the base input name for all fields
|
191
|
+
# within the block
|
192
|
+
# @yieldparam [FormBuilder] the form builder for the nested fields
|
193
|
+
# @yieldparam [Integer] the index of the iteration over the colletion, starting from zero
|
194
|
+
# @yieldparam [Object] the value of the element from the collection
|
195
|
+
#
|
196
|
+
# @example Basic usage
|
197
|
+
# <% f.fields_for_collection("addresses") do |fa| %>
|
198
|
+
# <%= fa.text_field("street") %>
|
199
|
+
# <% end %>
|
200
|
+
#
|
201
|
+
# =>
|
202
|
+
# <input type="text" name="delivery[addresses][][street]" id="delivery-address-0-street" value="">
|
203
|
+
# <input type="text" name="delivery[addresses][][street]" id="delivery-address-1-street" value="">
|
204
|
+
#
|
205
|
+
# @example Yielding index and value
|
206
|
+
# <% f.fields_for_collection("bill.addresses") do |fa, i, address| %>
|
207
|
+
# <div class="form-group">
|
208
|
+
# Address id: <%= address.id %>
|
209
|
+
# <%= fa.label("street") %>
|
210
|
+
# <%= fa.text_field("street", data: {index: i.to_s}) %>
|
211
|
+
# </div>
|
212
|
+
# <% end %>
|
213
|
+
#
|
214
|
+
# =>
|
215
|
+
# <div class="form-group">
|
216
|
+
# Address id: 23
|
217
|
+
# <label for="bill-addresses-0-street">Street</label>
|
218
|
+
# <input type="text" name="bill[addresses][][street]" id="bill-addresses-0-street" value="5th Ave" data-index="0">
|
219
|
+
# </div>
|
220
|
+
# <div class="form-group">
|
221
|
+
# Address id: 42
|
222
|
+
# <label for="bill-addresses-1-street">Street</label>
|
223
|
+
# <input type="text" name="bill[addresses][][street]" id="bill-addresses-1-street" value="4th Ave" data-index="1">
|
224
|
+
# </div>
|
225
|
+
#
|
226
|
+
# @api public
|
227
|
+
# @since 2.0.0
|
228
|
+
def fields_for_collection(name, &block)
|
229
|
+
collection_base_name = [base_name, name.to_s].compact.join(INPUT_NAME_SEPARATOR)
|
230
|
+
|
231
|
+
_value(name).each_with_index do |value, index|
|
232
|
+
fields_for("#{collection_base_name}.#{index}", index, value, &block)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns a label tag.
|
237
|
+
#
|
238
|
+
# @return [String] the tag
|
239
|
+
#
|
240
|
+
# @overload label(field_name, **attributes)
|
241
|
+
# Returns a label tag for the given field name, with a humanized version of the field name
|
242
|
+
# as the tag's content.
|
243
|
+
#
|
244
|
+
# @param field_name [String] the field name
|
245
|
+
# @param attributes [Hash] the tag attributes
|
246
|
+
#
|
247
|
+
# @example
|
248
|
+
# <%= f.label("book.extended_title") %>
|
249
|
+
# # => <label for="book-extended-title">Extended title</label>
|
250
|
+
#
|
251
|
+
# @example HTML attributes
|
252
|
+
# <%= f.label("book.title", class: "form-label") %>
|
253
|
+
# # => <label for="book-title" class="form-label">Title</label>
|
254
|
+
#
|
255
|
+
# @overload label(content, **attributes)
|
256
|
+
# Returns a label tag for the field name given as `for:`, with the given content string as
|
257
|
+
# the tag's content.
|
258
|
+
#
|
259
|
+
# @param content [String] the tag's content
|
260
|
+
# @param for [String] the field name
|
261
|
+
# @param attributes [Hash] the tag attributes
|
262
|
+
#
|
263
|
+
# @example
|
264
|
+
# <%= f.label("Title", for: "book.extended_title") %>
|
265
|
+
# # => <label for="book-extended-title">Title</label>
|
266
|
+
#
|
267
|
+
# f.label("book.extended_title", for: "ext-title")
|
268
|
+
# # => <label for="ext-title">Extended title</label>
|
269
|
+
#
|
270
|
+
# @overload label(field_name, **attributes, &block)
|
271
|
+
# Returns a label tag for the given field name, with the return value of the given block
|
272
|
+
# as the tag's content.
|
273
|
+
#
|
274
|
+
# @param field_name [String] the field name
|
275
|
+
# @param attributes [Hash] the tag attributes
|
276
|
+
# @yieldreturn [String] the tag content
|
277
|
+
#
|
278
|
+
# @example
|
279
|
+
# <%= f.label for: "book.free_shipping" do %>
|
280
|
+
# Free shipping
|
281
|
+
# <abbr title="optional" aria-label="optional">*</abbr>
|
282
|
+
# <% end %>
|
283
|
+
#
|
284
|
+
# # =>
|
285
|
+
# <label for="book-free-shipping">
|
286
|
+
# Free shipping
|
287
|
+
# <abbr title="optional" aria-label="optional">*</abbr>
|
288
|
+
# </label>
|
289
|
+
#
|
290
|
+
# @api public
|
291
|
+
# @since 2.0.0
|
292
|
+
def label(content = nil, **attributes, &block)
|
293
|
+
for_attribute_given = attributes.key?(:for)
|
294
|
+
|
295
|
+
attributes[:for] = _input_id(attributes[:for] || content)
|
296
|
+
|
297
|
+
if content && !for_attribute_given
|
298
|
+
content = inflector.humanize(content.split(INPUT_NAME_SEPARATOR).last)
|
299
|
+
end
|
300
|
+
|
301
|
+
tag.label(content, **attributes, &block)
|
302
|
+
end
|
303
|
+
|
304
|
+
# @overload fieldset(**attributes, &block)
|
305
|
+
# Returns a fieldset tag.
|
306
|
+
#
|
307
|
+
# @param attributes [Hash] the tag's HTML attributes
|
308
|
+
# @yieldreturn [String] the tag's content
|
309
|
+
#
|
310
|
+
# @return [String] the tag
|
311
|
+
#
|
312
|
+
# @example
|
313
|
+
# <%= f.fieldset do %>
|
314
|
+
# <%= f.legend("Author") %>
|
315
|
+
# <%= f.label("author.name") %>
|
316
|
+
# <%= f.text_field("author.name") %>
|
317
|
+
# <% end %>
|
318
|
+
#
|
319
|
+
# # =>
|
320
|
+
# <fieldset>
|
321
|
+
# <legend>Author</legend>
|
322
|
+
# <label for="book-author-name">Name</label>
|
323
|
+
# <input type="text" name="book[author][name]" id="book-author-name" value="">
|
324
|
+
# </fieldset>
|
325
|
+
#
|
326
|
+
# @since 2.0.0
|
327
|
+
# @api public
|
328
|
+
def fieldset(...)
|
329
|
+
# This is here only for documentation purposes
|
330
|
+
tag.fieldset(...)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Returns the tags for a check box.
|
334
|
+
#
|
335
|
+
# When editing a resource, the form automatically assigns the `checked` HTML attribute for
|
336
|
+
# the check box tag.
|
337
|
+
#
|
338
|
+
# Returns a hidden input tag in preceding the check box input tag. This ensures that
|
339
|
+
# unchecked values are submitted with the form.
|
340
|
+
#
|
341
|
+
# @param name [String] the input name
|
342
|
+
# @param attributes [Hash] the HTML attributes for the check box tag
|
343
|
+
# @option attributes [String] :checked_value (defaults to "1")
|
344
|
+
# @option attributes [String] :unchecked_value (defaults to "0")
|
345
|
+
#
|
346
|
+
# @return [String] the tags
|
347
|
+
#
|
348
|
+
# @example Basic usage
|
349
|
+
# f.check_box("delivery.free_shipping")
|
350
|
+
#
|
351
|
+
# # =>
|
352
|
+
# <input type="hidden" name="delivery[free_shipping]" value="0">
|
353
|
+
# <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="1">
|
354
|
+
#
|
355
|
+
# @example HTML Attributes
|
356
|
+
# f.check_box("delivery.free_shipping", class: "form-check-input")
|
357
|
+
#
|
358
|
+
# =>
|
359
|
+
# <input type="hidden" name="delivery[free_shipping]" value="0">
|
360
|
+
# <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="1" class="form-check-input">
|
361
|
+
#
|
362
|
+
# @example Specifying checked and unchecked values
|
363
|
+
# f.check_box("delivery.free_shipping", checked_value: "true", unchecked_value: "false")
|
364
|
+
#
|
365
|
+
# =>
|
366
|
+
# <input type="hidden" name="delivery[free_shipping]" value="false">
|
367
|
+
# <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="true">
|
368
|
+
#
|
369
|
+
# @example Automatic "checked" attribute
|
370
|
+
# # Given the request params:
|
371
|
+
# # {delivery: {free_shipping: "1"}}
|
372
|
+
# f.check_box("delivery.free_shipping")
|
373
|
+
#
|
374
|
+
# =>
|
375
|
+
# <input type="hidden" name="delivery[free_shipping]" value="0">
|
376
|
+
# <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="1" checked="checked">
|
377
|
+
#
|
378
|
+
# @example Forcing the "checked" attribute
|
379
|
+
# # Given the request params:
|
380
|
+
# # {delivery: {free_shipping: "0"}}
|
381
|
+
# f.check_box("deliver.free_shipping", checked: "checked")
|
382
|
+
#
|
383
|
+
# =>
|
384
|
+
# <input type="hidden" name="delivery[free_shipping]" value="0">
|
385
|
+
# <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="1" checked="checked">
|
386
|
+
#
|
387
|
+
# @example Multiple check boxes for an array of values
|
388
|
+
# f.check_box("book.languages", name: "book[languages][]", value: "italian", id: nil)
|
389
|
+
# f.check_box("book.languages", name: "book[languages][]", value: "english", id: nil)
|
390
|
+
#
|
391
|
+
# =>
|
392
|
+
# <input type="checkbox" name="book[languages][]" value="italian">
|
393
|
+
# <input type="checkbox" name="book[languages][]" value="english">
|
394
|
+
#
|
395
|
+
# @example Automatic "checked" attribute for an array of values
|
396
|
+
# # Given the request params:
|
397
|
+
# # {book: {languages: ["italian"]}}
|
398
|
+
# f.check_box("book.languages", name: "book[languages][]", value: "italian", id: nil)
|
399
|
+
# f.check_box("book.languages", name: "book[languages][]", value: "english", id: nil)
|
400
|
+
#
|
401
|
+
# =>
|
402
|
+
# <input type="checkbox" name="book[languages][]" value="italian" checked="checked">
|
403
|
+
# <input type="checkbox" name="book[languages][]" value="english">
|
404
|
+
#
|
405
|
+
# @api public
|
406
|
+
# @since 2.0.0
|
407
|
+
def check_box(name, **attributes)
|
408
|
+
(+"").tap { |output|
|
409
|
+
output << _hidden_field_for_check_box(name, attributes).to_s
|
410
|
+
output << input(**_attributes_for_check_box(name, attributes))
|
411
|
+
}.html_safe
|
412
|
+
end
|
413
|
+
|
414
|
+
# Returns a color input tag.
|
415
|
+
#
|
416
|
+
# @param name [String] the input name
|
417
|
+
# @param attributes [Hash] the tag's HTML attributes
|
418
|
+
#
|
419
|
+
# @return [String] the tag
|
420
|
+
#
|
421
|
+
# @example Basic usage
|
422
|
+
# f.color_field("user.background")
|
423
|
+
# => <input type="color" name="user[background]" id="user-background" value="">
|
424
|
+
#
|
425
|
+
# @example HTML Attributes
|
426
|
+
# f.color_field("user.background", class: "form-control")
|
427
|
+
# => <input type="color" name="user[background]" id="user-background" value="" class="form-control">
|
428
|
+
#
|
429
|
+
# @api public
|
430
|
+
# @since 2.0.0
|
431
|
+
def color_field(name, **attributes)
|
432
|
+
input(**_attributes(:color, name, attributes))
|
433
|
+
end
|
434
|
+
|
435
|
+
# Returns a date input tag.
|
436
|
+
#
|
437
|
+
# @param name [String] the input name
|
438
|
+
# @param attributes [Hash] the tag's HTML attributes
|
439
|
+
#
|
440
|
+
# @return [String] the tag
|
441
|
+
#
|
442
|
+
# @example Basic usage
|
443
|
+
# f.date_field("user.birth_date")
|
444
|
+
# # => <input type="date" name="user[birth_date]" id="user-birth-date" value="">
|
445
|
+
#
|
446
|
+
# @example HTML Attributes
|
447
|
+
# f.date_field("user.birth_date", class: "form-control")
|
448
|
+
# => <input type="date" name="user[birth_date]" id="user-birth-date" value="" class="form-control">
|
449
|
+
#
|
450
|
+
# @api public
|
451
|
+
# @since 2.0.0
|
452
|
+
def date_field(name, **attributes)
|
453
|
+
input(**_attributes(:date, name, attributes))
|
454
|
+
end
|
455
|
+
|
456
|
+
# Returns a datetime input tag.
|
457
|
+
#
|
458
|
+
# @param name [String] the input name
|
459
|
+
# @param attributes [Hash] the tag's HTML attributes
|
460
|
+
#
|
461
|
+
# @return [String] the tag
|
462
|
+
#
|
463
|
+
# @example Basic usage
|
464
|
+
# f.datetime_field("delivery.delivered_at")
|
465
|
+
# => <input type="datetime" name="delivery[delivered_at]" id="delivery-delivered-at" value="">
|
466
|
+
#
|
467
|
+
# @example HTML Attributes
|
468
|
+
# f.datetime_field("delivery.delivered_at", class: "form-control")
|
469
|
+
# => <input type="datetime" name="delivery[delivered_at]" id="delivery-delivered-at" value="" class="form-control">
|
470
|
+
#
|
471
|
+
# @api public
|
472
|
+
# @since 2.0.0
|
473
|
+
def datetime_field(name, **attributes)
|
474
|
+
input(**_attributes(:datetime, name, attributes))
|
475
|
+
end
|
476
|
+
|
477
|
+
# Returns a datetime-local input tag.
|
478
|
+
#
|
479
|
+
# @param name [String] the input name
|
480
|
+
# @param attributes [Hash] the tag's HTML attributes
|
481
|
+
#
|
482
|
+
# @return [String] the tag
|
483
|
+
#
|
484
|
+
# @example Basic usage
|
485
|
+
# f.datetime_local_field("delivery.delivered_at")
|
486
|
+
# => <input type="datetime-local" name="delivery[delivered_at]" id="delivery-delivered-at" value="">
|
487
|
+
#
|
488
|
+
# @example HTML Attributes
|
489
|
+
# f.datetime_local_field("delivery.delivered_at", class: "form-control")
|
490
|
+
# => <input type="datetime-local" name="delivery[delivered_at]" id="delivery-delivered-at" value="" class="form-control">
|
491
|
+
#
|
492
|
+
# @api public
|
493
|
+
# @since 2.0.0
|
494
|
+
def datetime_local_field(name, **attributes)
|
495
|
+
input(**_attributes(:"datetime-local", name, attributes))
|
496
|
+
end
|
497
|
+
|
498
|
+
# Returns a time input tag.
|
499
|
+
#
|
500
|
+
# @param name [String] the input name
|
501
|
+
# @param attributes [Hash] the tag's HTML attributes
|
502
|
+
#
|
503
|
+
# @return [String] the tag
|
504
|
+
#
|
505
|
+
# @example Basic usage
|
506
|
+
# f.time_field("book.release_hour")
|
507
|
+
# => <input type="time" name="book[release_hour]" id="book-release-hour" value="">
|
508
|
+
#
|
509
|
+
# @example HTML Attributes
|
510
|
+
# f.time_field("book.release_hour", class: "form-control")
|
511
|
+
# => <input type="time" name="book[release_hour]" id="book-release-hour" value="" class="form-control">
|
512
|
+
#
|
513
|
+
# @api public
|
514
|
+
# @since 2.0.0
|
515
|
+
def time_field(name, **attributes)
|
516
|
+
input(**_attributes(:time, name, attributes))
|
517
|
+
end
|
518
|
+
|
519
|
+
# Returns a month input tag.
|
520
|
+
#
|
521
|
+
# @param name [String] the input name
|
522
|
+
# @param attributes [Hash] the tag's HTML attributes
|
523
|
+
#
|
524
|
+
# @return [String] the tag
|
525
|
+
#
|
526
|
+
# @example Basic usage
|
527
|
+
# f.month_field("book.release_month")
|
528
|
+
# => <input type="month" name="book[release_month]" id="book-release-month" value="">
|
529
|
+
#
|
530
|
+
# @example HTML Attributes
|
531
|
+
# f.month_field("book.release_month", class: "form-control")
|
532
|
+
# => <input type="month" name="book[release_month]" id="book-release-month" value="" class="form-control">
|
533
|
+
#
|
534
|
+
# @api public
|
535
|
+
# @since 2.0.0
|
536
|
+
def month_field(name, **attributes)
|
537
|
+
input(**_attributes(:month, name, attributes))
|
538
|
+
end
|
539
|
+
|
540
|
+
# Returns a week input tag.
|
541
|
+
#
|
542
|
+
# @param name [String] the input name
|
543
|
+
# @param attributes [Hash] the tag's HTML attributes
|
544
|
+
#
|
545
|
+
# @return [String] the tag
|
546
|
+
#
|
547
|
+
# @example Basic usage
|
548
|
+
# f.week_field("book.release_week")
|
549
|
+
# => <input type="week" name="book[release_week]" id="book-release-week" value="">
|
550
|
+
#
|
551
|
+
# @example HTML Attributes
|
552
|
+
# f.week_field("book.release_week", class: "form-control")
|
553
|
+
# => <input type="week" name="book[release_week]" id="book-release-week" value="" class="form-control">
|
554
|
+
#
|
555
|
+
# @api public
|
556
|
+
# @since 2.0.0
|
557
|
+
def week_field(name, **attributes)
|
558
|
+
input(**_attributes(:week, name, attributes))
|
559
|
+
end
|
560
|
+
|
561
|
+
# Returns an email input tag.
|
562
|
+
#
|
563
|
+
# @param name [String] the input name
|
564
|
+
# @param attributes [Hash] the tag's HTML attributes
|
565
|
+
#
|
566
|
+
# @return [String] the tag
|
567
|
+
#
|
568
|
+
# @example Basic usage
|
569
|
+
# f.email_field("user.email")
|
570
|
+
# => <input type="email" name="user[email]" id="user-email" value="">
|
571
|
+
#
|
572
|
+
# @example HTML Attributes
|
573
|
+
# f.email_field("user.email", class: "form-control")
|
574
|
+
# => <input type="email" name="user[email]" id="user-email" value="" class="form-control">
|
575
|
+
#
|
576
|
+
# @api public
|
577
|
+
# @since 2.0.0
|
578
|
+
def email_field(name, **attributes)
|
579
|
+
input(**_attributes(:email, name, attributes))
|
580
|
+
end
|
581
|
+
|
582
|
+
# Returns a URL input tag.
|
583
|
+
#
|
584
|
+
# @param name [String] the input name
|
585
|
+
# @param attributes [Hash] the tag's HTML attributes
|
586
|
+
#
|
587
|
+
# @return [String] the tag
|
588
|
+
#
|
589
|
+
# @example Basic usage
|
590
|
+
# f.url_field("user.website")
|
591
|
+
# => <input type="url" name="user[website]" id="user-website" value="">
|
592
|
+
#
|
593
|
+
# @example HTML Attributes
|
594
|
+
# f.url_field("user.website", class: "form-control")
|
595
|
+
# => <input type="url" name="user[website]" id="user-website" value="" class="form-control">
|
596
|
+
#
|
597
|
+
# @api public
|
598
|
+
# @since 2.0.0
|
599
|
+
def url_field(name, **attributes)
|
600
|
+
attributes[:value] = sanitize_url(attributes.fetch(:value) { _value(name) })
|
601
|
+
|
602
|
+
input(**_attributes(:url, name, attributes))
|
603
|
+
end
|
604
|
+
|
605
|
+
# Returns a telephone input tag.
|
606
|
+
#
|
607
|
+
# @param name [String] the input name
|
608
|
+
# @param attributes [Hash] the tag's HTML attributes
|
609
|
+
#
|
610
|
+
# @return [String] the tag
|
611
|
+
#
|
612
|
+
# @example
|
613
|
+
# f.tel_field("user.telephone")
|
614
|
+
# => <input type="tel" name="user[telephone]" id="user-telephone" value="">
|
615
|
+
#
|
616
|
+
# @example HTML Attributes
|
617
|
+
# f.tel_field("user.telephone", class: "form-control")
|
618
|
+
# => <input type="tel" name="user[telephone]" id="user-telephone" value="" class="form-control">
|
619
|
+
#
|
620
|
+
# @api public
|
621
|
+
# @since 2.0.0
|
622
|
+
def tel_field(name, **attributes)
|
623
|
+
input(**_attributes(:tel, name, attributes))
|
624
|
+
end
|
625
|
+
|
626
|
+
# Returns a hidden input tag.
|
627
|
+
#
|
628
|
+
# @param name [String] the input name
|
629
|
+
# @param attributes [Hash] the tag's HTML attributes
|
630
|
+
#
|
631
|
+
# @return [String] the tag
|
632
|
+
#
|
633
|
+
# @example
|
634
|
+
# f.hidden_field("delivery.customer_id")
|
635
|
+
# => <input type="hidden" name="delivery[customer_id]" id="delivery-customer-id" value="">
|
636
|
+
#
|
637
|
+
# @api public
|
638
|
+
# @since 2.0.0
|
639
|
+
def hidden_field(name, **attributes)
|
640
|
+
input(**_attributes(:hidden, name, attributes))
|
641
|
+
end
|
642
|
+
|
643
|
+
# Returns a file input tag.
|
644
|
+
#
|
645
|
+
# @param name [String] the input name
|
646
|
+
# @param attributes [Hash] the tag's HTML attributes
|
647
|
+
# @option attributes [String, Array] :accept Optional set of accepted MIME Types
|
648
|
+
# @option attributes [Boolean] :multiple allow multiple file upload
|
649
|
+
#
|
650
|
+
# @return [String] the tag
|
651
|
+
#
|
652
|
+
# @example Basic usage
|
653
|
+
# f.file_field("user.avatar")
|
654
|
+
# => <input type="file" name="user[avatar]" id="user-avatar">
|
655
|
+
#
|
656
|
+
# @example HTML Attributes
|
657
|
+
# f.file_field("user.avatar", class: "avatar-upload")
|
658
|
+
# => <input type="file" name="user[avatar]" id="user-avatar" class="avatar-upload">
|
659
|
+
#
|
660
|
+
# @example Accepted MIME Types
|
661
|
+
# f.file_field("user.resume", accept: "application/pdf,application/ms-word")
|
662
|
+
# => <input type="file" name="user[resume]" id="user-resume" accept="application/pdf,application/ms-word">
|
663
|
+
#
|
664
|
+
# f.file_field("user.resume", accept: ["application/pdf", "application/ms-word"])
|
665
|
+
# => <input type="file" name="user[resume]" id="user-resume" accept="application/pdf,application/ms-word">
|
666
|
+
#
|
667
|
+
# @example Accept multiple file uploads
|
668
|
+
# f.file_field("user.resume", multiple: true)
|
669
|
+
# => <input type="file" name="user[resume]" id="user-resume" multiple="multiple">
|
670
|
+
#
|
671
|
+
# @api public
|
672
|
+
# @since 2.0.0
|
673
|
+
def file_field(name, **attributes)
|
674
|
+
form_attributes[:enctype] = "multipart/form-data"
|
675
|
+
|
676
|
+
attributes[:accept] = Array(attributes[:accept]).join(ACCEPT_SEPARATOR) if attributes.key?(:accept)
|
677
|
+
attributes = {type: :file, name: _input_name(name), id: _input_id(name), **attributes}
|
678
|
+
|
679
|
+
input(**attributes)
|
680
|
+
end
|
681
|
+
|
682
|
+
# Returns a number input tag.
|
683
|
+
#
|
684
|
+
# For this tag, you can make use of the `max`, `min`, and `step` HTML attributes.
|
685
|
+
#
|
686
|
+
# @param name [String] the input name
|
687
|
+
# @param attributes [Hash] the tag's HTML attributes
|
688
|
+
#
|
689
|
+
# @return [String] the tag
|
690
|
+
#
|
691
|
+
# @example Basic usage
|
692
|
+
# f.number_field("book.percent_read")
|
693
|
+
# => <input type="number" name="book[percent_read]" id="book-percent-read" value="">
|
694
|
+
#
|
695
|
+
# @example Advanced attributes
|
696
|
+
# f.number_field("book.percent_read", min: 1, max: 100, step: 1)
|
697
|
+
# => <input type="number" name="book[percent_read]" id="book-precent-read" value="" min="1" max="100" step="1">
|
698
|
+
#
|
699
|
+
# @api public
|
700
|
+
# @since 2.0.0
|
701
|
+
def number_field(name, **attributes)
|
702
|
+
input(**_attributes(:number, name, attributes))
|
703
|
+
end
|
704
|
+
|
705
|
+
# Returns a range input tag.
|
706
|
+
#
|
707
|
+
# For this tag, you can make use of the `max`, `min`, and `step` HTML attributes.
|
708
|
+
#
|
709
|
+
# @param name [String] the input name
|
710
|
+
# @param attributes [Hash] the tag's HTML attributes
|
711
|
+
#
|
712
|
+
# @return [String] the tag
|
713
|
+
#
|
714
|
+
# @example Basic usage
|
715
|
+
# f.range_field("book.discount_percentage")
|
716
|
+
# => <input type="range" name="book[discount_percentage]" id="book-discount-percentage" value="">
|
717
|
+
#
|
718
|
+
# @example Advanced attributes
|
719
|
+
# f.range_field("book.discount_percentage", min: 1, max: 1'0, step: 1)
|
720
|
+
# => <input type="number" name="book[discount_percentage]" id="book-discount-percentage" value="" min="1" max="100" step="1">
|
721
|
+
#
|
722
|
+
# @api public
|
723
|
+
# @since 2.0.0
|
724
|
+
def range_field(name, **attributes)
|
725
|
+
input(**_attributes(:range, name, attributes))
|
726
|
+
end
|
727
|
+
|
728
|
+
# Returns a textarea tag.
|
729
|
+
#
|
730
|
+
# @param name [String] the input name
|
731
|
+
# @param content [String] the content of the textarea
|
732
|
+
# @param attributes [Hash] the tag's HTML attributes
|
733
|
+
#
|
734
|
+
# @return [String] the tag
|
735
|
+
#
|
736
|
+
# @example Basic usage
|
737
|
+
# f.text_area("user.hobby")
|
738
|
+
# => <textarea name="user[hobby]" id="user-hobby"></textarea>
|
739
|
+
#
|
740
|
+
# f.text_area "user.hobby", "Football"
|
741
|
+
# =>
|
742
|
+
# <textarea name="user[hobby]" id="user-hobby">
|
743
|
+
# Football</textarea>
|
744
|
+
#
|
745
|
+
# @example HTML attributes
|
746
|
+
# f.text_area "user.hobby", class: "form-control"
|
747
|
+
# => <textarea name="user[hobby]" id="user-hobby" class="form-control"></textarea>
|
748
|
+
#
|
749
|
+
# @api public
|
750
|
+
# @since 2.0.0
|
751
|
+
def text_area(name, content = nil, **attributes)
|
752
|
+
if content.respond_to?(:to_hash)
|
753
|
+
attributes = content
|
754
|
+
content = nil
|
755
|
+
end
|
756
|
+
|
757
|
+
attributes = {name: _input_name(name), id: _input_id(name), **attributes}
|
758
|
+
tag.textarea(content || _value(name), **attributes)
|
759
|
+
end
|
760
|
+
|
761
|
+
# Returns a text input tag.
|
762
|
+
#
|
763
|
+
# @param name [String] the input name
|
764
|
+
# @param attributes [Hash] the tag's HTML attributes
|
765
|
+
#
|
766
|
+
# @return [String] the tag
|
767
|
+
#
|
768
|
+
# @example Basic usage
|
769
|
+
# f.text_field("user.first_name")
|
770
|
+
# => <input type="text" name="user[first_name]" id="user-first-name" value="">
|
771
|
+
#
|
772
|
+
# @example HTML Attributes
|
773
|
+
# f.text_field("user.first_name", class: "form-control")
|
774
|
+
# => <input type="text" name="user[first_name]" id="user-first-name" value="" class="form-control">
|
775
|
+
#
|
776
|
+
# @api public
|
777
|
+
# @since 2.0.0
|
778
|
+
def text_field(name, **attributes)
|
779
|
+
input(**_attributes(:text, name, attributes))
|
780
|
+
end
|
781
|
+
alias_method :input_text, :text_field
|
782
|
+
|
783
|
+
# Returns a search input tag.
|
784
|
+
#
|
785
|
+
# @param name [String] the input name
|
786
|
+
# @param attributes [Hash] the tag's HTML attributes
|
787
|
+
#
|
788
|
+
# @return [String] the tag
|
789
|
+
#
|
790
|
+
# @example Basic usage
|
791
|
+
# f.search_field("search.q")
|
792
|
+
# => <input type="search" name="search[q]" id="search-q" value="">
|
793
|
+
#
|
794
|
+
# @example HTML Attributes
|
795
|
+
# f.search_field("search.q", class: "form-control")
|
796
|
+
# => <input type="search" name="search[q]" id="search-q" value="" class="form-control">
|
797
|
+
#
|
798
|
+
# @api public
|
799
|
+
# @since 2.0.0
|
800
|
+
def search_field(name, **attributes)
|
801
|
+
input(**_attributes(:search, name, attributes))
|
802
|
+
end
|
803
|
+
|
804
|
+
# Returns a radio input tag.
|
805
|
+
#
|
806
|
+
# When editing a resource, the form automatically assigns the `checked` HTML attribute for
|
807
|
+
# the tag.
|
808
|
+
#
|
809
|
+
# @param name [String] the input name
|
810
|
+
# @param value [String] the input value
|
811
|
+
# @param attributes [Hash] the tag's HTML attributes
|
812
|
+
#
|
813
|
+
# @return [String] the tag
|
814
|
+
#
|
815
|
+
# @example Basic usage
|
816
|
+
# f.radio_button("book.category", "Fiction")
|
817
|
+
# f.radio_button("book.category", "Non-Fiction")
|
818
|
+
#
|
819
|
+
# =>
|
820
|
+
# <input type="radio" name="book[category]" value="Fiction">
|
821
|
+
# <input type="radio" name="book[category]" value="Non-Fiction">
|
822
|
+
#
|
823
|
+
# @example HTML Attributes
|
824
|
+
# f.radio_button("book.category", "Fiction", class: "form-check")
|
825
|
+
# f.radio_button("book.category", "Non-Fiction", class: "form-check")
|
826
|
+
#
|
827
|
+
# =>
|
828
|
+
# <input type="radio" name="book[category]" value="Fiction" class="form-check">
|
829
|
+
# <input type="radio" name="book[category]" value="Non-Fiction" class="form-check">
|
830
|
+
#
|
831
|
+
# @example Automatic checked value
|
832
|
+
# # Given the request params:
|
833
|
+
# # {book: {category: "Non-Fiction"}}
|
834
|
+
# f.radio_button("book.category", "Fiction")
|
835
|
+
# f.radio_button("book.category", "Non-Fiction")
|
836
|
+
#
|
837
|
+
# =>
|
838
|
+
# <input type="radio" name="book[category]" value="Fiction">
|
839
|
+
# <input type="radio" name="book[category]" value="Non-Fiction" checked="checked">
|
840
|
+
#
|
841
|
+
# @api public
|
842
|
+
# @since 2.0.0
|
843
|
+
def radio_button(name, value, **attributes)
|
844
|
+
attributes = {type: :radio, name: _input_name(name), value: value, **attributes}
|
845
|
+
attributes[:checked] = true if _value(name).to_s == value.to_s
|
846
|
+
|
847
|
+
input(**attributes)
|
848
|
+
end
|
849
|
+
|
850
|
+
# Returns a password input tag.
|
851
|
+
#
|
852
|
+
# @param name [String] the input name
|
853
|
+
# @param attributes [Hash] the tag's HTML attributes
|
854
|
+
#
|
855
|
+
# @return [String] the tag
|
856
|
+
#
|
857
|
+
# @example Basic usage
|
858
|
+
# f.password_field("signup.password")
|
859
|
+
# => <input type="password" name="signup[password]" id="signup-password" value="">
|
860
|
+
#
|
861
|
+
# @api public
|
862
|
+
# @since 2.0.0
|
863
|
+
def password_field(name, **attributes)
|
864
|
+
attrs = {type: :password, name: _input_name(name), id: _input_id(name), value: nil, **attributes}
|
865
|
+
attrs[:value] = EMPTY_STRING if attrs[:value].nil?
|
866
|
+
|
867
|
+
input(**attrs)
|
868
|
+
end
|
869
|
+
|
870
|
+
# Returns a select input tag containing option tags for the given values.
|
871
|
+
#
|
872
|
+
# The values should be an enumerable of pairs of content (the displayed text for the option)
|
873
|
+
# and value (the value for the option) strings.
|
874
|
+
#
|
875
|
+
# When editing a resource, automatically assigns the `selected` HTML attribute for any
|
876
|
+
# option tags matching the resource's values.
|
877
|
+
#
|
878
|
+
# @param name [String] the input name
|
879
|
+
# @param values [Hash] a Hash to generate `<option>` tags.
|
880
|
+
# @param attributes [Hash] the tag's HTML attributes
|
881
|
+
#
|
882
|
+
# @return [String] the tag
|
883
|
+
#
|
884
|
+
# @example Basic usage
|
885
|
+
# values = {"Italy" => "it", "Australia" => "au"}
|
886
|
+
# f.select("book.store", values)
|
887
|
+
#
|
888
|
+
# =>
|
889
|
+
# <select name="book[store]" id="book-store">
|
890
|
+
# <option value="it">Italy</option>
|
891
|
+
# <option value="au">Australia</option>
|
892
|
+
# </select>
|
893
|
+
#
|
894
|
+
# @example HTML Attributes
|
895
|
+
# values = {"Italy" => "it", "Australia" => "au"}
|
896
|
+
# f.select("book.store", values, class: "form-control")
|
897
|
+
#
|
898
|
+
# =>
|
899
|
+
# <select name="book[store]" id="book-store" class="form-control">
|
900
|
+
# <option value="it">Italy</option>
|
901
|
+
# <option value="au">Australia</option>
|
902
|
+
# </select>
|
903
|
+
#
|
904
|
+
# @example Selected options
|
905
|
+
# # Given the following request params:
|
906
|
+
# # {book: {store: "it"}}
|
907
|
+
# values = {"Italy" => "it", "Australia" => "au"}
|
908
|
+
# f.select("book.store", values)
|
909
|
+
#
|
910
|
+
# =>
|
911
|
+
# <select name="book[store]" id="book-store">
|
912
|
+
# <option value="it" selected="selected">Italy</option>
|
913
|
+
# <option value="au">Australia</option>
|
914
|
+
# </select>
|
915
|
+
#
|
916
|
+
# @example Prompt option
|
917
|
+
# values = {"Italy" => "it", "Australia" => "au"}
|
918
|
+
# f.select("book.store", values, options: {prompt: "Select a store"})
|
919
|
+
#
|
920
|
+
# =>
|
921
|
+
# <select name="book[store]" id="book-store">
|
922
|
+
# <option>Select a store</option>
|
923
|
+
# <option value="it">Italy</option>
|
924
|
+
# <option value="au">Australia</option>
|
925
|
+
# </select>
|
926
|
+
#
|
927
|
+
# @example Selected option
|
928
|
+
# values = {"Italy" => "it", "Australia" => "au"}
|
929
|
+
# f.select("book.store", values, options: {selected: "it"})
|
930
|
+
#
|
931
|
+
# =>
|
932
|
+
# <select name="book[store]" id="book-store">
|
933
|
+
# <option value="it" selected="selected">Italy</option>
|
934
|
+
# <option value="au">Australia</option>
|
935
|
+
# </select>
|
936
|
+
#
|
937
|
+
# @example Prompt option and HTML attributes
|
938
|
+
# values = {"Italy" => "it", "Australia" => "au"}
|
939
|
+
# f.select("book.store", values, options: {prompt: "Select a store"}, class: "form-control")
|
940
|
+
#
|
941
|
+
# =>
|
942
|
+
# <select name="book[store]" id="book-store" class="form-control">
|
943
|
+
# <option disabled="disabled">Select a store</option>
|
944
|
+
# <option value="it">Italy</option>
|
945
|
+
# <option value="au">Australia</option>
|
946
|
+
# </select>
|
947
|
+
#
|
948
|
+
# @example Multiple select
|
949
|
+
# values = {"Italy" => "it", "Australia" => "au"}
|
950
|
+
# f.select("book.stores", values, multiple: true)
|
951
|
+
#
|
952
|
+
# =>
|
953
|
+
# <select name="book[store][]" id="book-store" multiple="multiple">
|
954
|
+
# <option value="it">Italy</option>
|
955
|
+
# <option value="au">Australia</option>
|
956
|
+
# </select>
|
957
|
+
#
|
958
|
+
# @example Multiple select and HTML attributes
|
959
|
+
# values = {"Italy" => "it", "Australia" => "au"}
|
960
|
+
# f.select("book.stores", values, multiple: true, class: "form-control")
|
961
|
+
#
|
962
|
+
# =>
|
963
|
+
# <select name="book[store][]" id="book-store" multiple="multiple" class="form-control">
|
964
|
+
# <option value="it">Italy</option>
|
965
|
+
# <option value="au">Australia</option>
|
966
|
+
# </select>
|
967
|
+
#
|
968
|
+
# @example Values as an array, supporting repeated entries
|
969
|
+
# values = [["Italy", "it"],
|
970
|
+
# ["---", ""],
|
971
|
+
# ["Afghanistan", "af"],
|
972
|
+
# ...
|
973
|
+
# ["Italy", "it"],
|
974
|
+
# ...
|
975
|
+
# ["Zimbabwe", "zw"]]
|
976
|
+
# f.select("book.stores", values)
|
977
|
+
#
|
978
|
+
# =>
|
979
|
+
# <select name="book[store]" id="book-store">
|
980
|
+
# <option value="it">Italy</option>
|
981
|
+
# <option value="">---</option>
|
982
|
+
# <option value="af">Afghanistan</option>
|
983
|
+
# ...
|
984
|
+
# <option value="it">Italy</option>
|
985
|
+
# ...
|
986
|
+
# <option value="zw">Zimbabwe</option>
|
987
|
+
# </select>
|
988
|
+
#
|
989
|
+
# @api public
|
990
|
+
# @since 2.0.0
|
991
|
+
def select(name, values, **attributes) # rubocop:disable Metrics/AbcSize
|
992
|
+
options = attributes.delete(:options) { {} }
|
993
|
+
multiple = attributes[:multiple]
|
994
|
+
attributes = {name: _select_input_name(name, multiple), id: _input_id(name), **attributes}
|
995
|
+
prompt = options.delete(:prompt)
|
996
|
+
selected = options.delete(:selected)
|
997
|
+
input_value = _value(name)
|
998
|
+
|
999
|
+
option_tags = []
|
1000
|
+
option_tags << tag.option(prompt, disabled: true) if prompt
|
1001
|
+
|
1002
|
+
already_selected = nil
|
1003
|
+
values.each do |content, value|
|
1004
|
+
if (multiple || !already_selected) &&
|
1005
|
+
(already_selected = _select_option_selected?(value, selected, input_value, multiple))
|
1006
|
+
option_tags << tag.option(content, value: value, selected: true, **options)
|
1007
|
+
else
|
1008
|
+
option_tags << tag.option(content, value: value, **options)
|
1009
|
+
end
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
tag.select(option_tags.join.html_safe, **attributes)
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
# Returns a datalist input tag.
|
1016
|
+
#
|
1017
|
+
# @param name [String] the input name
|
1018
|
+
# @param values [Array,Hash] a collection that is transformed into `<option>` tags
|
1019
|
+
# @param list [String] the name of list for the text input; also the id of datalist
|
1020
|
+
# @param attributes [Hash] the tag's HTML attributes
|
1021
|
+
#
|
1022
|
+
# @return [String] the tag
|
1023
|
+
#
|
1024
|
+
# @example Basic Usage
|
1025
|
+
# values = ["Italy", "Australia"]
|
1026
|
+
# f.datalist("book.stores", values, "books")
|
1027
|
+
#
|
1028
|
+
# =>
|
1029
|
+
# <input type="text" name="book[store]" id="book-store" value="" list="books">
|
1030
|
+
# <datalist id="books">
|
1031
|
+
# <option value="Italy"></option>
|
1032
|
+
# <option value="Australia"></option>
|
1033
|
+
# </datalist>
|
1034
|
+
#
|
1035
|
+
# @example Options As Hash
|
1036
|
+
# values = Hash["Italy" => "it", "Australia" => "au"]
|
1037
|
+
# f.datalist("book.stores", values, "books")
|
1038
|
+
#
|
1039
|
+
# =>
|
1040
|
+
# <input type="text" name="book[store]" id="book-store" value="" list="books">
|
1041
|
+
# <datalist id="books">
|
1042
|
+
# <option value="Italy">it</option>
|
1043
|
+
# <option value="Australia">au</option>
|
1044
|
+
# </datalist>
|
1045
|
+
#
|
1046
|
+
# @example Specifying custom attributes for the datalist input
|
1047
|
+
# values = ["Italy", "Australia"]
|
1048
|
+
# f.datalist "book.stores", values, "books", datalist: {class: "form-control"}
|
1049
|
+
#
|
1050
|
+
# =>
|
1051
|
+
# <input type="text" name="book[store]" id="book-store" value="" list="books">
|
1052
|
+
# <datalist id="books" class="form-control">
|
1053
|
+
# <option value="Italy"></option>
|
1054
|
+
# <option value="Australia"></option>
|
1055
|
+
# </datalist>
|
1056
|
+
#
|
1057
|
+
# @example Specifying custom attributes for the options list
|
1058
|
+
# values = ["Italy", "Australia"]
|
1059
|
+
# f.datalist("book.stores", values, "books", options: {class: "form-control"})
|
1060
|
+
#
|
1061
|
+
# =>
|
1062
|
+
# <input type="text" name="book[store]" id="book-store" value="" list="books">
|
1063
|
+
# <datalist id="books">
|
1064
|
+
# <option value="Italy" class="form-control"></option>
|
1065
|
+
# <option value="Australia" class="form-control"></option>
|
1066
|
+
# </datalist>
|
1067
|
+
#
|
1068
|
+
# @api public
|
1069
|
+
# @since 2.0.0
|
1070
|
+
def datalist(name, values, list, **attributes)
|
1071
|
+
options = attributes.delete(:options) || {}
|
1072
|
+
datalist = attributes.delete(:datalist) || {}
|
1073
|
+
|
1074
|
+
attributes[:list] = list
|
1075
|
+
datalist[:id] = list
|
1076
|
+
|
1077
|
+
(+"").tap { |output|
|
1078
|
+
output << text_field(name, **attributes)
|
1079
|
+
output << tag.datalist(**datalist) {
|
1080
|
+
(+"").tap { |inner|
|
1081
|
+
values.each do |value, content|
|
1082
|
+
inner << tag.option(content, value: value, **options)
|
1083
|
+
end
|
1084
|
+
}.html_safe
|
1085
|
+
}
|
1086
|
+
}.html_safe
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
# Returns a button tag.
|
1090
|
+
#
|
1091
|
+
# @return [String] the tag
|
1092
|
+
#
|
1093
|
+
# @overload button(content, **attributes)
|
1094
|
+
# Returns a button tag with the given content.
|
1095
|
+
#
|
1096
|
+
# @param content [String] the content for the tag
|
1097
|
+
# @param attributes [Hash] the tag's HTML attributes
|
1098
|
+
#
|
1099
|
+
# @overload button(**attributes, &block)
|
1100
|
+
# Returns a button tag with the return value of the given block as the tag's content.
|
1101
|
+
#
|
1102
|
+
# @param attributes [Hash] the tag's HTML attributes
|
1103
|
+
# @yieldreturn [String] the tag content
|
1104
|
+
#
|
1105
|
+
# @example Basic usage
|
1106
|
+
# f.button("Click me")
|
1107
|
+
# => <button>Click me</button>
|
1108
|
+
#
|
1109
|
+
# @example HTML Attributes
|
1110
|
+
# f.button("Click me", class: "btn btn-secondary")
|
1111
|
+
# => <button class="btn btn-secondary">Click me</button>
|
1112
|
+
#
|
1113
|
+
# @example Returning content from a block
|
1114
|
+
# <%= f.button class: "btn btn-secondary" do %>
|
1115
|
+
# <span class="oi oi-check">
|
1116
|
+
# <% end %>
|
1117
|
+
#
|
1118
|
+
# =>
|
1119
|
+
# <button class="btn btn-secondary">
|
1120
|
+
# <span class="oi oi-check"></span>
|
1121
|
+
# </button>
|
1122
|
+
#
|
1123
|
+
# @api public
|
1124
|
+
# @since 2.0.0
|
1125
|
+
def button(...)
|
1126
|
+
tag.button(...)
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
# Returns an image input tag, to be used as a visual button for the form.
|
1130
|
+
#
|
1131
|
+
# For security reasons, you should use the absolute URL of the given image.
|
1132
|
+
#
|
1133
|
+
# @param source [String] The absolute URL of the image
|
1134
|
+
# @param attributes [Hash] the tag's HTML attributes
|
1135
|
+
#
|
1136
|
+
# @return [String] the tag
|
1137
|
+
#
|
1138
|
+
# @example Basic usage
|
1139
|
+
# f.image_button("https://hanamirb.org/assets/button.png")
|
1140
|
+
# => <input type="image" src="https://hanamirb.org/assets/button.png">
|
1141
|
+
#
|
1142
|
+
# @example HTML Attributes
|
1143
|
+
# f.image_button("https://hanamirb.org/assets/button.png", name: "image", width: "50")
|
1144
|
+
# => <input name="image" width="50" type="image" src="https://hanamirb.org/assets/button.png">
|
1145
|
+
#
|
1146
|
+
# @api public
|
1147
|
+
# @since 2.0.0
|
1148
|
+
def image_button(source, **attributes)
|
1149
|
+
attributes[:type] = :image
|
1150
|
+
attributes[:src] = sanitize_url(source)
|
1151
|
+
|
1152
|
+
input(**attributes)
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
# Returns a submit button tag.
|
1156
|
+
#
|
1157
|
+
# @return [String] the tag
|
1158
|
+
#
|
1159
|
+
# @overload submit(content, **attributes)
|
1160
|
+
# Returns a submit button tag with the given content.
|
1161
|
+
#
|
1162
|
+
# @param content [String] the content for the tag
|
1163
|
+
# @param attributes [Hash] the tag's HTML attributes
|
1164
|
+
#
|
1165
|
+
# @overload submit(**attributes, &blk)
|
1166
|
+
# Returns a submit button tag with the return value of the given block as the tag's
|
1167
|
+
# content.
|
1168
|
+
#
|
1169
|
+
# @param attributes [Hash] the tag's HTML attributes
|
1170
|
+
# @yieldreturn [String] the tag content
|
1171
|
+
#
|
1172
|
+
# @example Basic usage
|
1173
|
+
# f.submit("Create")
|
1174
|
+
# => <button type="submit">Create</button>
|
1175
|
+
#
|
1176
|
+
# @example HTML Attributes
|
1177
|
+
# f.submit("Create", class: "btn btn-primary")
|
1178
|
+
# => <button type="submit" class="btn btn-primary">Create</button>
|
1179
|
+
#
|
1180
|
+
# @example Returning content from a block
|
1181
|
+
# <%= f.submit(class: "btn btn-primary") do %>
|
1182
|
+
# <span class="oi oi-check">
|
1183
|
+
# <% end %>
|
1184
|
+
#
|
1185
|
+
# =>
|
1186
|
+
# <button type="submit" class="btn btn-primary">
|
1187
|
+
# <span class="oi oi-check"></span>
|
1188
|
+
# </button>
|
1189
|
+
#
|
1190
|
+
# @api public
|
1191
|
+
# @since 2.0.0
|
1192
|
+
def submit(content = nil, **attributes, &blk)
|
1193
|
+
if content.is_a?(::Hash)
|
1194
|
+
attributes = content
|
1195
|
+
content = nil
|
1196
|
+
end
|
1197
|
+
|
1198
|
+
attributes = {type: :submit, **attributes}
|
1199
|
+
tag.button(content, **attributes, &blk)
|
1200
|
+
end
|
1201
|
+
|
1202
|
+
# Returns an input tag.
|
1203
|
+
#
|
1204
|
+
# Generates an input tag without any special handling. For more convenience and other
|
1205
|
+
# advanced features, see the other methods of the form builder.
|
1206
|
+
#
|
1207
|
+
# @param attributes [Hash] the tag's HTML attributes
|
1208
|
+
# @yieldreturn [String] the tag content
|
1209
|
+
#
|
1210
|
+
# @return [String] the tag
|
1211
|
+
#
|
1212
|
+
# @since 2.0.0
|
1213
|
+
# @api public
|
1214
|
+
#
|
1215
|
+
# @example Basic usage
|
1216
|
+
# f.input(type: :text, name: "book[title]", id: "book-title", value: book.title)
|
1217
|
+
# => <input type="text" name="book[title]" id="book-title" value="Hanami book">
|
1218
|
+
#
|
1219
|
+
# @api public
|
1220
|
+
# @since 2.0.0
|
1221
|
+
def input(...)
|
1222
|
+
tag.input(...)
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
private
|
1226
|
+
|
1227
|
+
# @api private
|
1228
|
+
# @since 2.0.0
|
1229
|
+
def _form_method(attributes)
|
1230
|
+
attributes[:method] ||= DEFAULT_METHOD
|
1231
|
+
attributes[:method] = attributes[:method].to_s.upcase
|
1232
|
+
|
1233
|
+
original_form_method = attributes[:method]
|
1234
|
+
|
1235
|
+
if (method_override = !BROWSER_METHODS.include?(attributes[:method]))
|
1236
|
+
attributes[:method] = DEFAULT_METHOD
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
[method_override, original_form_method]
|
1240
|
+
end
|
1241
|
+
|
1242
|
+
# @api private
|
1243
|
+
# @since 2.0.0
|
1244
|
+
def _csrf_token(values, attributes)
|
1245
|
+
return [] if values.csrf_token.nil?
|
1246
|
+
|
1247
|
+
return [] if EXCLUDED_CSRF_METHODS.include?(attributes[:method])
|
1248
|
+
|
1249
|
+
[true, values.csrf_token]
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
# @api private
|
1253
|
+
# @since 2.0.0
|
1254
|
+
def _attributes(type, name, attributes)
|
1255
|
+
attrs = {
|
1256
|
+
type: type,
|
1257
|
+
name: _input_name(name),
|
1258
|
+
id: _input_id(name),
|
1259
|
+
value: _value(name)
|
1260
|
+
}
|
1261
|
+
attrs.merge!(attributes)
|
1262
|
+
attrs[:value] = escape_html(attrs[:value]).html_safe
|
1263
|
+
attrs
|
1264
|
+
end
|
1265
|
+
|
1266
|
+
# @api private
|
1267
|
+
# @since 2.0.0
|
1268
|
+
def _input_name(name)
|
1269
|
+
tokens = _split_input_name(name)
|
1270
|
+
result = tokens.shift
|
1271
|
+
|
1272
|
+
tokens.each do |t|
|
1273
|
+
if t =~ %r{\A\d+\z}
|
1274
|
+
result << "[]"
|
1275
|
+
else
|
1276
|
+
result << "[#{t}]"
|
1277
|
+
end
|
1278
|
+
end
|
1279
|
+
|
1280
|
+
result
|
1281
|
+
end
|
1282
|
+
|
1283
|
+
# @api private
|
1284
|
+
# @since 2.0.0
|
1285
|
+
def _input_id(name)
|
1286
|
+
[base_name, name].compact.join(INPUT_NAME_SEPARATOR).to_s.tr("._", "-")
|
1287
|
+
end
|
1288
|
+
|
1289
|
+
# @api private
|
1290
|
+
# @since 2.0.0
|
1291
|
+
def _value(name)
|
1292
|
+
values.get(*_split_input_name(name).map(&:to_sym))
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
# @api private
|
1296
|
+
# @since 2.0.0
|
1297
|
+
def _split_input_name(name)
|
1298
|
+
[
|
1299
|
+
*base_name.to_s.split(INPUT_NAME_SEPARATOR),
|
1300
|
+
*name.to_s.split(INPUT_NAME_SEPARATOR)
|
1301
|
+
].compact
|
1302
|
+
end
|
1303
|
+
|
1304
|
+
# @api private
|
1305
|
+
# @since 2.0.0
|
1306
|
+
#
|
1307
|
+
# @see #check_box
|
1308
|
+
def _hidden_field_for_check_box(name, attributes)
|
1309
|
+
return unless attributes[:value].nil? || !attributes[:unchecked_value].nil?
|
1310
|
+
|
1311
|
+
input(
|
1312
|
+
type: :hidden,
|
1313
|
+
name: attributes[:name] || _input_name(name),
|
1314
|
+
value: (attributes.delete(:unchecked_value) || DEFAULT_UNCHECKED_VALUE).to_s
|
1315
|
+
)
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
# @api private
|
1319
|
+
# @since 2.0.0
|
1320
|
+
#
|
1321
|
+
# @see #check_box
|
1322
|
+
def _attributes_for_check_box(name, attributes)
|
1323
|
+
attributes = {
|
1324
|
+
type: :checkbox,
|
1325
|
+
name: _input_name(name),
|
1326
|
+
id: _input_id(name),
|
1327
|
+
value: (attributes.delete(:checked_value) || DEFAULT_CHECKED_VALUE).to_s,
|
1328
|
+
**attributes
|
1329
|
+
}
|
1330
|
+
|
1331
|
+
attributes[:checked] = true if _check_box_checked?(attributes[:value], _value(name))
|
1332
|
+
|
1333
|
+
attributes
|
1334
|
+
end
|
1335
|
+
|
1336
|
+
# @api private
|
1337
|
+
# @since 1.2.0
|
1338
|
+
def _select_input_name(name, multiple)
|
1339
|
+
select_name = _input_name(name)
|
1340
|
+
select_name = "#{select_name}[]" if multiple
|
1341
|
+
select_name
|
1342
|
+
end
|
1343
|
+
|
1344
|
+
# @api private
|
1345
|
+
# @since 1.2.0
|
1346
|
+
def _select_option_selected?(value, selected, input_value, multiple)
|
1347
|
+
if input_value && selected.nil?
|
1348
|
+
value.to_s == input_value.to_s
|
1349
|
+
else
|
1350
|
+
(value == selected) ||
|
1351
|
+
_is_current_value?(input_value, value) ||
|
1352
|
+
_is_in_selected_values?(multiple, selected, value) ||
|
1353
|
+
_is_in_input_values?(multiple, input_value, value)
|
1354
|
+
end
|
1355
|
+
end
|
1356
|
+
|
1357
|
+
# @api private
|
1358
|
+
# @since 1.2.0
|
1359
|
+
def _is_current_value?(input_value, value)
|
1360
|
+
return unless input_value
|
1361
|
+
|
1362
|
+
value.to_s == input_value.to_s
|
1363
|
+
end
|
1364
|
+
|
1365
|
+
# @api private
|
1366
|
+
# @since 1.2.0
|
1367
|
+
def _is_in_selected_values?(multiple, selected, value)
|
1368
|
+
return unless multiple && selected.is_a?(Array)
|
1369
|
+
|
1370
|
+
selected.include?(value)
|
1371
|
+
end
|
1372
|
+
|
1373
|
+
# @api private
|
1374
|
+
# @since 1.2.0
|
1375
|
+
def _is_in_input_values?(multiple, input_value, value)
|
1376
|
+
return unless multiple && input_value.is_a?(Array)
|
1377
|
+
|
1378
|
+
input_value.include?(value)
|
1379
|
+
end
|
1380
|
+
|
1381
|
+
# @api private
|
1382
|
+
# @since 1.2.0
|
1383
|
+
def _check_box_checked?(value, input_value)
|
1384
|
+
!input_value.nil? &&
|
1385
|
+
(input_value.to_s == value.to_s || input_value.is_a?(TrueClass) ||
|
1386
|
+
(input_value.is_a?(Array) && input_value.include?(value)))
|
1387
|
+
end
|
1388
|
+
end
|
1389
|
+
end
|
1390
|
+
end
|
1391
|
+
end
|