hanami-helpers 0.0.0 → 0.3.0

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.
@@ -0,0 +1,911 @@
1
+ require 'hanami/helpers/form_helper/html_node'
2
+ require 'hanami/helpers/form_helper/values'
3
+ require 'hanami/helpers/html_helper/html_builder'
4
+ require 'hanami/utils/string'
5
+
6
+ module Hanami
7
+ module Helpers
8
+ module FormHelper
9
+ # Form builder
10
+ #
11
+ # @since 0.2.0
12
+ #
13
+ # @see Hanami::Helpers::HtmlHelper::HtmlBuilder
14
+ class FormBuilder < ::Hanami::Helpers::HtmlHelper::HtmlBuilder
15
+ # Set of HTTP methods that are understood by web browsers
16
+ #
17
+ # @since 0.2.0
18
+ # @api private
19
+ BROWSER_METHODS = ['GET', 'POST'].freeze
20
+
21
+ # Set of HTTP methods that should NOT generate CSRF token
22
+ #
23
+ # @since 0.2.0
24
+ # @api private
25
+ EXCLUDED_CSRF_METHODS = ['GET'].freeze
26
+
27
+ # Checked attribute value
28
+ #
29
+ # @since 0.2.0
30
+ # @api private
31
+ #
32
+ # @see Hanami::Helpers::FormHelper::FormBuilder#radio_button
33
+ CHECKED = 'checked'.freeze
34
+
35
+ # Selected attribute value for option
36
+ #
37
+ # @since 0.2.0
38
+ # @api private
39
+ #
40
+ # @see Hanami::Helpers::FormHelper::FormBuilder#select
41
+ SELECTED = 'selected'.freeze
42
+
43
+ # Separator for accept attribute of file input
44
+ #
45
+ # @since 0.2.0
46
+ # @api private
47
+ #
48
+ # @see Hanami::Helpers::FormHelper::FormBuilder#file_input
49
+ ACCEPT_SEPARATOR = ','.freeze
50
+
51
+ # Replacement for input id interpolation
52
+ #
53
+ # @since 0.2.0
54
+ # @api private
55
+ #
56
+ # @see Hanami::Helpers::FormHelper::FormBuilder#_input_id
57
+ INPUT_ID_REPLACEMENT = '-\k<token>'.freeze
58
+
59
+ # Replacement for input value interpolation
60
+ #
61
+ # @since 0.2.0
62
+ # @api private
63
+ #
64
+ # @see Hanami::Helpers::FormHelper::FormBuilder#_value
65
+ INPUT_VALUE_REPLACEMENT = '.\k<token>'.freeze
66
+
67
+ # Default value for unchecked check box
68
+ #
69
+ # @since 0.2.0
70
+ # @api private
71
+ #
72
+ # @see Hanami::Helpers::FormHelper::FormBuilder#check_box
73
+ DEFAULT_UNCHECKED_VALUE = '0'.freeze
74
+
75
+ # Default value for checked check box
76
+ #
77
+ # @since 0.2.0
78
+ # @api private
79
+ #
80
+ # @see Hanami::Helpers::FormHelper::FormBuilder#check_box
81
+ DEFAULT_CHECKED_VALUE = '1'.freeze
82
+
83
+ # ENCTYPE_MULTIPART = 'multipart/form-data'.freeze
84
+
85
+ self.html_node = ::Hanami::Helpers::FormHelper::HtmlNode
86
+
87
+ # Instantiate a form builder
88
+ #
89
+ # @overload initialize(form, attributes, params, &blk)
90
+ # Top level form
91
+ # @param form [Hanami::Helpers:FormHelper::Form] the form
92
+ # @param attributes [::Hash] a set of HTML attributes
93
+ # @param params [Hanami::Action::Params] request params
94
+ # @param blk [Proc] a block that describes the contents of the form
95
+ #
96
+ # @overload initialize(form, attributes, params, &blk)
97
+ # Nested form
98
+ # @param form [Hanami::Helpers:FormHelper::Form] the form
99
+ # @param attributes [Hanami::Helpers::FormHelper::Values] user defined
100
+ # values
101
+ # @param blk [Proc] a block that describes the contents of the form
102
+ #
103
+ # @return [Hanami::Helpers::FormHelper::FormBuilder] the form builder
104
+ #
105
+ # @since 0.2.0
106
+ # @api private
107
+ def initialize(form, attributes, context = nil, &blk)
108
+ super()
109
+
110
+ @context = context
111
+ @blk = blk
112
+
113
+ # Nested form
114
+ if @context.nil? && attributes.is_a?(Values)
115
+ @values = attributes
116
+ @attributes = {}
117
+ @name = form
118
+ else
119
+ @form = form
120
+ @name = form.name
121
+ @values = Values.new(form.values, @context.params)
122
+ @attributes = attributes
123
+ @verb_method = verb_method
124
+ @csrf_token = csrf_token
125
+ end
126
+ end
127
+
128
+ # Resolves all the nodes and generates the markup
129
+ #
130
+ # @return [Hanami::Utils::Escape::SafeString] the output
131
+ #
132
+ # @since 0.2.0
133
+ # @api private
134
+ #
135
+ # @see Hanami::Helpers::HtmlHelper::HtmlBuilder#to_s
136
+ # @see http://www.rubydoc.info/gems/hanami-utils/Hanami/Utils/Escape/SafeString
137
+ def to_s
138
+ if toplevel?
139
+ _method_override!
140
+ form(@blk, @attributes)
141
+ end
142
+
143
+ super
144
+ end
145
+
146
+ # Nested fields
147
+ #
148
+ # The inputs generated by the wrapped block will be prefixed with the given name
149
+ # It supports infinite levels of nesting.
150
+ #
151
+ # @param name [Symbol] the nested name, it's used to generate input
152
+ # names, ids, and to lookup params to fill values.
153
+ #
154
+ # @since 0.2.0
155
+ #
156
+ # @example Basic usage
157
+ # <%=
158
+ # form_for :delivery, routes.deliveries_path do
159
+ # text_field :customer_name
160
+ #
161
+ # fields_for :address do
162
+ # text_field :street
163
+ # end
164
+ #
165
+ # submit 'Create'
166
+ # end
167
+ # %>
168
+ #
169
+ # Output:
170
+ # # <form action="/deliveries" method="POST" accept-charset="utf-8" id="delivery-form">
171
+ # # <input type="text" name="delivery[customer_name]" id="delivery-customer-name" value="">
172
+ # # <input type="text" name="delivery[address][street]" id="delivery-address-street" value="">
173
+ # #
174
+ # # <button type="submit">Create</button>
175
+ # # </form>
176
+ #
177
+ # @example Multiple levels of nesting
178
+ # <%=
179
+ # form_for :delivery, routes.deliveries_path do
180
+ # text_field :customer_name
181
+ #
182
+ # fields_for :address do
183
+ # text_field :street
184
+ #
185
+ # fields_for :location do
186
+ # text_field :city
187
+ # text_field :country
188
+ # end
189
+ # end
190
+ #
191
+ # submit 'Create'
192
+ # end
193
+ # %>
194
+ #
195
+ # Output:
196
+ # # <form action="/deliveries" method="POST" accept-charset="utf-8" id="delivery-form">
197
+ # # <input type="text" name="delivery[customer_name]" id="delivery-customer-name" value="">
198
+ # # <input type="text" name="delivery[address][street]" id="delivery-address-street" value="">
199
+ # # <input type="text" name="delivery[address][location][city]" id="delivery-address-location-city" value="">
200
+ # # <input type="text" name="delivery[address][location][country]" id="delivery-address-location-country" value="">
201
+ # #
202
+ # # <button type="submit">Create</button>
203
+ # # </form>
204
+ def fields_for(name)
205
+ current_name = @name
206
+ @name = _input_name(name)
207
+ yield
208
+ ensure
209
+ @name = current_name
210
+ end
211
+
212
+ # Label tag
213
+ #
214
+ # The first param <tt>content</tt> can be a <tt>Symbol</tt> that represents
215
+ # the target field (Eg. <tt>:extended_title</tt>), or a <tt>String</tt>
216
+ # which is used as it is.
217
+ #
218
+ # @param content [Symbol,String] the field name or a content string
219
+ # @param attributes [Hash] HTML attributes to pass to the label tag
220
+ #
221
+ # @since 0.2.0
222
+ #
223
+ # @example Basic usage
224
+ # <%=
225
+ # # ...
226
+ # label :extended_title
227
+ # %>
228
+ #
229
+ # # Output:
230
+ # # <label for="book-extended-title">Extended title</label>
231
+ #
232
+ # @example Custom content
233
+ # <%=
234
+ # # ...
235
+ # label 'Title', for: :extended_title
236
+ # %>
237
+ #
238
+ # # Output:
239
+ # # <label for="book-extended-title">Title</label>
240
+ #
241
+ # @example Custom "for" attribute
242
+ # <%=
243
+ # # ...
244
+ # label :extended_title, for: 'ext-title'
245
+ # %>
246
+ #
247
+ # # Output:
248
+ # # <label for="ext-title">Extended title</label>
249
+ #
250
+ # @example Nested fields usage
251
+ # <%=
252
+ # # ...
253
+ # fields_for :address do
254
+ # label :city
255
+ # text_field :city
256
+ # end
257
+ # %>
258
+ #
259
+ # # Output:
260
+ # # <label for="delivery-address-city">City</label>
261
+ # # <input type="text" name="delivery[address][city] id="delivery-address-city" value="">
262
+ def label(content, attributes = {})
263
+ attributes = { for: _for(content, attributes.delete(:for)) }.merge(attributes)
264
+ content = case content
265
+ when String, Hanami::Utils::String
266
+ content
267
+ else
268
+ Utils::String.new(content).capitalize
269
+ end
270
+
271
+ super(content, attributes)
272
+ end
273
+
274
+ # Check box
275
+ #
276
+ # It renders a check box input.
277
+ #
278
+ # When a form is submitted, browsers don't send the value of unchecked
279
+ # check boxes. If an user unchecks a check box, their browser won't send
280
+ # the unchecked value. On the server side the corresponding value is
281
+ # missing, so the application will assume that the user action never
282
+ # happened.
283
+ #
284
+ # To solve this problem the form renders a hidden field with the
285
+ # "unchecked value". When the user unchecks the input, the browser will
286
+ # ignore it, but it will still send the value of the hidden input. See
287
+ # the examples below.
288
+ #
289
+ # When editing a resource, the form automatically assigns the
290
+ # <tt>checked="checked"</tt> attribute.
291
+ #
292
+ # @param name [Symbol] the input name
293
+ # @param attributes [Hash] HTML attributes to pass to the input tag
294
+ # @option attributes [String] :checked_value (defaults to "1")
295
+ # @option attributes [String] :unchecked_value (defaults to "0")
296
+ #
297
+ # @since 0.2.0
298
+ #
299
+ # @example Basic usage
300
+ # <%=
301
+ # check_box :free_shipping
302
+ # %>
303
+ #
304
+ # # Output:
305
+ # # <input type="hidden" name="delivery[free_shipping]" value="0">
306
+ # # <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="1">
307
+ #
308
+ # @example Specify (un)checked values
309
+ # <%=
310
+ # check_box :free_shipping, checked_value: 'true', unchecked_value: 'false'
311
+ # %>
312
+ #
313
+ # # Output:
314
+ # # <input type="hidden" name="delivery[free_shipping]" value="false">
315
+ # # <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="true">
316
+ #
317
+ # @example Automatic "checked" attribute
318
+ # # For this example the params are:
319
+ # #
320
+ # # { delivery: { free_shipping: '1' } }
321
+ # <%=
322
+ # check_box :free_shipping
323
+ # %>
324
+ #
325
+ # # Output:
326
+ # # <input type="hidden" name="delivery[free_shipping]" value="0">
327
+ # # <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="1" checked="checked">
328
+ #
329
+ # @example Force "checked" attribute
330
+ # # For this example the params are:
331
+ # #
332
+ # # { delivery: { free_shipping: '0' } }
333
+ # <%=
334
+ # check_box :free_shipping, checked: 'checked'
335
+ # %>
336
+ #
337
+ # # Output:
338
+ # # <input type="hidden" name="delivery[free_shipping]" value="0">
339
+ # # <input type="checkbox" name="delivery[free_shipping]" id="delivery-free-shipping" value="1" checked="checked">
340
+ #
341
+ # @example Multiple check boxes
342
+ # <%=
343
+ # check_box :languages, name: 'book[languages][]', value: 'italian', id: nil
344
+ # check_box :languages, name: 'book[languages][]', value: 'english', id: nil
345
+ # %>
346
+ #
347
+ # # Output:
348
+ # # <input type="checkbox" name="book[languages][]" value="italian">
349
+ # # <input type="checkbox" name="book[languages][]" value="english">
350
+ #
351
+ # @example Automatic "checked" attribute for multiple check boxes
352
+ # # For this example the params are:
353
+ # #
354
+ # # { book: { languages: ['italian'] } }
355
+ # <%=
356
+ # check_box :languages, name: 'book[languages][]', value: 'italian', id: nil
357
+ # check_box :languages, name: 'book[languages][]', value: 'english', id: nil
358
+ # %>
359
+ #
360
+ # # Output:
361
+ # # <input type="checkbox" name="book[languages][]" value="italian" checked="checked">
362
+ # # <input type="checkbox" name="book[languages][]" value="english">
363
+ def check_box(name, attributes = {})
364
+ _hidden_field_for_check_box( name, attributes)
365
+ input _attributes_for_check_box(name, attributes)
366
+ end
367
+
368
+ # Color input
369
+ #
370
+ # @param name [Symbol] the input name
371
+ # @param attributes [Hash] HTML attributes to pass to the input tag
372
+ #
373
+ # @since 0.2.0
374
+ #
375
+ # @example Basic usage
376
+ # <%=
377
+ # # ...
378
+ # color_field :background
379
+ # %>
380
+ #
381
+ # # Output:
382
+ # # <input type="color" name="user[background]" id="user-background" value="">
383
+ def color_field(name, attributes = {})
384
+ input _attributes(:color, name, attributes)
385
+ end
386
+
387
+ # Date input
388
+ #
389
+ # @param name [Symbol] the input name
390
+ # @param attributes [Hash] HTML attributes to pass to the input tag
391
+ #
392
+ # @since 0.2.0
393
+ #
394
+ # @example Basic usage
395
+ # <%=
396
+ # # ...
397
+ # date_field :birth_date
398
+ # %>
399
+ #
400
+ # # Output:
401
+ # # <input type="date" name="user[birth_date]" id="user-birth-date" value="">
402
+ def date_field(name, attributes = {})
403
+ input _attributes(:date, name, attributes)
404
+ end
405
+
406
+ # Datetime input
407
+ #
408
+ # @param name [Symbol] the input name
409
+ # @param attributes [Hash] HTML attributes to pass to the input tag
410
+ #
411
+ # @since 0.2.0
412
+ #
413
+ # @example Basic usage
414
+ # <%=
415
+ # # ...
416
+ # datetime_field :delivered_at
417
+ # %>
418
+ #
419
+ # # Output:
420
+ # # <input type="datetime" name="delivery[delivered_at]" id="delivery-delivered-at" value="">
421
+ def datetime_field(name, attributes = {})
422
+ input _attributes(:datetime, name, attributes)
423
+ end
424
+
425
+ # Datetime Local input
426
+ #
427
+ # @param name [Symbol] the input name
428
+ # @param attributes [Hash] HTML attributes to pass to the input tag
429
+ #
430
+ # @since 0.2.0
431
+ #
432
+ # @example Basic usage
433
+ # <%=
434
+ # # ...
435
+ # datetime_local_field :delivered_at
436
+ # %>
437
+ #
438
+ # # Output:
439
+ # # <input type="datetime-local" name="delivery[delivered_at]" id="delivery-delivered-at" value="">
440
+ def datetime_local_field(name, attributes = {})
441
+ input _attributes(:'datetime-local', name, attributes)
442
+ end
443
+
444
+ # Email input
445
+ #
446
+ # @param name [Symbol] the input name
447
+ # @param attributes [Hash] HTML attributes to pass to the input tag
448
+ #
449
+ # @since 0.2.0
450
+ #
451
+ # @example Basic usage
452
+ # <%=
453
+ # # ...
454
+ # email_field :email
455
+ # %>
456
+ #
457
+ # # Output:
458
+ # # <input type="email" name="user[email]" id="user-email" value="">
459
+ def email_field(name, attributes = {})
460
+ input _attributes(:email, name, attributes)
461
+ end
462
+
463
+ # Hidden input
464
+ #
465
+ # @param name [Symbol] the input name
466
+ # @param attributes [Hash] HTML attributes to pass to the input tag
467
+ #
468
+ # @since 0.2.0
469
+ #
470
+ # @example Basic usage
471
+ # <%=
472
+ # # ...
473
+ # hidden_field :customer_id
474
+ # %>
475
+ #
476
+ # # Output:
477
+ # # <input type="hidden" name="delivery[customer_id]" id="delivery-customer-id" value="">
478
+ def hidden_field(name, attributes = {})
479
+ input _attributes(:hidden, name, attributes)
480
+ end
481
+
482
+ # File input
483
+ #
484
+ # PLEASE REMEMBER TO ADD <tt>enctype: 'multipart/form-data'</tt> ATTRIBUTE TO THE FORM
485
+ #
486
+ # @param name [Symbol] the input name
487
+ # @param attributes [Hash] HTML attributes to pass to the input tag
488
+ # @option attributes [String,Array] :accept Optional set of accepted MIME Types
489
+ #
490
+ # @since 0.2.0
491
+ #
492
+ # @example Basic usage
493
+ # <%=
494
+ # # ...
495
+ # file_field :avatar
496
+ # %>
497
+ #
498
+ # # Output:
499
+ # # <input type="file" name="user[avatar]" id="user-avatar">
500
+ #
501
+ # @example Accepted mime types
502
+ # <%=
503
+ # # ...
504
+ # file_field :resume, accept: 'application/pdf,application/ms-word'
505
+ # %>
506
+ #
507
+ # # Output:
508
+ # # <input type="file" name="user[resume]" id="user-resume" accept="application/pdf,application/ms-word">
509
+ #
510
+ # @example Accepted mime types (as array)
511
+ # <%=
512
+ # # ...
513
+ # file_field :resume, accept: ['application/pdf', 'application/ms-word']
514
+ # %>
515
+ #
516
+ # # Output:
517
+ # # <input type="file" name="user[resume]" id="user-resume" accept="application/pdf,application/ms-word">
518
+ def file_field(name, attributes = {})
519
+ attributes[:accept] = Array(attributes[:accept]).join(ACCEPT_SEPARATOR) if attributes.key?(:accept)
520
+ attributes = { type: :file, name: _input_name(name), id: _input_id(name) }.merge(attributes)
521
+
522
+ input(attributes)
523
+ end
524
+
525
+ # Number input
526
+ #
527
+ # @param name [Symbol] the input name
528
+ # @param attributes [Hash] HTML attributes to pass to the number input
529
+ #
530
+ # @example Basic usage
531
+ # <%=
532
+ # # ...
533
+ # number_field :percent_read
534
+ # %>
535
+ #
536
+ # # Output:
537
+ # # <input type="number" name="book[percent_read]" id="book-percent-read" value="">
538
+ #
539
+ # You can also make use of the 'max', 'min', and 'step' attributes for
540
+ # the HTML5 number field.
541
+ #
542
+ # @example Advanced attributes
543
+ # <%=
544
+ # # ...
545
+ # number_field :priority, min: 1, max: 10, step: 1
546
+ # %>
547
+ #
548
+ # # Output:
549
+ # # <input type="number" name="book[percent_read]" id="book-precent-read" value="" min="1" max="10" step="1">
550
+ def number_field(name, attributes = {})
551
+ input _attributes(:number, name, attributes)
552
+ end
553
+
554
+ # Text-area input
555
+ #
556
+ # @param name [Symbol] the input name
557
+ # @param content [String] the content of the textarea
558
+ # @param attributes [Hash] HTML attributes to pass to the textarea tag
559
+ #
560
+ # @since 0.2.5
561
+ #
562
+ # @example Basic usage
563
+ # <%=
564
+ # # ...
565
+ # text_area :hobby
566
+ # %>
567
+ #
568
+ # # Output:
569
+ # # <textarea name="user[hobby]" id="user-hobby"></textarea>
570
+ #
571
+ # @example Set content
572
+ # <%=
573
+ # # ...
574
+ # text_area :hobby, 'Football'
575
+ # %>
576
+ #
577
+ # # Output:
578
+ # # <textarea name="user[hobby]" id="user-hobby">Football</textarea>
579
+ #
580
+ # @example Set content and HTML attributes
581
+ # <%=
582
+ # # ...
583
+ # text_area :hobby, 'Football', class: 'form-control'
584
+ # %>
585
+ #
586
+ # # Output:
587
+ # # <textarea name="user[hobby]" id="user-hobby" class="form-control">Football</textarea>
588
+ #
589
+ # @example Omit content and specify HTML attributes
590
+ # <%=
591
+ # # ...
592
+ # text_area :hobby, class: 'form-control'
593
+ # %>
594
+ #
595
+ # # Output:
596
+ # # <textarea name="user[hobby]" id="user-hobby" class="form-control"></textarea>
597
+ #
598
+ # @example Force blank value
599
+ # <%=
600
+ # # ...
601
+ # text_area :hobby, '', class: 'form-control'
602
+ # %>
603
+ #
604
+ # # Output:
605
+ # # <textarea name="user[hobby]" id="user-hobby" class="form-control"></textarea>
606
+ def text_area(name, content = nil, attributes = {})
607
+ if content.respond_to?(:to_hash)
608
+ attributes = content
609
+ content = nil
610
+ end
611
+
612
+ attributes = {name: _input_name(name), id: _input_id(name)}.merge(attributes)
613
+ textarea(content || _value(name), attributes)
614
+ end
615
+
616
+ # Text input
617
+ #
618
+ # @param name [Symbol] the input name
619
+ # @param attributes [Hash] HTML attributes to pass to the input tag
620
+ #
621
+ # @since 0.2.0
622
+ #
623
+ # @example Basic usage
624
+ # <%=
625
+ # # ...
626
+ # text_field :first_name
627
+ # %>
628
+ #
629
+ # # Output:
630
+ # # <input type="text" name="user[first_name]" id="user-first-name" value="">
631
+ def text_field(name, attributes = {})
632
+ input _attributes(:text, name, attributes)
633
+ end
634
+ alias_method :input_text, :text_field
635
+
636
+ # Radio input
637
+ #
638
+ # If request params have a value that corresponds to the given value,
639
+ # it automatically sets the <tt>checked</tt> attribute.
640
+ # This Hanami::Controller integration happens without any developer intervention.
641
+ #
642
+ # @param name [Symbol] the input name
643
+ # @param value [String] the input value
644
+ # @param attributes [Hash] HTML attributes to pass to the input tag
645
+ #
646
+ # @since 0.2.0
647
+ #
648
+ # @example Basic usage
649
+ # <%=
650
+ # # ...
651
+ # radio_button :category, 'Fiction'
652
+ # radio_button :category, 'Non-Fiction'
653
+ # %>
654
+ #
655
+ # # Output:
656
+ # # <input type="radio" name="book[category]" value="Fiction">
657
+ # # <input type="radio" name="book[category]" value="Non-Fiction">
658
+ #
659
+ # @example Automatic checked value
660
+ # # Given the following params:
661
+ # #
662
+ # # book: {
663
+ # # category: 'Non-Fiction'
664
+ # # }
665
+ #
666
+ # <%=
667
+ # # ...
668
+ # radio_button :category, 'Fiction'
669
+ # radio_button :category, 'Non-Fiction'
670
+ # %>
671
+ #
672
+ # # Output:
673
+ # # <input type="radio" name="book[category]" value="Fiction">
674
+ # # <input type="radio" name="book[category]" value="Non-Fiction" checked="checked">
675
+ def radio_button(name, value, attributes = {})
676
+ attributes = { type: :radio, name: _input_name(name), value: value }.merge(attributes)
677
+ attributes[:checked] = CHECKED if _value(name) == value
678
+ input(attributes)
679
+ end
680
+
681
+ # Password input
682
+ #
683
+ # @param name [Symbol] the input name
684
+ # @param attributes [Hash] HTML attributes to pass to the input tag
685
+ #
686
+ # @since 0.2.0
687
+ #
688
+ # @example Basic usage
689
+ # <%=
690
+ # # ...
691
+ # password_field :password
692
+ # %>
693
+ #
694
+ # # Output:
695
+ # # <input type="password" name="signup[password]" id="signup-password" value="">
696
+ def password_field(name, attributes = {})
697
+ input({ type: :password, name: _input_name(name), id: _input_id(name), value: nil }.merge(attributes))
698
+ end
699
+
700
+ # Select input
701
+ #
702
+ # @param name [Symbol] the input name
703
+ # @param values [Hash] a Hash to generate <tt><option></tt> tags.
704
+ # Keys correspond to <tt>value</tt> and values correspond to the content.
705
+ # @param attributes [Hash] HTML attributes to pass to the input tag
706
+ #
707
+ # If request params have a value that corresponds to one of the given values,
708
+ # it automatically sets the <tt>selected</tt> attribute on the <tt><option></tt> tag.
709
+ # This Hanami::Controller integration happens without any developer intervention.
710
+ #
711
+ # @since 0.2.0
712
+ #
713
+ # @example Basic usage
714
+ # <%=
715
+ # # ...
716
+ # values = Hash['it' => 'Italy', 'us' => 'United States']
717
+ # select :stores, values
718
+ # %>
719
+ #
720
+ # # Output:
721
+ # # <select name="book[store]" id="book-store">
722
+ # # <option value="it">Italy</option>
723
+ # # <option value="us">United States</option>
724
+ # # </select>
725
+ #
726
+ # @example Automatic selected option
727
+ # # Given the following params:
728
+ # #
729
+ # # book: {
730
+ # # store: 'it'
731
+ # # }
732
+ #
733
+ # <%=
734
+ # # ...
735
+ # values = Hash['it' => 'Italy', 'us' => 'United States']
736
+ # select :stores, values
737
+ # %>
738
+ #
739
+ # # Output:
740
+ # # <select name="book[store]" id="book-store">
741
+ # # <option value="it" selected="selected">Italy</option>
742
+ # # <option value="us">United States</option>
743
+ # # </select>
744
+ def select(name, values, attributes = {})
745
+ options = attributes.delete(:options) || {}
746
+ attributes = { name: _input_name(name), id: _input_id(name) }.merge(attributes)
747
+
748
+ super(attributes) do
749
+ values.each do |value, content|
750
+ if _value(name) == value
751
+ option(content, {value: value, selected: SELECTED}.merge(options))
752
+ else
753
+ option(content, {value: value}.merge(options))
754
+ end
755
+ end
756
+ end
757
+ end
758
+
759
+ # Submit button
760
+ #
761
+ # @param content [String] The content
762
+ # @param attributes [Hash] HTML attributes to pass to the button tag
763
+ #
764
+ # @since 0.2.0
765
+ #
766
+ # @example Basic usage
767
+ # <%=
768
+ # # ...
769
+ # submit 'Create'
770
+ # %>
771
+ #
772
+ # # Output:
773
+ # # <button type="submit">Create</button>
774
+ def submit(content, attributes = {})
775
+ attributes = { type: :submit }.merge(attributes)
776
+ button(content, attributes)
777
+ end
778
+
779
+ protected
780
+ # A set of options to pass to the sub form helpers.
781
+ #
782
+ # @api private
783
+ # @since 0.2.0
784
+ def options
785
+ Hash[name: @name, values: @values, verb: @verb, csrf_token: @csrf_token]
786
+ end
787
+
788
+ private
789
+ # Check the current builder is top-level
790
+ #
791
+ # @api private
792
+ # @since 0.2.0
793
+ def toplevel?
794
+ @attributes.any?
795
+ end
796
+
797
+ # Prepare for method override
798
+ #
799
+ # @api private
800
+ # @since 0.2.0
801
+ def _method_override!
802
+ if BROWSER_METHODS.include?(@verb_method)
803
+ @attributes[:method] = @verb_method
804
+ else
805
+ @attributes[:method] = DEFAULT_METHOD
806
+ @verb = @verb_method
807
+ end
808
+ end
809
+
810
+ # Return the method from attributes
811
+ #
812
+ # @api private
813
+ def verb_method
814
+ (@attributes.fetch(:method) { DEFAULT_METHOD }).to_s.upcase
815
+ end
816
+
817
+ # Return CSRF Protection token from view context
818
+ #
819
+ # @api private
820
+ # @since 0.2.0
821
+ def csrf_token
822
+ @context.csrf_token if @context.respond_to?(:csrf_token) && !EXCLUDED_CSRF_METHODS.include?(@verb_method)
823
+ end
824
+
825
+ # Return a set of default HTML attributes
826
+ #
827
+ # @api private
828
+ # @since 0.2.0
829
+ def _attributes(type, name, attributes)
830
+ { type: type, name: _input_name(name), id: _input_id(name), value: _value(name) }.merge(attributes)
831
+ end
832
+
833
+ # Input <tt>name</tt> HTML attribute
834
+ #
835
+ # @api private
836
+ # @since 0.2.0
837
+ def _input_name(name)
838
+ "#{ @name }[#{ name }]"
839
+ end
840
+
841
+ # Input <tt>id</tt> HTML attribute
842
+ #
843
+ # @api private
844
+ # @since 0.2.0
845
+ def _input_id(name)
846
+ name = _input_name(name).gsub(/\[(?<token>[[[:word:]]\-]*)\]/, INPUT_ID_REPLACEMENT)
847
+ Utils::String.new(name).dasherize
848
+ end
849
+
850
+ # Input <tt>value</tt> HTML attribute
851
+ #
852
+ # @api private
853
+ # @since 0.2.0
854
+ def _value(name)
855
+ name = _input_name(name).gsub(/\[(?<token>[[:word:]]*)\]/, INPUT_VALUE_REPLACEMENT)
856
+ @values.get(name)
857
+ end
858
+
859
+ # Input <tt>for</tt> HTML attribute
860
+ #
861
+ # @api private
862
+ # @since 0.2.0
863
+ def _for(content, name)
864
+ case name
865
+ when String, Hanami::Utils::String
866
+ name
867
+ else
868
+ _input_id(name || content)
869
+ end
870
+ end
871
+
872
+ # Hidden field for check box
873
+ #
874
+ # @api private
875
+ # @since 0.2.0
876
+ #
877
+ # @see Hanami::Helpers::FormHelper::FormBuilder#check_box
878
+ def _hidden_field_for_check_box(name, attributes)
879
+ if attributes[:value].nil? || !attributes[:unchecked_value].nil?
880
+ input({
881
+ type: :hidden,
882
+ name: attributes[:name] || _input_name(name),
883
+ value: attributes.delete(:unchecked_value) || DEFAULT_UNCHECKED_VALUE
884
+ })
885
+ end
886
+ end
887
+
888
+ # HTML attributes for check box
889
+ #
890
+ # @api private
891
+ # @since 0.2.0
892
+ #
893
+ # @see Hanami::Helpers::FormHelper::FormBuilder#check_box
894
+ def _attributes_for_check_box(name, attributes)
895
+ attributes = {
896
+ type: :checkbox,
897
+ name: _input_name(name),
898
+ id: _input_id(name),
899
+ value: attributes.delete(:checked_value) || DEFAULT_CHECKED_VALUE
900
+ }.merge(attributes)
901
+
902
+ value = _value(name)
903
+ attributes[:checked] = CHECKED if value &&
904
+ ( value == attributes[:value] || value.include?(attributes[:value]) )
905
+
906
+ attributes
907
+ end
908
+ end
909
+ end
910
+ end
911
+ end