hanami 2.0.2 → 2.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +25 -9
  5. data/hanami.gemspec +2 -2
  6. data/lib/hanami/config/actions.rb +0 -4
  7. data/lib/hanami/config/logger.rb +1 -1
  8. data/lib/hanami/config/views.rb +0 -4
  9. data/lib/hanami/config.rb +54 -0
  10. data/lib/hanami/extensions/action/slice_configured_action.rb +15 -7
  11. data/lib/hanami/extensions/action.rb +4 -4
  12. data/lib/hanami/extensions/router/errors.rb +58 -0
  13. data/lib/hanami/extensions/view/context.rb +129 -60
  14. data/lib/hanami/extensions/view/part.rb +26 -0
  15. data/lib/hanami/extensions/view/scope.rb +26 -0
  16. data/lib/hanami/extensions/view/slice_configured_context.rb +0 -2
  17. data/lib/hanami/extensions/view/slice_configured_helpers.rb +44 -0
  18. data/lib/hanami/extensions/view/slice_configured_view.rb +106 -21
  19. data/lib/hanami/extensions/view/standard_helpers.rb +14 -0
  20. data/lib/hanami/extensions.rb +10 -3
  21. data/lib/hanami/helpers/form_helper/form_builder.rb +1391 -0
  22. data/lib/hanami/helpers/form_helper/values.rb +75 -0
  23. data/lib/hanami/helpers/form_helper.rb +213 -0
  24. data/lib/hanami/middleware/public_errors_app.rb +75 -0
  25. data/lib/hanami/middleware/render_errors.rb +93 -0
  26. data/lib/hanami/slice.rb +28 -2
  27. data/lib/hanami/slice_configurable.rb +3 -2
  28. data/lib/hanami/version.rb +1 -1
  29. data/lib/hanami/web/rack_logger.rb +8 -20
  30. data/lib/hanami.rb +1 -1
  31. data/spec/integration/action/view_rendering/view_context_spec.rb +221 -0
  32. data/spec/integration/action/view_rendering_spec.rb +0 -18
  33. data/spec/integration/rack_app/middleware_spec.rb +23 -23
  34. data/spec/integration/rack_app/rack_app_spec.rb +5 -1
  35. data/spec/integration/slices/slice_registrations_spec.rb +80 -0
  36. data/spec/integration/view/config/default_context_spec.rb +149 -0
  37. data/spec/integration/view/{inflector_spec.rb → config/inflector_spec.rb} +1 -1
  38. data/spec/integration/view/config/part_class_spec.rb +147 -0
  39. data/spec/integration/view/config/part_namespace_spec.rb +103 -0
  40. data/spec/integration/view/config/paths_spec.rb +119 -0
  41. data/spec/integration/view/config/scope_class_spec.rb +147 -0
  42. data/spec/integration/view/config/scope_namespace_spec.rb +103 -0
  43. data/spec/integration/view/config/template_spec.rb +38 -0
  44. data/spec/integration/view/context/request_spec.rb +3 -7
  45. data/spec/integration/view/helpers/form_helper_spec.rb +174 -0
  46. data/spec/integration/view/helpers/part_helpers_spec.rb +124 -0
  47. data/spec/integration/view/helpers/scope_helpers_spec.rb +84 -0
  48. data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +162 -0
  49. data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +119 -0
  50. data/spec/integration/view/slice_configuration_spec.rb +9 -9
  51. data/spec/integration/web/render_detailed_errors_spec.rb +90 -0
  52. data/spec/integration/web/render_errors_spec.rb +240 -0
  53. data/spec/spec_helper.rb +1 -1
  54. data/spec/support/matchers.rb +32 -0
  55. data/spec/unit/hanami/config/actions/default_values_spec.rb +0 -4
  56. data/spec/unit/hanami/config/logger_spec.rb +9 -0
  57. data/spec/unit/hanami/config/render_detailed_errors_spec.rb +25 -0
  58. data/spec/unit/hanami/config/render_errors_spec.rb +25 -0
  59. data/spec/unit/hanami/config/views_spec.rb +0 -18
  60. data/spec/unit/hanami/extensions/view/context_spec.rb +59 -0
  61. data/spec/unit/hanami/helpers/form_helper_spec.rb +2826 -0
  62. data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +27 -0
  63. data/spec/unit/hanami/router/errors/not_found_error_spec.rb +22 -0
  64. data/spec/unit/hanami/slice_configurable_spec.rb +18 -0
  65. data/spec/unit/hanami/version_spec.rb +1 -1
  66. data/spec/unit/hanami/web/rack_logger_spec.rb +1 -1
  67. metadata +67 -33
  68. data/spec/integration/action/view_integration_spec.rb +0 -165
  69. data/spec/integration/view/part_namespace_spec.rb +0 -96
  70. data/spec/integration/view/path_spec.rb +0 -56
  71. data/spec/integration/view/template_spec.rb +0 -68
  72. data/spec/isolation/hanami/application/already_configured_spec.rb +0 -19
  73. data/spec/isolation/hanami/application/inherit_anonymous_class_spec.rb +0 -10
  74. data/spec/isolation/hanami/application/inherit_concrete_class_spec.rb +0 -14
  75. data/spec/isolation/hanami/application/not_configured_spec.rb +0 -9
  76. data/spec/isolation/hanami/application/routes/configured_spec.rb +0 -44
  77. data/spec/isolation/hanami/application/routes/not_configured_spec.rb +0 -16
  78. 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