hanami 2.0.2 → 2.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/LICENSE.md +1 -1
- data/README.md +25 -9
- data/hanami.gemspec +2 -2
- data/lib/hanami/config/actions.rb +0 -4
- data/lib/hanami/config/logger.rb +1 -1
- data/lib/hanami/config/views.rb +0 -4
- data/lib/hanami/config.rb +54 -0
- data/lib/hanami/extensions/action/slice_configured_action.rb +15 -7
- data/lib/hanami/extensions/action.rb +4 -4
- 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 +14 -0
- data/lib/hanami/extensions.rb +10 -3
- 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/public_errors_app.rb +75 -0
- data/lib/hanami/middleware/render_errors.rb +93 -0
- data/lib/hanami/slice.rb +28 -2
- data/lib/hanami/slice_configurable.rb +3 -2
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami/web/rack_logger.rb +8 -20
- data/lib/hanami.rb +1 -1
- 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/rack_app/middleware_spec.rb +23 -23
- data/spec/integration/rack_app/rack_app_spec.rb +5 -1
- data/spec/integration/slices/slice_registrations_spec.rb +80 -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/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 +90 -0
- data/spec/integration/web/render_errors_spec.rb +240 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/matchers.rb +32 -0
- data/spec/unit/hanami/config/actions/default_values_spec.rb +0 -4
- data/spec/unit/hanami/config/logger_spec.rb +9 -0
- 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/extensions/view/context_spec.rb +59 -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 +67 -33
- 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
|