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