form_input 0.9.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE +19 -0
  4. data/README.md +3160 -0
  5. data/Rakefile +19 -0
  6. data/example/controllers/ramaze/press_release.rb +104 -0
  7. data/example/controllers/ramaze/profile.rb +38 -0
  8. data/example/controllers/sinatra/press_release.rb +114 -0
  9. data/example/controllers/sinatra/profile.rb +39 -0
  10. data/example/forms/change_password_form.rb +17 -0
  11. data/example/forms/login_form.rb +14 -0
  12. data/example/forms/lost_password_form.rb +14 -0
  13. data/example/forms/new_password_form.rb +15 -0
  14. data/example/forms/password_form.rb +18 -0
  15. data/example/forms/press_release_form.rb +153 -0
  16. data/example/forms/profile_form.rb +21 -0
  17. data/example/forms/signup_form.rb +25 -0
  18. data/example/views/press_release.slim +65 -0
  19. data/example/views/profile.slim +28 -0
  20. data/example/views/snippets/form_block.slim +27 -0
  21. data/example/views/snippets/form_chunked.slim +25 -0
  22. data/example/views/snippets/form_hidden.slim +21 -0
  23. data/example/views/snippets/form_panel.slim +89 -0
  24. data/form_input.gemspec +32 -0
  25. data/lib/form_input/core.rb +1165 -0
  26. data/lib/form_input/localize.rb +49 -0
  27. data/lib/form_input/r18n/cs.yml +97 -0
  28. data/lib/form_input/r18n/en.yml +70 -0
  29. data/lib/form_input/r18n/pl.yml +122 -0
  30. data/lib/form_input/r18n/sk.yml +120 -0
  31. data/lib/form_input/r18n.rb +163 -0
  32. data/lib/form_input/steps.rb +365 -0
  33. data/lib/form_input/types.rb +176 -0
  34. data/lib/form_input/version.rb +12 -0
  35. data/lib/form_input.rb +5 -0
  36. data/test/helper.rb +21 -0
  37. data/test/localize/en.yml +63 -0
  38. data/test/r18n/cs.yml +60 -0
  39. data/test/r18n/xx.yml +51 -0
  40. data/test/reference/cs.txt +352 -0
  41. data/test/reference/cs.yml +14 -0
  42. data/test/reference/en.txt +76 -0
  43. data/test/reference/en.yml +8 -0
  44. data/test/reference/pl.txt +440 -0
  45. data/test/reference/pl.yml +16 -0
  46. data/test/reference/sk.txt +352 -0
  47. data/test/reference/sk.yml +14 -0
  48. data/test/test_core.rb +1272 -0
  49. data/test/test_localize.rb +27 -0
  50. data/test/test_r18n.rb +373 -0
  51. data/test/test_steps.rb +482 -0
  52. data/test/test_types.rb +307 -0
  53. metadata +145 -0
data/README.md ADDED
@@ -0,0 +1,3160 @@
1
+ # Form input
2
+
3
+ Form input is a gem which helps dealing with a web request input and with the creation of HTML forms.
4
+
5
+ Install the gem:
6
+
7
+ ``` shell
8
+ gem install form_input
9
+ ```
10
+
11
+ Describe your forms in a [DSL] like this:
12
+
13
+ ``` ruby
14
+ # contact_form.rb
15
+ require 'form_input'
16
+ class ContactForm < FormInput
17
+ param! :email, "Email address", EMAIL_ARGS
18
+ param! :name, "Name"
19
+ param :company, "Company"
20
+ param! :message, "Message", 1000, type: :textarea, size: 16, filter: ->{ rstrip }
21
+ end
22
+ ```
23
+
24
+ Then use them in your controllers/route handlers like this:
25
+
26
+ ``` ruby
27
+ # app.rb
28
+ get '/contact' do
29
+ @form = ContactForm.new
30
+ @form.set( email: user.email, name: user.full_name ) if user?
31
+ slim :contact_form
32
+ end
33
+
34
+ post '/contact' do
35
+ @form = ContactForm.new( request )
36
+ return slim :contact_form unless @form.valid?
37
+ text = @form.params.map{ |p| "#{p.title}: #{p.value}\n" }.join
38
+ sent = Email.send( settings.contact_recipient, text, reply_to: @form.email )
39
+ slim( sent ? :contact_sent : :contact_failed )
40
+ end
41
+ ```
42
+
43
+ Using them in your templates is as simple as this:
44
+
45
+ ``` slim
46
+ // contact_form.slim
47
+ .panel.panel-default
48
+ .panel-heading
49
+ = @title = "Contact Form"
50
+ .panel-body
51
+ form *form_attrs
52
+ fieldset
53
+ == snippet :form_panel, params: @form.params
54
+ button.btn.btn-default type='submit' Send
55
+ ```
56
+
57
+ The `FormInput` class will take care of sanitizing the input,
58
+ converting it into any desired internal representation,
59
+ validating it, and making it available in a model-like structure.
60
+ The provided template snippets will take care of rendering the form parameters
61
+ as well as any errors detected back to the user.
62
+ You just get to use the input and control the flow the way you want.
63
+ The gem is completely framework agnostic,
64
+ comes with full test coverage,
65
+ and even supports multi-step forms and localization out of the box.
66
+ Sounds cool enough? Then read on.
67
+
68
+ ## Table of Contents
69
+
70
+ * [Introduction](#form-input)
71
+ * [Table of Contents](#table-of-contents)
72
+ * [Form Basics](#form-basics)
73
+ * [Defining Parameters](#defining-parameters)
74
+ * [Internal vs External Representation](#internal-vs-external-representation)
75
+ * [Input Filter](#input-filter)
76
+ * [Output Format](#output-format)
77
+ * [Input Transform](#input-transform)
78
+ * [Array and Hash Parameters](#array-and-hash-parameters)
79
+ * [Reusing Form Parameters](#reusing-form-parameters)
80
+ * [Creating Forms](#creating-forms)
81
+ * [Errors and Validation](#errors-and-validation)
82
+ * [Using Forms](#using-forms)
83
+ * [URL Helpers](#url-helpers)
84
+ * [Form Helpers](#form-helpers)
85
+ * [Extending Forms](#extending-forms)
86
+ * [Parameter Options](#parameter-options)
87
+ * [Form Templates](#form-templates)
88
+ * [Form Template](#form-template)
89
+ * [Simple Parameters](#simple-parameters)
90
+ * [Hidden Parameters](#hidden-parameters)
91
+ * [Complex Parameters](#complex-parameters)
92
+ * [Text Area](#text-area)
93
+ * [Select and Multi-Select](#select-and-multi-select)
94
+ * [Radio Buttons](#radio-buttons)
95
+ * [Checkboxes](#checkboxes)
96
+ * [Inflatable Parameters](#inflatable-parameters)
97
+ * [Extending Parameters](#extending-parameters)
98
+ * [Grouped Parameters](#grouped-parameters)
99
+ * [Chunked Parameters](#chunked-parameters)
100
+ * [Multi-Step Forms](#multi-step-forms)
101
+ * [Defining Multi-Step Forms](#defining-multi-step-forms)
102
+ * [Multi-Step Form Functionality](#multi-step-form-functionality)
103
+ * [Using Multi-Step Forms](#using-multi-step-forms)
104
+ * [Rendering Multi-Step Forms](#rendering-multi-step-forms)
105
+ * [Localization](#localization)
106
+ * [Error Messages and Inflection](#error-messages-and-inflection)
107
+ * [Localizing Forms](#localizing-forms)
108
+ * [Localizing Parameters](#localizing-parameters)
109
+ * [Localization Helpers](#localization-helpers)
110
+ * [Inflection Filter](#inflection-filter)
111
+ * [Localizing Form Steps](#localizing-form-steps)
112
+ * [Supported Locales](#supported-locales)
113
+
114
+
115
+ ## Form Basics
116
+
117
+ The following chapters explain how to describe your forms using a [DSL],
118
+ what's the difference between internal and external representation,
119
+ how to create form instances and how to deal with errors,
120
+ and, finally, how to access the form input itself.
121
+
122
+ ### Defining Parameters
123
+
124
+ To create a form, simply inherit from `FormInput` and then
125
+ use the `param` or `param!` methods to define form parameters like this:
126
+
127
+ ``` ruby
128
+ require 'form_input'
129
+ class MyForm < FormInput
130
+ param! :email, "Email Address"
131
+ param :name, "Full Name"
132
+ end
133
+ ```
134
+
135
+ The `param` method takes
136
+ parameter _name_
137
+ and
138
+ parameter _title_
139
+ as arguments.
140
+ The _name_ is how you will address the parameter in your code,
141
+ while the optional _title_ is the string which will be displayed to the user by default.
142
+
143
+ The `param!` method works the same way but creates a required parameter.
144
+ Such parameters are required to appear in the input and have non-empty value.
145
+ Failure to do so will be automatically reported as an error
146
+ (discussed further in [Errors and Validation](#errors-and-validation)).
147
+
148
+ Both methods actually take the optional _options_ as their last argument, too.
149
+ The _options_ is a hash which is used to control
150
+ most aspects of the parameter.
151
+ In fact, using the _title_ argument is just a shortcut identical to
152
+ setting the parameter option `:title` to the same value.
153
+ And using the `param!` method is identical to setting the parameter option `:required` to `true`.
154
+ The following two declarations are therefore the same:
155
+
156
+ ``` ruby
157
+ param! :email, "Email Address"
158
+ param :email, title: "Email Address", required: true
159
+ ```
160
+
161
+ Parameters support many more parameter options,
162
+ and we will discuss each in turn as we go.
163
+ Comprehensive summary for an avid reader is however available in [Parameter Options](#parameter-options).
164
+
165
+ The value of each parameter is a string by default (or `nil` if the parameter is not set).
166
+ The string size is implicitly limited to 255 characters and bytes by default.
167
+ To limit the size explictly, you can use an optional _size_ parameter like this:
168
+
169
+ ``` ruby
170
+ param! :title, "Title", 100
171
+ ```
172
+
173
+ This limits the string to 100 characters and 255 bytes.
174
+ That's because
175
+ as long as the character size limit is less than or equal to 255,
176
+ the implicit 255 bytes limit is retained.
177
+ Such setting is most suitable for strings stored in a database as the `varchar` type.
178
+ If the character size limit is greater than 255, no byte size limit is enforced by default.
179
+ Such setting is most suitable for strings stored in a database as the `text` type.
180
+ Of course, you can set both character and byte size limits yourself like this:
181
+
182
+ ``` ruby
183
+ param :text, "Text", 50000, max_bytesize: 65535
184
+ ```
185
+
186
+ This is identical to setting the `:max_size` and `:max_bytesize` options explicitly.
187
+ Similarly, there are the `:min_size` and `:min_bytesize` counterparts,
188
+ which you can use to limit the minimum sizes like this:
189
+
190
+ ``` ruby
191
+ param :nick, "Nick Name", min_size: 3, max_size: 8
192
+ ```
193
+
194
+ The size limits are also often used for passwords.
195
+ Those usually use a bit more options, though:
196
+
197
+ ``` ruby
198
+ class PasswordForm
199
+ param :password, "Password", min_size: 8, max_size: 16, type: :password,
200
+ filter: ->{ chomp }
201
+ end
202
+ ```
203
+
204
+ The `:filter` option specifies a code block
205
+ which is used to preprocess the incoming string value.
206
+ By default, all parameters use a filter which squeezes any whitespace into a single space
207
+ and then strips the leading and trailing whitespace entirely.
208
+ This way the string input is always nice and clean even if the user types some extra spaces somewhere.
209
+ For passwords, though, we want to preserve the characters as they are, including spaces.
210
+ We could do that by simply setting the `:filter` option to `nil`.
211
+ However, at the same time we want to get rid of the trailing newline character
212
+ which is often appended by the browser
213
+ when the user cuts and pastes the password from somewhere.
214
+ Not doing this would make the password fail for no apparent reason,
215
+ resulting in poor user experience.
216
+ That's why we use `chomp` as the filter above.
217
+ The filter block is executed in the context of the string value itself,
218
+ so it actually executes `String#chomp` to strip the trailing newline if present.
219
+ More details about filters will follow in the very next chapter
220
+ [Internal vs External Representation](#internal-vs-external-representation).
221
+
222
+ The `:type` option shown above is another common option used often.
223
+ The `FormInput` class itself doesn't care much about it,
224
+ but it is passed through to the form templates to make the parameter render properly.
225
+ Similarly, the `:disabled` option can be used to render the parameter as disabled.
226
+ In fact, any parameter option you specify is available in the templates,
227
+ so you can pass through arbitrary things like `:subtitle` or `:help`
228
+ and use them in the templates any way you like.
229
+
230
+ It's also worth mentioning that the options
231
+ can be evaluated dynamically at runtime.
232
+ Simply pass a code block in place of any option value
233
+ (except those whose value is already supposed to contain a code block, like `:filter` above)
234
+ and it will be called to obtain the actual value.
235
+ The block is called in context of the form parameter itself,
236
+ so it can access any of its methods and its form's methods easily.
237
+ For example, you can let the form automatically disable some fields
238
+ based on available user permissions by defining `is_forbidden?` accordingly:
239
+
240
+ ``` ruby
241
+ param :avatar, "Avatar",
242
+ disabled: ->{ form.is_forbidden?( :avatar ) }
243
+ param :comment, "Comment", type: :textarea,
244
+ disabled: ->{ form.is_forbidden?( :comment ) }
245
+ ```
246
+
247
+ If you happen to use some option arguments often,
248
+ you can factor them out and share them like this:
249
+
250
+ ``` ruby
251
+ FEATURED_ARGS = { disabled: ->{ form.is_forbidden?( name ) } }
252
+ param :avatar, "Avatar", FEATURED_ARGS
253
+ param :comment, "Comment", FEATURED_ARGS, type: :textarea
254
+ ```
255
+
256
+ This works since you can actually pass several hashes in place of the _options_ parameter
257
+ and they all get merged together from left to right.
258
+ This allows you to mix various options presets together and then tweak them further as needed.
259
+
260
+ ### Internal vs External Representation
261
+
262
+ Now when you know how to define some parameters,
263
+ let's talk about the parameter values a bit.
264
+ For this, it is important that you understand
265
+ the difference between their internal and external representations.
266
+
267
+ The internal representation, as you might have guessed,
268
+ are the parameter values which you will use in your application.
269
+ The external representation is how the parameters are present
270
+ to the browser via HTML forms or URLs
271
+ and passed back to the the server.
272
+
273
+ Normally, both representations are the same.
274
+ The parameters are named the same way in both cases
275
+ and their values are strings in both cases, too.
276
+ But that doesn't have to be that way.
277
+
278
+ First of all, it is possible to change the external name of the parameter.
279
+ Both `param` and `param!` methods actually accept
280
+ an optional _code_ argument,
281
+ which can be used like this:
282
+
283
+ ``` ruby
284
+ param! :query, :q, "Query"
285
+ ```
286
+
287
+ This lets you call the parameter `query` in your application,
288
+ but in forms and URLs it will use its shorter code name `q` instead.
289
+ This also comes handy when you need to change the external name for some reason,
290
+ but want to retain the internal name which your application uses all over the place.
291
+
292
+ #### Input Filter
293
+
294
+ Now the code name was the easy part.
295
+ The cool part is that the parameter values can have different
296
+ internal and external representations as well.
297
+ The external representation is always a string, of course,
298
+ but we can choose the internal representation at will.
299
+
300
+ We have already seen above that each parameter has a `:filter` option
301
+ which is used to preprocess the input string the way we want.
302
+ If you don't specify any filter explicitly,
303
+ the parameter gets an implicit one which cleans up any whitespace in the input string like this:
304
+
305
+ ``` ruby
306
+ filter: ->{ gsub( /\s+/, ' ' ).strip }
307
+ ```
308
+
309
+ Note that parameters which are not present in the web request
310
+ are never passed through a filter and
311
+ simply remain set to their previous value, which is `nil` by default.
312
+ The filter therefore only needs to deal with string input values,
313
+ not `nil` or anything else.
314
+
315
+ Of course, the filter can do any string processing you need.
316
+ For example, this filter converts typical product keys into their canonic form:
317
+
318
+ ``` ruby
319
+ filter: ->{ gsub( /[\s-]+/, '' ).gsub( /.{5}(?=.)/, '\0-' ).upcase }
320
+ ```
321
+
322
+ However, the truth is that the filter doesn't have to return a string.
323
+ It can return any object type you want.
324
+ For example, here is a naive filter which converts any input string into an integer value:
325
+
326
+ ``` ruby
327
+ filter: ->{ to_i },
328
+ class: Integer
329
+ ```
330
+
331
+ The `:class` option is used to tell `FormInput` what kind of object is the filter supposed to return.
332
+ When set, it is used to validate the input after the conversion,
333
+ and any mismatch is reported as an error.
334
+ The option accepts an array of object types, too.
335
+ This is handy for example when the filter returns boolean values:
336
+
337
+ ``` ruby
338
+ filter: ->{ self == "true" },
339
+ class: [ TrueClass, FalseClass ]
340
+ ```
341
+
342
+ The naive integer filter shown above works fine as long as the input is correct,
343
+ but the problem is that it creates integers even from completely incorrect input.
344
+ If you want to make sure the user didn't make a typo in their input,
345
+ the following filter is more suitable:
346
+
347
+ ``` ruby
348
+ filter: ->{ Integer( self, 10 ) rescue self },
349
+ class: Integer
350
+ ```
351
+
352
+ This filter uses more strict conversion which fails in case of invalid input.
353
+ In such case the filter uses the `rescue` clause
354
+ to keep the original string value intact.
355
+ This assures that it can be displayed to the user and edited again to fix it.
356
+ This is a really good practice -
357
+ making sure that even bad input can round trip back to the user -
358
+ so you should stick to it whenever possible.
359
+
360
+ There is one last thing to take care of - an empty input string.
361
+ Whenever the user submits the form without entering anything in the input fields,
362
+ the browser sends empty strings to the server as the parameter values.
363
+ In this regard an empty string is the same as no input as far as the form is concerned,
364
+ so both `nil` value and empty string are considered as valid input for optional parameters.
365
+ The `FormInput` normally preserves those values intact so you can distinguish the two cases if you wish.
366
+ But in case of the integer conversion it is much more convenient if the empty string gets converted to `nil`.
367
+ It makes it easier to work with the input value afterwards,
368
+ testing for its presence, using the `||=` operator, and so on.
369
+
370
+ The complete filter for converting numbers to integers should thus look like this:
371
+
372
+ ``` ruby
373
+ filter: ->{ ( Integer( self, 10 ) rescue self ) unless empty? },
374
+ class: Integer
375
+ ```
376
+
377
+ Of course, all that would be a lot of typing for something as common as integer parameters.
378
+ That's why the `FormInput` class comes with plenty standard filters predefined:
379
+
380
+ ``` ruby
381
+ param :int, INTEGER_ARGS
382
+ param :float, FLOAT_ARGS
383
+ param :bool, BOOL_ARGS # pulldown style.
384
+ param :check, CHECKBOX_ARGS # checkbox style.
385
+ ```
386
+
387
+ You can check the `form_input/types.rb` source file to see how they are defined
388
+ and either use them directly as they are or use them as a starting point for your own variants.
389
+
390
+ And that's about it.
391
+ However, as this chapter is quite important for understanding how the input filters work,
392
+ let's reiterate:
393
+
394
+ * You can use filters to convert input parameters into any type you want.
395
+ * Make sure the filters keep the original string in case of errors so the user can fix it.
396
+ * You don't have to worry about `nil` input values in filters.
397
+ * Just make sure you treat an empty or blank string as whatever you consider appropriate.
398
+
399
+ #### Output Format
400
+
401
+ Now you know how to convert external values into their internal representation,
402
+ but that's only half of the story.
403
+ The internal values have to be converted to their external representation as well,
404
+ and that's what output formatters are for.
405
+
406
+ By default, the `FormInput` class will use simple `to_s` conversion to create the external value.
407
+ But you can easily change this by providing your own `:format` filter instead:
408
+
409
+ ``` ruby
410
+ param :scientific_float, FLOAT_ARGS,
411
+ format: ->{ '%e' % self }
412
+ ```
413
+
414
+ The provided block will be called in the context of the parameter value itself
415
+ and its result will be passed to the `to_s` conversion to create the final external value.
416
+
417
+ But the use of a formatter is more than just mere cosmetics.
418
+ You will often use the formatter to complement your input filter.
419
+ For example, this is one possible way how to map arbitrary external values
420
+ to their internal representation and back:
421
+
422
+ ``` ruby
423
+ SORT_MODES = { id: 'n', views: 'v', age: 'a', likes: 'l' }
424
+ SORT_MODE_PARAMETERS = SORT_MODES.invert
425
+ param :sort_mode, :s,
426
+ filter: ->{ SORT_MODE_PARAMETERS[ self ] || self },
427
+ format: ->{ SORT_MODES[ self ] },
428
+ class: Symbol
429
+ ```
430
+
431
+ Note that once again the original value is preserved in case of error,
432
+ so it can be passed back to the user for fixing.
433
+
434
+ Another example shows how to process credit card expiration field:
435
+
436
+ ``` ruby
437
+ EXPIRY_ARGS = {
438
+ placeholder: 'MM/YYYY',
439
+ filter: ->{
440
+ FormInput.parse_time( self, '%m/%y' ) rescue FormInput.parse_time( self, '%m/%Y' ) rescue self
441
+ },
442
+ format: ->{ strftime( '%m/%Y' ) rescue self },
443
+ class: Time,
444
+ }
445
+ param :expiry, EXPIRY_ARGS
446
+ ```
447
+
448
+ Note that the formatter won't be called if the parameter value is `nil`
449
+ or if it is already a string when it should be some other type
450
+ (for example because the input filter conversion failed),
451
+ so you don't have to worry about that.
452
+ But it doesn't hurt to add the rescue clause like above
453
+ just in case the parameter value is set to something unexpected,
454
+ especially if the formatter is supposed to be reused at multiple places.
455
+
456
+ The `FormInput.parse_time` is a helper method which works like `Time.strptime`,
457
+ except that it fails if the input string contains trailing garbage.
458
+ Without this feature, input like `01/2016` would be parsed as `01/20` by `'%m/%y'`
459
+ and interpreted as `01/2020`, which is utterly wrong.
460
+ So better use this helper instead if you want your input validated properly.
461
+ An added bonus is that it can also ignore the `-_^` modifiers after the `%` sign,
462
+ so you can use the same time format string for both parsing and formatting.
463
+
464
+ To help you get started,
465
+ the `FormInput` class comes with several time filters and formatters predefined:
466
+
467
+ ``` ruby
468
+ param :time, TIME_ARGS # YYYY-MM-DD HH:MM:SS stored as Time.
469
+ param :us_date, US_DATE_ARGS # MM/DD/YYYY stored as Time.
470
+ param :uk_date, UK_DATE_ARGS # DD/MM/YYYY stored as Time.
471
+ param :eu_date, EU_DATE_ARGS # D.M.YYYY stored as Time.
472
+ param :hours, HOURS_ARGS # HH:MM stored as seconds since midnight.
473
+ ```
474
+
475
+ You can use them as they are but feel free to create your own variants instead.
476
+
477
+ #### Input Transform
478
+
479
+ So, there are the `:filter` and `:format` options to convert the parameter values
480
+ from an external to internal representation and back. So far so good.
481
+ But the truth is that the `FormInput` class supports one additional input transformation.
482
+ This transformation is set with the `:transform` option
483
+ and is invoked after the `:filter` filter.
484
+ So, what's the difference between `:filter` and `:transform`?
485
+
486
+ For scalar values, like normal string or integer parameters, there is none.
487
+ In that case the `:transform` is just an additional filter,
488
+ and you are free to use either or both.
489
+ But `FormInput` class supports also array and hash parameters,
490
+ as we will learn in the very next chapter,
491
+ and that's where it makes the difference.
492
+ The input filter is used to convert each individual element,
493
+ whereas the input transformation operates on the entire parameter value,
494
+ and can thus process the entire array or hash as a whole.
495
+
496
+ What you use the input transformation for is up to you.
497
+ The `FormInput` class however comes with a predefined `PRUNED_ARGS` transformation
498
+ which converts an empty string value to `nil` and prunes `nil` and empty elements from arrays and hashes,
499
+ ensuring that the resulting input is free of clutter.
500
+ This comes especially handy when used together with array parameters, which we will discuss next.
501
+
502
+ ### Array and Hash Parameters
503
+
504
+ So far we have been discussing only simple scalar parameters,
505
+ like strings or integers.
506
+ But web requests commonly support the array and hash parameters as well
507
+ using the `array[]=value` and `hash[key]=value` syntax, respectively,
508
+ and thus so does the `FormInput` class.
509
+
510
+ To declare an array parameter, use either the `array` or `array!` method:
511
+
512
+ ``` ruby
513
+ array :keywords, "Keywords"
514
+ ```
515
+
516
+ Similarly to `param!`, the `array!` method creates a required array parameter,
517
+ which means that the array must be present and may not be empty.
518
+ The `array` method on the other hand creates an optional array parameter,
519
+ which doesn't have to be filled in at all.
520
+ Note that like in case of scalar parameters,
521
+ array parameters not found in the input remain set to their default `nil` value,
522
+ rather than becoming an empty array.
523
+
524
+ All the parameter options of scalar parameters can be used with array parameters as well.
525
+ In this case, however, they apply to the individual elements of the array.
526
+ The array parameters additionaly support the `:min_count` and `:max_count` options,
527
+ which restrict the number of elements the array can have.
528
+ For example, to limit the keywords both in string size and element count, you can do this:
529
+
530
+ ``` ruby
531
+ array :keywords, "Keywords", 35, max_count: 20
532
+ ```
533
+
534
+ We have already discussed the input and output filters and input transformation.
535
+ The input `:filter` and output `:format` are applied to the elements of the array,
536
+ whereas the input `:transform` is applied to the array as a whole.
537
+ For example, to get sorted array of integers you can do this:
538
+
539
+ ``` ruby
540
+ array :ids, INTEGER_ARGS, transform: ->{ compact.sort }
541
+ ```
542
+
543
+ The `compact` method above takes care of removing any unfilled entries from the array prior sorting.
544
+ This is often desirable,
545
+ and if you don't need to use your own transformation,
546
+ you can use the predefined `PRUNED_ARGS` transformation which does the same
547
+ and discards both `nil` and empty elements:
548
+
549
+ ``` ruby
550
+ array :ids, INTEGER_ARGS, PRUNED_ARGS
551
+ array :keywords, "Keywords", PRUNED_ARGS
552
+ ```
553
+
554
+ The hash attributes are very much like the array attributes,
555
+ you just use the `hash` or `hash!` method to declare them:
556
+
557
+ ``` ruby
558
+ hash :users, "Users"
559
+ ```
560
+
561
+ The biggest difference from arrays is that the hash parameters use keys to address the elements.
562
+ By default, `FormInput` accepts only integer keys and automatically converts them to integers.
563
+ Their range can be restricted by `:min_key` and `:max_key` options,
564
+ which default to 0 and 2<sup>64</sup>-1, respectively.
565
+ Alternatively, if you know what are you doing,
566
+ you can allow use of non-integer string keys by using the `:match_key` option,
567
+ which should specify a regular expression
568
+ (or an array of regular expressions)
569
+ which all hash keys must match.
570
+ This may not be the wisest move, but it's your call.
571
+ Just make sure you use the `\A` and `\z` anchors rather than `^` and `$`,
572
+ so you don't leave yourself open to nasty suprises.
573
+
574
+ While practical use of hash parameters with forms is fairly limited,
575
+ so you will most likely only use them with URL based non-form input, if ever,
576
+ the array parameters are pretty common.
577
+ The examples above could be used for gathering list of input fields into single array,
578
+ which is useful as well,
579
+ but the most common use of array parameters is for multi-select or multi-checkbox fields.
580
+
581
+ To declare a select parameter, you can set the `:type` to `:select` and
582
+ use the `:data` option to provide an array of values for the select menu.
583
+ The array contains pairs of parameter values to use and the corresonding text to show to the user.
584
+ For example, using a [Sequel]-like `Country` model:
585
+
586
+ ``` ruby
587
+ COUNTRIES = Country.all.map{ |c| [ c.code, c.name ] }
588
+ param :country, "Country", type: :select, data: COUNTRIES
589
+ ```
590
+
591
+ To turn select into multi-select, basically just change `param` into `array` and that's it:
592
+
593
+ ``` ruby
594
+ array :countries, "Countries", type: :select, data: COUNTRIES
595
+ ```
596
+
597
+ Note that it also makes sense to change the parameter name into the plural form, so we did that.
598
+
599
+ Now if you want to render this as a list of radio buttons or checkboxes instead,
600
+ all you need to do is to change the parameter type to `:radio:` or `:checkbox`, respectively:
601
+
602
+ ``` ruby
603
+ param :country, "Country", type: :radio, data: COUNTRIES
604
+ array :countries, "Countries", type: :checkbox, data: COUNTRIES
605
+ ```
606
+
607
+ That's all it takes.
608
+
609
+ To validate the input, you will likely want to make sure the code received is really a valid country code.
610
+ In case of scalar parameters, this can be done easily by using the `:check` callback,
611
+ which is executed in the context of the parameter itself and can examine the value and do any checks it wants:
612
+
613
+ ``` ruby
614
+ check: ->{ report( "%p is not valid" ) unless Country[ value ] }
615
+ ```
616
+
617
+ It can be also done by the `:test` callback,
618
+ which is executed in the context of the parameter itself as well,
619
+ but receives the value to test as an argument.
620
+ In case of arrays and hashes, it is passed each element value in turn,
621
+ for as long as no error is reported and the parameter remains valid:
622
+
623
+ ``` ruby
624
+ test: ->( value ){ report( "%p contain invalid code" ) unless Country[ value ] }
625
+ ```
626
+
627
+ The advantage of the `:test` callback is that it works the same way regardless of the parameter kind,
628
+ scalar or not,
629
+ so it is preferable to use it
630
+ if you plan to factor this into a `COUNTRY_ARGS` helper which works with both kinds of parameters.
631
+
632
+ In either case, the `report` method is used to report any problems about the parameter,
633
+ which marks the parameter as invalid at the same time.
634
+ More on this will follow in the chapter [Errors and Validation](#errors-and-validation).
635
+
636
+ Alternatively, you may want to convert the country code into the `Country` object internally,
637
+ which will take the care of validation as well:
638
+
639
+ ``` ruby
640
+ COUNTRY_ARGS = {
641
+ data: ->{ Country.all.map{ |c| [ c, c.name ] } },
642
+ filter: ->{ Country[ self ] },
643
+ format: ->{ code },
644
+ class: Country
645
+ }
646
+ param! :country, "Country", COUNTRY_ARGS, type: :select
647
+ ```
648
+
649
+ Either way is fine, so choose whichever suits you best.
650
+ Just note that the data array now contains the `Country` objects themselves rather than their country codes,
651
+ and that we have opted for creating that array dynamically instead of using a static one.
652
+ And remember that it is really wise to factor reusable things like this
653
+ into their own helper like the `COUNTRY_ARGS` above for easier reuse.
654
+
655
+ Finally, a little bit of warning.
656
+ Note that the web request syntax supports arbitrarily nested hash and array attributes.
657
+ The `FormInput` class will accept them and apply the input transformations appropriately,
658
+ but then it will refuse to validate anything but flat arrays and hashes,
659
+ as it is way too easy to shoot yourself in the foot with complex nested structures coming from untrusted source.
660
+ The word of advice is just to stay away from those
661
+ and let the `FormInput` protect you from such input automatically.
662
+ But if you think you know what you are doing and really need such a complex input,
663
+ you can use the input transformation
664
+ to convert it to flat array or hash,
665
+ or intercept the validation and handle the parameter yourself,
666
+ which will very likely open a can of worms and leave you prone to many problems.
667
+ You have been warned.
668
+
669
+ ### Reusing Form Parameters
670
+
671
+ It happens fairly often that you will want to use some form parameters at multiple places.
672
+ The `FormInput` class provides two ways of dealing with this - form inheritance and parameter copying.
673
+
674
+ The form inheritance is straightforward.
675
+ Simply define some form, then inherit from it and add more parameters as needed:
676
+
677
+ ``` ruby
678
+ class NewPasswordForm < PasswordForm
679
+ param! :password_check, "Repeated Password"
680
+ end
681
+ ```
682
+
683
+ Obviously, the practical use of such approach is very limited.
684
+ Most often the parameters you want to reuse won't be the first parameters of the form.
685
+ For this reason, the `FormInput` also supports parameter copying which is way more flexible.
686
+ You can copy either entire forms or just select parameters like this:
687
+
688
+ ``` ruby
689
+ class SignupForm < FormInput
690
+ param! :first_name, "First Name"
691
+ param! :last_name, "Last Name"
692
+ param! :email, "Email"
693
+ copy PasswordForm
694
+ end
695
+ class ProfileForm < FormInput
696
+ copy SignupForm[ :first_name, :last_name ]
697
+ param :company, "Company"
698
+ param :country, "Country"
699
+ end
700
+ ```
701
+
702
+ Parameter copying has another advantage - you can actually pass in options
703
+ which you want to add or change in the copied versions:
704
+
705
+ ``` ruby
706
+ class ChangePasswordForm < FormInput
707
+ param! :old_password, "Old Password"
708
+ copy PasswordForm, title: "New Password"
709
+ end
710
+ ```
711
+
712
+ Just make sure the new options make sense for all the parameters copied.
713
+
714
+ ### Creating Forms
715
+
716
+ Now when you know how to create the `FormInput` classes which describe your input parameters,
717
+ it's about time you learn how to create the instances of those classes themselves.
718
+ We will use the `ContactForm` class from the [Introduction](#form-input) as an example.
719
+
720
+ First of all, before there is any external input, you will want to create an empty form input instance:
721
+
722
+ ``` ruby
723
+ form = ContactForm.new
724
+ ```
725
+
726
+ Once you have it, you can preset its parameters from a hash with the `set` method:
727
+
728
+ ``` ruby
729
+ form.set( email: user.email, name: user.full_name ) if user?
730
+ ```
731
+
732
+ If you want to preset the parameters unconditionally,
733
+ you may pass the hash directly to the `new` method instead:
734
+
735
+ ``` ruby
736
+ form = ContactForm.new( email: user.email, name: user.full_name )
737
+ ```
738
+
739
+ You can even ask your models to prefill complex forms without knowing the details:
740
+
741
+ ``` ruby
742
+ form = ProfileForm.new( user.profile_hash )
743
+ ```
744
+
745
+ Later on, after you receive the web request containing the input parameters,
746
+ just instantiate the form and fill it with the request input
747
+ by passing it the `Rack::Request` compatible `request` argument:
748
+
749
+ ``` ruby
750
+ form = ContactForm.new( request )
751
+ ```
752
+
753
+ The `initialize` method internally dispatches any `Hash` argument to the `set` method,
754
+ while any other argument is passed to the `import` method,
755
+ so the above is equivalent to this:
756
+
757
+ ``` ruby
758
+ form = ContactForm.new.import( request )
759
+ ```
760
+
761
+ There is a fundamental difference between the `set` and `import` methods
762
+ which you must understand.
763
+ The former takes parameters in their internal representation and applies no input processing,
764
+ while the latter takes parameters in their external representation and applies input filtering and transformations to them.
765
+ It also conveniently ignores any input parameters which the form doesn't define.
766
+ On the contrary, the `set` method can be used to set any attributes of the instance,
767
+ even those which are not the form parameters.
768
+
769
+ Normally, it is pretty safe to simply use the `new` method alone.
770
+ You have to make sure to use the `import` method explicitly only
771
+ if you have a hash with parameters in their external representation which you want processed.
772
+ This can happen for example if you want to use [Sinatra]'s `params` hash
773
+ to include parts of the URL as the form input:
774
+
775
+ ``` ruby
776
+ get '/contact/:email' do
777
+ form = ContactForm.new( params ) # NEVER EVER DO THIS!
778
+ form = ContactForm.new.import( params ) # Do this instead if you need to.
779
+ end
780
+ ```
781
+
782
+ If you are worried that you might make a mistake,
783
+ you can use one of the three helper shortcuts
784
+ which make it easier to remember which one to use when:
785
+
786
+ ``` ruby
787
+ form = ContactForm.from_request( request ) # Like new.import, for Rack request with external values.
788
+ form = ContactForm.from_params( params ) # Like new.import, for params hash of external values.
789
+ form = ContactForm.from_hash( some_hash ) # Like new.set, for hash of internal values.
790
+ ```
791
+
792
+ Regardless of how you create the form instance,
793
+ if you later decide to clear some parameters, you can use the `clear` method.
794
+ You can either clear the entire form, named parameters, or parameter subsets (which we will discuss in detail later):
795
+
796
+ ``` ruby
797
+ form.clear
798
+ form.clear( :message )
799
+ form.clear( :name, :company )
800
+ form.clear( form.disabled_params )
801
+ ```
802
+
803
+ Alternatively, you can create form copies with just a subset of parameters set:
804
+
805
+ ``` ruby
806
+ form.only( :email, :message )
807
+ form.only( form.required_params )
808
+ form.except( :message )
809
+ form.except( form.hidden_params )
810
+ ```
811
+
812
+ Of course, creating copies with either `dup` or `clone` works as well.
813
+
814
+ In either case, you now have your form with the input parameters set,
815
+ and you are all eager to use it.
816
+ But before we discuss how to do that,
817
+ you need to learn about errors and input validation.
818
+
819
+ ### Errors and Validation
820
+
821
+ Input validation is a must.
822
+ It's impossible to overstate how important it is.
823
+ Many applications opt for letting the models do the validation for them,
824
+ but that's often way too late.
825
+ Besides, lot of input is not intended for models at all.
826
+
827
+ The `FormInput` class therefore helps you validate all input as soon as possible instead,
828
+ before you even touch it.
829
+ All you need to do is to call the `valid?` method
830
+ and refrain from using the input unless it returns `true`:
831
+
832
+ ``` ruby
833
+ return unless form.valid?
834
+ ```
835
+
836
+ Of course, you don't have to give up right away.
837
+ The `FormInput` class does all it can so even the invalid input is preserved intact
838
+ and can be fed back to the form template so the user can fix it.
839
+ The fact that the form says the input is not valid doesn't mean you can't access it.
840
+ It's perfectly safe to render the form parameters back in the form template
841
+ and it is the intended use.
842
+ Just make sure you don't use the invalid input the way you normally would, that's all.
843
+
844
+ The input validation works by testing the current value of each parameter against
845
+ several validation criteria.
846
+ As soon as any of these validation restrictions is not met,
847
+ an error message describing the problem is reported and remembered for that parameter
848
+ and next parameter is tested.
849
+ Any parameter with an error message reported is considered invalid
850
+ for as long as the error message remains on record.
851
+ The entire form is considered invalid as long as any of its parameters are invalid.
852
+
853
+ It's important to realize that the input validation protection is
854
+ only as effective as the individual validation restrictions
855
+ you place on your parameters.
856
+ When defining your parameters,
857
+ always think of how you can restrict them.
858
+ It's always better to add too many restrictions than too little
859
+ and leave yourself open to exploits caused by unchecked input.
860
+
861
+ So, what kind of validations are available?
862
+ We have already discussed the required vs optional parameters.
863
+ The former are required to be present and non-empty.
864
+ Empty or `nil` parameter values are allowed only if the parameters are optional.
865
+ Unless it is `nil`, the value must also match the parameter kind (string, array or hash).
866
+ Note that the `FormInput` provides default error messages for any problems detected,
867
+ but you can set a custom error message for required parameters with the `:required_msg` option:
868
+
869
+ ``` ruby
870
+ param! :login, "Login Name",
871
+ required_msg: "Please fill in your Login Name"
872
+ ```
873
+
874
+ We have also discussed the string character and byte size limits,
875
+ which are controlled by `:min_size`, `:max_size`, `:min_bytesize`, and `:max_bytesize` options, respectively.
876
+ The array and hash parameters additionally support the `:min_count` and `:max_count` options,
877
+ which limit the number of elements.
878
+ The hash parameters also support the `:min_key` and `:max_key` limits to control the range of their integer keys,
879
+ plus the `:match_key` pattern(s) to enable restricted use of non-integer string keys.
880
+
881
+ What we haven't discussed yet are the `:min` and `:max` limits.
882
+ When used, these enforce that the input values are
883
+ not less than or greater than given limit, respectively.
884
+ Similarly, the `:inf` and `:sup` limits (from infimum and supremum)
885
+ ensure that the input values are
886
+ greater than and less than given limit, respectively.
887
+ Note that any of these work with both strings and Numeric types,
888
+ as well as anything which responds to the `to_f` method:
889
+
890
+ ``` ruby
891
+ param :age, INTEGER_ARGS, min: 1, max: 200 # 1 <= age <= 200
892
+ param :rate, FLOAT_ARGS, inf: 0, sup: 1 # 0 < rate < 1
893
+ ```
894
+
895
+ Additionally, you may specify a regular expression or an array of regular expressions
896
+ which the input values must match using the `:match` option.
897
+ If you intend to match the input in its entirety,
898
+ make sure you use the `\A` and `\z` anchors rather than `^` and `$`,
899
+ so a newline in the input doesn't let an unexpected input sneak in:
900
+
901
+ ``` ruby
902
+ param :nick, match: /\A[a-z]+\z/i
903
+ ```
904
+
905
+ Custom error message if the match fails can be set with the `:msg` or `:match_msg` options:
906
+
907
+ ``` ruby
908
+ param :password,
909
+ match: [ /[A-Z]/, /[a-z]/, /\d/ ],
910
+ msg: "Password must contain one lowercase and one uppercase letter and one digit"
911
+ ```
912
+
913
+ Similarly to `:match`, you may specify a regular expression or an array of regular expressions
914
+ which the input values may not match using the `:reject` option.
915
+ Custom error message if this fails can be set with the `:msg` or `:reject_msg` options:
916
+
917
+ ``` ruby
918
+ param :password,
919
+ reject: /\P{ASCII}|[\t\r\n]/u,
920
+ reject_msg: "Password may contain only ASCII characters and spaces",
921
+ ```
922
+
923
+ Of course, prior to all this, the `FormInput` also ensures
924
+ that the strings are in valid encoding and don't contain weird control characters,
925
+ so you don't have to worry about that at all.
926
+ Alternatively,
927
+ for parameters which use a custom object type instead of a string,
928
+ the `:class` option ensures that the object is of the correct type instead.
929
+
930
+ Now, any violation of these restrictions is automatically reported as an error.
931
+ Note that `FormInput` normally reports only the first error detected per parameter,
932
+ but you can report arbitrary number of custom errors for given parameter
933
+ using the `report` method.
934
+ This comes handy as it allows you to pass the form into your models
935
+ and let them report any belated additional errors which might get detected during the database transaction,
936
+ for example:
937
+
938
+ ``` ruby
939
+ form.report( :email, "Email is already taken" ) unless unique_email?( form.email )
940
+ ```
941
+
942
+ As we have already seen, it is common to use the `report`
943
+ method from within the `:check` or `:test` callback of the parameter itself as well:
944
+
945
+ ``` ruby
946
+ check: ->{ report( "%p is already taken" ) unless unique_email?( value ) }
947
+ ```
948
+
949
+ In this case the `%p` string is replaced by the `title` of the parameter.
950
+ If the parameter has the `:error_title` option set, it is used preferably instead.
951
+ If neither is set, it fallbacks to the parameter `code` name instead.
952
+
953
+ You can get hash of all errors reported for each parameter from the `errors` method,
954
+ or list consisting of first error message for each parameter from the `error_messages` method:
955
+
956
+ ``` ruby
957
+ form.errors # { email: [ "Email address is already taken" ] }
958
+ form.error_messages # [ "Email address is already taken" ]
959
+ ```
960
+
961
+ You can get all errors or first error for given parameter by using
962
+ the `errors_for` or `error_for` method, respectively:
963
+
964
+ ``` ruby
965
+ form.errors_for( :email ) # [ "Email address is already taken" ]
966
+ form.error_for( :email ) # "Email address is already taken"
967
+ ```
968
+
969
+ As we have seen, you can test the validity of the entire form with the `valid?` or `invalid?` methods.
970
+ You can use those methods for testing validity of given parameter or parameters, too:
971
+
972
+ ``` ruby
973
+ form.valid?
974
+ form.invalid?
975
+ form.valid?( :email )
976
+ form.invalid?( :name, :message )
977
+ form.valid?( form.required_params )
978
+ form.invalid?( form.hidden_params )
979
+ ```
980
+
981
+ The validation is run automatically when you first access any of
982
+ the validation related methods mentioned above,
983
+ so you don't have to worry about its invocation at all.
984
+ But you can also invoke it explicitly by calling `validate`, `validate?` or `validate!` methods.
985
+ The `validate` method is the standard variant which validates all parameters.
986
+ If any errors were reported before already, however, it leaves them intact.
987
+ The `validate?` method is a lazy variant which invokes the validation only if it was not invoked yet.
988
+ The `validate!` method on the other hand always invokes the validation,
989
+ wiping any previously reported errors first.
990
+
991
+ In either case any errors collected will remain stored
992
+ until you change any of the parameter values with `set`, `clear`, or `[]=` methods,
993
+ or you explicitly call `validate!`.
994
+ Copies created with `dup` (but not `clone`), `only`, and `except` methods
995
+ also have any errors reported before cleared.
996
+ All this ensures you automatically get consistent validation results anytime you ask for them.
997
+ The only exception is when you set the parameter values explicitly using their setter methods.
998
+ This intentionally leaves the errors reported intact,
999
+ allowing you to adjust the parameter values
1000
+ without interferring with the validation results.
1001
+ Which finally brings us to the topic of accessing the parameter values themselves.
1002
+
1003
+ ### Using Forms
1004
+
1005
+ So, now when you have verified that the input is valid,
1006
+ let's finally use it.
1007
+
1008
+ The `FormInput` classes use standard instance variables
1009
+ for keeping the parameter values,
1010
+ along with standard read and write accessors.
1011
+ The simplest way is thus to access the parameters by their name as usual:
1012
+
1013
+ ``` ruby
1014
+ form.email
1015
+ form.message ||= "Default text"
1016
+ ```
1017
+
1018
+ Note that the standard accessors are defined for you when you declare the parameter,
1019
+ but you are free to provide your own if you want to.
1020
+ For example, if you want a parameter to always have some default value instead of the default `nil`,
1021
+ this is the simplest way to do it:
1022
+
1023
+ ``` ruby
1024
+ param :sort_mode, :s
1025
+
1026
+ def sort_mode
1027
+ @sort_mode || 'default'
1028
+ end
1029
+ ```
1030
+
1031
+ Another way how to access the parameter values is to use the hash-like interface.
1032
+ Note that it can return an array of multiple attributes at once as well:
1033
+
1034
+ ``` ruby
1035
+ form[ :email ]
1036
+ form[ :name ] = user.name
1037
+ form[ :first_name, :last_name ]
1038
+ ```
1039
+
1040
+ Of course, this interface is often used when you need to access the parameter values programatically,
1041
+ without knowing their exact name.
1042
+ The form provides names of all parameters via its `params_names` or `parameters_names` methods,
1043
+ so you can do things like this:
1044
+
1045
+ ``` ruby
1046
+ form.params_names.each{ |name| puts "#{name}: #{form[ name ].inspect}" }
1047
+ ```
1048
+
1049
+ Sometimes, you may want to use some chosen parameters as long as they are all valid,
1050
+ even if the entire form may be not.
1051
+ You can do this by using the `valid` method,
1052
+ which returns the requested values only if they are all valid.
1053
+ Otherwise it returns `nil`.
1054
+
1055
+ ``` ruby
1056
+ return unless email = form.valid( :email )
1057
+ first_name, last_name = form.valid( :first_name, :last_name )
1058
+ ```
1059
+
1060
+ To find out if no parameter values are filled at all, you can use the `empty?` method:
1061
+
1062
+ ``` ruby
1063
+ form.set( email: user.email ) if form.empty?
1064
+ ```
1065
+
1066
+ The parameters are more than their value, though,
1067
+ so the `FormInput` allows you to access the parameters themselves as well.
1068
+ You can get a single named parameter from the `param` or `parameter` methods,
1069
+ list of named parameters from the `named_params` or `named_parameters` methods,
1070
+ or all parameters from the `params` or `parameters` methods,
1071
+ respectively:
1072
+
1073
+ ``` ruby
1074
+ p = form.param( :message )
1075
+ p1, p2 = form.named_params( :email, :name )
1076
+ list = form.params
1077
+ ```
1078
+
1079
+ Once you get hold of the parameter, you can query it about lot of things.
1080
+ First of all, you can ask it about its `name`, `code`, `title` or `value`:
1081
+
1082
+ ``` ruby
1083
+ p = form.params.first
1084
+ puts p.name
1085
+ puts p.code
1086
+ puts p.title
1087
+ puts p.value
1088
+ ```
1089
+
1090
+ All parameter options are available via its `opts` hash.
1091
+ However, it is preferable to query them via the `[]` operator,
1092
+ which also resolves the dynamic options
1093
+ and can support localized variants as well:
1094
+
1095
+ ``` ruby
1096
+ puts p[ :help ] # Use this ...
1097
+ puts p.opts[ :help ] # ... rather than this.
1098
+ ```
1099
+
1100
+ The parameter also knows about the form it belongs to,
1101
+ so you can get back to it using the `form` method if you need to:
1102
+
1103
+ ``` ruby
1104
+ fail unless p.form.valid?
1105
+ ```
1106
+
1107
+ As we have seen, you can report errors about the parameter using its `report` method.
1108
+ You can ask it about all its errors or just the first error using the `errors` or `error` methods, respectively:
1109
+
1110
+ ``` ruby
1111
+ p.report( "This is invalid" )
1112
+ p.errors # [ "This is invalid" ]
1113
+ p.error # "This is invalid"
1114
+ ```
1115
+
1116
+ You can also simply ask whether the parameter is valid or not by using the `valid?` and `invalid?` methods.
1117
+ In fact, the parameter has a dozen of simple boolean getters like this which you can use to ask it about many things:
1118
+
1119
+ ``` ruby
1120
+ p.valid? # Does the parameter have no errors reported?
1121
+ p.invalid? # Does the parameter have some errors reported?
1122
+
1123
+ p.blank? # Is the value nil or empty or whitespace only string?
1124
+ p.empty? # Is the value nil or empty?
1125
+ p.filled? # Is the value neither nil nor empty?
1126
+
1127
+ p.required? # Is the parameter required?
1128
+ p.optional? # Is the parameter not required?
1129
+
1130
+ p.disabled? # Is the parameter disabled?
1131
+ p.enabled? # Is the parameter not disabled?
1132
+
1133
+ p.hidden? # Is the parameter type :hidden?
1134
+ p.ignored? # Is the parameter type :ignore?
1135
+ p.visible? # Is the parameter type neither :hidden nor :ignore?
1136
+
1137
+ p.array? # Was the parameter declared as an array?
1138
+ p.hash? # Was the parameter declared as a hash?
1139
+ p.scalar? # Was the parameter declared as a simple param?
1140
+
1141
+ p.correct? # Does the value match param/array/hash kind?
1142
+ p.incorrect? # Doesn't the value match the parameter kind?
1143
+ ```
1144
+
1145
+ Building upon these boolean getters,
1146
+ the `FormInput` instance lets you get a list of parameters of certain type.
1147
+ The following methods are available:
1148
+
1149
+ ``` ruby
1150
+ form.valid_params # Parameters with no errors reported.
1151
+ form.invalid_params # Parameters with some errors reported.
1152
+ form.blank_params # Parameters with nil, empty, or blank value.
1153
+ form.empty_params # Parameters with nil or empty value.
1154
+ form.filled_params # Parameters with some non-empty value.
1155
+ form.required_params # Parameters which are required and have to be filled.
1156
+ form.optional_params # Parameters which are not required and can be nil or empty.
1157
+ form.disabled_params # Parameters which are disabled and shall be rendered as such.
1158
+ form.enabled_params # Parameters which are not disabled and are rendered normally.
1159
+ form.hidden_params # Parameters to be rendered as hidden in the form.
1160
+ form.ignored_params # Parameters not to be rendered at all in the form.
1161
+ form.visible_params # Parameters rendered normally in the form.
1162
+ form.array_params # Parameters declared as an array parameter.
1163
+ form.hash_params # Parameters declared as a hash parameter.
1164
+ form.scalar_params # Parameters declared as a simple scalar parameter.
1165
+ form.correct_params # Parameters whose current value matches their kind.
1166
+ form.incorrect_params # Parameters whose current value doesn't match their kind.
1167
+ ```
1168
+
1169
+ Each of them simply selects the paramaters using their boolean getter of the same name.
1170
+ Each of them is available in the `*_parameters` form for as well,
1171
+ for those who don't like the `params` shortcut.
1172
+
1173
+ As you can see, this allows you to get many parameter subsets,
1174
+ but sometimes even that is not enough.
1175
+ For this reason, parameters also support the so-called tagging,
1176
+ which allows you to group them by any additional criteria you need.
1177
+ Simply tag a parameter with one or more tags using either the `:tag` or `:tags` option:
1178
+
1179
+ ``` ruby
1180
+ param :age, tag: :indecent
1181
+ param :ratio, tags: [ :knob, :limited ]
1182
+ ```
1183
+
1184
+ Note that the parameter tags can be also generated dynamically the same way as any other option,
1185
+ but once accessed, their value is frozen for that parameter instance afterwards,
1186
+ both for performance reasons and to prevent their inconsistent changes.
1187
+
1188
+ You can ask the parameter for an array of its tags with the `tags` method.
1189
+ Note that it returns an empty array if the parameter was not tagged at all.
1190
+ Rather than using the tags array directly, though,
1191
+ it's easier to test parameter's tags using its `tagged?` and `untagged?` methods:
1192
+
1193
+ ``` ruby
1194
+ p.tagged? # Tagged with some tag?
1195
+ p.untagged? # Not tagged at all?
1196
+ p.tagged?( :indecent ) # Tagged with this tag?
1197
+ p.untagged?( :limited ) # Not tagged with this tag?
1198
+ p.tagged?( :indecent, :limited ) # Tagged with any of these tags?
1199
+ p.untagged?( :indecent, :limited ) # Not tagged with any of these tags?
1200
+ ```
1201
+
1202
+ You can get the desired parameters using the form's `tagged_params` and `untagged_params` methods, too:
1203
+
1204
+ ``` ruby
1205
+ form.tagged_params # Parameters with some tag.
1206
+ form.untagged_params # Parameters with no tag.
1207
+ form.tagged_params( :indecent ) # Parameters tagged with this tag.
1208
+ form.untagged_params( :limited ) # Parameters not tagged with this tag.
1209
+ form.tagged_params( :indecent, :limited ) # Parameters with either of these tags.
1210
+ form.untagged_params( :indecent, :limited ) # Parameters with neither of these tags.
1211
+ ```
1212
+
1213
+ What you use this for is up to you.
1214
+ We will see later that for example the [Multi-Step Forms](#multi-step-forms) use this for grouping parameters which belong to individual steps,
1215
+ but it has plenty other uses as well.
1216
+
1217
+ #### URL Helpers
1218
+
1219
+ The `FormInput` is primarily intended for use with HTML forms,
1220
+ which we will discuss in detail in the [Form Templates](#form-templates) chapter,
1221
+ but it can be used for processing any web request input,
1222
+ regardless of if it comes from a form post or from the URL query string.
1223
+ It is therefore quite natural that the `FormInput` provides helpers for generating
1224
+ URL query strings as well in addition to helpers used for form creation.
1225
+
1226
+ You can get a hash of filled parameters suitable for use in the URL query by using the `url_params` method,
1227
+ or get them combined into the URL query string by using the `url_query` method.
1228
+ Note that the `url_params` result differs considerably from the result of the `to_hash` method,
1229
+ as it uses parameter code rather than name for keys and their external representation for the values:
1230
+
1231
+ ``` ruby
1232
+ class MyInput < FormInput
1233
+ param :query, :q
1234
+ array :feeds, INTEGER_ARGS
1235
+ end
1236
+
1237
+ input = MyInput.new( query: "abc", feeds: [ 1, 7 ] )
1238
+
1239
+ input.to_hash # { query: "abc", feeds: [ 1, 7 ] }
1240
+ input.url_params # { q: "abc", feeds: [ "1", "7" ] }
1241
+ input.url_query # "q=abc&feeds[]=1&feeds[]=7"
1242
+ ```
1243
+
1244
+ Unless you want to construct the URL yourself,
1245
+ you can use the `extend_url` method to let the `FormInput` create the URL for you:
1246
+
1247
+ ``` ruby
1248
+ input.extend_url( "/search" ) # "/search?q=abc&feeds[]=1&feeds[]=7"
1249
+ input.extend_url( "/search?e=utf8" ) # "/search?e=utf8&q=abc&feeds[]=1&feeds[]=7"
1250
+ ```
1251
+
1252
+ Note that this works well together with the `only` and `except` methods,
1253
+ which allow you to control which arguments get included:
1254
+
1255
+ ``` ruby
1256
+ input.only( :query ).extend_url( "/search" ) # "/search?q=abc"
1257
+ input.except( :query ).extend_url( "/search" ) # "/search?feeds[]=1&feeds[]=7"
1258
+ ```
1259
+
1260
+ You can use this for example to create an URL suitable for redirection
1261
+ which retains only valid parameters when some parameters are not valid:
1262
+
1263
+ ``` ruby
1264
+ # In your class:
1265
+ def valid_url( url )
1266
+ only( valid_params ).extend_url( url )
1267
+ end
1268
+
1269
+ # In your route handler:
1270
+ input = MyInput.new( request )
1271
+ redirect input.valid_url( request.path ) unless input.valid?
1272
+ ```
1273
+
1274
+ If you want to temporarily adjust some parameters just for the creation of a single URL,
1275
+ you can use the `build_url` method, which combines current parameters with the provided ones:
1276
+
1277
+ ``` ruby
1278
+ input.build_url( "/search", query: "xyz" ) # "/search?q=xyz&feeds[]=1&feeds[]=7"
1279
+ ```
1280
+
1281
+ Finally, if you do not like the idea of parameter arrays in your URLs, you can use something like this instead:
1282
+
1283
+ ``` ruby
1284
+ param :feeds,
1285
+ filter: ->{ split.map( &:to_i ) },
1286
+ format: ->{ join( ' ' ) },
1287
+ class: Array
1288
+
1289
+ input.url_params # { q: "abc", feeds: "1 7" }
1290
+ input.url_query # "q=abc&feeds=1+7"
1291
+ ```
1292
+
1293
+ Just note that none of the standard array parameter validations apply in this case,
1294
+ so make sure to apply your own validations using the `:check` callback if you need to.
1295
+
1296
+ #### Form Helpers
1297
+
1298
+ It may come as a surprise, but `FormInput` provides no helpers for creating HTML tags.
1299
+ That's because doing so would be a completely futile effort.
1300
+ No tag helper will suit all your needs when it comes to form creation.
1301
+
1302
+ Instead, `FormInput` provides several helpers
1303
+ which allow you to easily create the forms in the templating engine of your choice.
1304
+ This has many advantages.
1305
+ In particular, it allows you to nest the HTML tags exactly the way you want,
1306
+ style it using whatever classes you want, and include any extra bits the way you want.
1307
+ Furthermore, it allows you to have templates for rendering the parameters in several styles
1308
+ and choose among them as you need.
1309
+ All this and more will be discussed in detail in the [Form Templates](#form-templates) chapter, though.
1310
+ This chapter just describes the form helpers themselves.
1311
+
1312
+ You can ask each form parameter about how it should be rendered by using its `type` method,
1313
+ which defaults to `:text` if the option `:type` is not set.
1314
+ Furthermore,
1315
+ you can ask each form parameter for the appropriate name and value attributes
1316
+ to use in form elements by using the `form_name` and `form_value` methods.
1317
+ The simplest form parameters can be thus rendered in [Slim] like this:
1318
+
1319
+ ``` slim
1320
+ input type=p.type name=p.form_name value=p.form_value
1321
+ ```
1322
+
1323
+ For array parameters, the `form_value` returns an array of values,
1324
+ so it is rendered like this:
1325
+
1326
+ ``` slim
1327
+ - for value in p.form_value
1328
+ input type=p.type name=p.form_name value=value
1329
+ ```
1330
+
1331
+ Finally, for hash parameters, the `form_value` returns an array of keys and values,
1332
+ and keys are passed to `form_name` to create the actual name:
1333
+
1334
+ ``` slim
1335
+ - for key, value in p.form_value
1336
+ input type=p.type name=p.form_name( key ) value=value
1337
+ ```
1338
+
1339
+ For parameters which require additional data,
1340
+ like select, multi-select, or multi-checkbox parameters,
1341
+ you can ask for the data using the `data` method.
1342
+ It returns pairs of allowed parameter values together with their names.
1343
+ If the `:data` option is not set, it returns an empty array.
1344
+ The values can be passed to the `selected?` method to test if they are currently selected,
1345
+ and then must be passed to the `format_value` method to turn them into their external representation.
1346
+ To illustrate all this, a select parameter can be rendered like this:
1347
+
1348
+ ``` slim
1349
+ select name=p.form_name multiple=p.array?
1350
+ - for value, name in p.data
1351
+ option selected=p.selected?( value ) value=p.format_value( value ) = name
1352
+ ```
1353
+
1354
+ Finally, you will likely want to render the parameter name in some way.
1355
+ For this, each parameter has the `form_title` method,
1356
+ which returns the title to show in the form.
1357
+ It defaults to its title, but can be overriden with the `:form_title` option.
1358
+ If neither is set, the code name will be used instead.
1359
+ To render it, you will use something like this:
1360
+
1361
+ ``` slim
1362
+ label
1363
+ = p.form_title
1364
+ input type=p.type name=p.form_name value=p.form_value
1365
+ ```
1366
+
1367
+ Of course, you are free to use any other parameter methods as well.
1368
+ Want to render the parameter disabled?
1369
+ Add some placeholder text?
1370
+ It's as simple as adding this to your template:
1371
+
1372
+ ``` slim
1373
+ input ... disabled=p.disabled? placeholder=p[:placeholder]
1374
+ ```
1375
+
1376
+ And that's about it.
1377
+ Check out the [Form Templates](#form-templates) chapter if you want to see more form related tips and tricks.
1378
+
1379
+ ### Extending Forms
1380
+
1381
+ While the `FormInput` comes with a lot of functionality built in,
1382
+ you will eventually want to extend it further to better fit your project.
1383
+ To do this, it's common to define the `Form` class inherited from `FormInput`,
1384
+ put various helpers there, and base your own forms on that.
1385
+ This is also the place where you can include your own `FormInput` types extensions.
1386
+ This chapter merely shows some ideas you may want to built upon to get you started.
1387
+
1388
+ Adding custom boolean getters which you may need:
1389
+
1390
+ ``` ruby
1391
+ # Test if the form input which the user can't fix is malformed.
1392
+ def malformed?
1393
+ invalid?( hidden_params )
1394
+ end
1395
+ ```
1396
+
1397
+ Adding custom URL helpers, see [URL Helpers](#url-helpers) for details:
1398
+
1399
+ ``` ruby
1400
+ # Add valid parameters to given URL.
1401
+ def valid_url( url )
1402
+ only( valid_params ).extend_url( url )
1403
+ end
1404
+ ```
1405
+
1406
+ Keeping track of additional form state:
1407
+
1408
+ ``` ruby
1409
+ # Hook into the request import so we can test form posting.
1410
+ def import( request )
1411
+ @posted ||= request.respond_to?( :post? ) && request.post?
1412
+ super
1413
+ end
1414
+
1415
+ # Test if the form content was posted with a post request.
1416
+ def posted?
1417
+ @posted
1418
+ end
1419
+
1420
+ # Explicitly mark the form as posted, to enforce the post action to be taken.
1421
+ # Returns self for chaining.
1422
+ def posted!
1423
+ @posted = true
1424
+ self
1425
+ end
1426
+ ```
1427
+
1428
+ The list could go on and on, as everyone might need slightly different tweaks.
1429
+ Eventually, though, you will come up with your own set of extensions which you will keep using across projects.
1430
+ Once you do, consider sharing them with the rest of the world. Thanks.
1431
+
1432
+ ### Parameter Options
1433
+
1434
+ This is a brief but comprehensive summary of all parameter options:
1435
+
1436
+ * `:name` - not really a parameter option, this can be used to change the parameter name and code name when copying form parameters.
1437
+ See [Reusing Form Parameters](#reusing-form-parameters).
1438
+ * `:code` - not really a parameter option, this can be used to change the parameter code name when copying form parameters.
1439
+ See [Reusing Form Parameters](#reusing-form-parameters).
1440
+ * `:title` - the title of the parameter, the default value shown in forms and error messages.
1441
+ * `:form_title` - the title of the parameter to show in forms. Overrides `:title` when present.
1442
+ * `:error_title` - the title of the parameter to use in error messages containing `%p`. Overrides `:title` when present.
1443
+ * `:required` - flag set when the parameter is required.
1444
+ * `:required_msg` - custom error message used when the required parameter is not filled in.
1445
+ Default error message is used if not set.
1446
+ * `:disabled` - flag set when the parameter shall be rendered as disabled.
1447
+ Note that it doesn't affect it in any other way, in particular it doesn't prevent it from being set or being invalid.
1448
+ * `:array` - flag set for array parameters.
1449
+ * `:hash` - flag set for hash parameters.
1450
+ * `:type` - type of the form parameter used for form rendering. Defaults to `:text` if not set.
1451
+ Other common values are `:password`, `:textarea`, `:select`, `:checkbox`, `:radio`.
1452
+ Somewhat special values are `:hidden` and `:ignore`.
1453
+ * `:data` - array containing data for parameter types which need one, like select, multi-select, or multi-checkbox.
1454
+ Shall contain the allowed parameter values paired with the corresponding text to display in forms.
1455
+ Defaults to empty array if not set.
1456
+ * `:tag` or `:tags` - arbitrary symbol or array of symbols used to tag the parameter with arbitrary semtantics.
1457
+ See `tagged?` method in the [Using Forms](#using-forms) chapter.
1458
+ * `:filter` - callback used to cleanup or convert the input values.
1459
+ See [Input Filter](#input-filter).
1460
+ * `:transform` - optional callback used to further convert the input values.
1461
+ See [Input Transform](#input-transform).
1462
+ * `:format` - optional callback used to format the output values.
1463
+ See [Output Format](#output-format).
1464
+ * `:class` - object type (or array thereof) which the input filter is expected to convert the input value into.
1465
+ See [Input Filter](#input-filter).
1466
+ * `:check` - optional callback used to perform arbitrary checks when testing the parameter validity.
1467
+ See [Errors and Validation](#errors-and-validation).
1468
+ * `:test` - optional callback used to perform arbitrary tests when testing validity of each parameter value.
1469
+ See [Errors and Validation](#errors-and-validation).
1470
+ * `:min_key` - minimum allowed value for keys of hash parameters. Defaults to 0.
1471
+ * `:max_key` - maximum allowed value for keys of hash parameters. Defaults to 2<sup>64</sup>-1.
1472
+ * `:match_key` - regular expression (or array thereof) which all hash keys must match. Disabled by default.
1473
+ * `:min_count` - minimum allowed number of elements for array or hash parameters.
1474
+ * `:max_count` - maximum allowed number of elements for array or hash parameters.
1475
+ * `:min` - when set, value(s) of that parameter must be greater than or equal to this.
1476
+ * `:max` - when set, value(s) of that parameter must be less than or equal to this.
1477
+ * `:inf` - when set, value(s) of that parameter must be greater than this.
1478
+ * `:sup` - when set, value(s) of that parameter must be less than this.
1479
+ * `:min_size` - when set, value(s) of that parameter must have at least this many characters.
1480
+ * `:max_size` - when set, value(s) of that parameter may have at most this many characters.
1481
+ Defaults to 255.
1482
+ * `:min_bytesize` - when set, value(s) of that parameter must have at least this many bytes.
1483
+ * `:max_bytesize` - when set, value(s) of that parameter may have at most this many bytes.
1484
+ Defaults to 255 if `:max_size` is dynamic option or less than 256.
1485
+ * `:reject` - regular expression (or array thereof) which the parameter value(s) may not match.
1486
+ * `:reject_msg` - custom error message used when the `:reject` check fails.
1487
+ Defaults to `:msg` message.
1488
+ * `:match` - regular expression (or array thereof) which the parameter value(s) must match.
1489
+ * `:match_msg` - custom error message used when the `:match` check fails.
1490
+ Defaults to `:msg` message.
1491
+ * `:msg` - default custom error message used when either of `:match` or `:reject` checks fails.
1492
+ Default error message is used if not set.
1493
+ * `:inflect` - explicit inflection string used for localization.
1494
+ Defaults to combination of `:plural` and `:gender` options,
1495
+ see [Localization](#localization) for details.
1496
+ * `:plural` - explicit grammatical number used for localization.
1497
+ See [Localization](#localization) for details.
1498
+ Defaults to `false` for scalar parameters and to `true` for array and hash parameters.
1499
+ * `:gender` - grammatical gender used for localization.
1500
+ See [Localization](#localization) for details.
1501
+ * `:row` - used for grouping several parameters together, usually to render them in a single row.
1502
+ See [Chunked parameters](#chunked-parameters) chapter.
1503
+ * `:cols` - optional custom option used to set span of the parameter in a single row.
1504
+ See [Chunked parameters](#chunked-parameters).
1505
+ * `:group` - custom option used for grouping parameters in arbitrary ways.
1506
+ See [Grouped parameters](#grouped-parameters).
1507
+ * `:size` - custom option used to set size of `:select` and `:textarea` parameters.
1508
+ * `:subtitle` - custom option used to render an additional subtitle after the form title.
1509
+ * `:placeholder` - custom option used for setting the placeholder attribute of the parameter.
1510
+ * `:help` - custom option used to render a help block explaining the parameter.
1511
+ * `:text` - custom option used to render an arbitrary text associated with the parameter.
1512
+
1513
+ Note that the last few options listed above are not used by the `FormInput` class itself,
1514
+ but are instead used to pass additional data to snippets used for form rendering.
1515
+ Feel free to extend this further if you need to pass additional data this way yourself.
1516
+
1517
+ ## Form Templates
1518
+
1519
+ The `FormInput` form rendering is based on the power of standard templates,
1520
+ the same ones which are used for page rendering.
1521
+ It builds upon the set of form helpers described in the [Form Helpers](#form-helpers) chapter.
1522
+ This chapter shows several typical form templates and how they are supposed to be created and used.
1523
+
1524
+ First of all, the templates are based on the concept of _snippets_,
1525
+ which allows the individual template pieces to be reused at will.
1526
+ Chances are your framework already has support for snippets -
1527
+ if not, it's usually trivial to build it upon the provided template rendering functionality.
1528
+ For example, this is a `snippet` helper based on [Sinatra]'s partials:
1529
+
1530
+ ``` ruby
1531
+ # Render partial, our style.
1532
+ def snippet( name, opts = {}, locals = nil )
1533
+ opts, locals = {}, opts unless locals
1534
+ partial( "snippets/#{name}", opts.merge( locals: locals ) )
1535
+ end
1536
+ ```
1537
+
1538
+ And here is the same thing for [Ramaze]:
1539
+
1540
+ ``` ruby
1541
+ # Render partial, our way.
1542
+ def snippet( name, *args )
1543
+ render_partial( "snippets/#{name}", *args )
1544
+ end
1545
+ ```
1546
+
1547
+ Whatever you decide to use, the following examples will simply assume that
1548
+ the `snippet` method renders the specified template,
1549
+ while making the optionally provided hash of values accessible as local variables in that template.
1550
+ We will use [Slim] templates in these examples,
1551
+ but you could use the same principles in [HAML] or any other templating engine as well.
1552
+ Also note that you can find the example templates discussed here in the `form_input/example/views` directory.
1553
+
1554
+ ### Form Template
1555
+
1556
+ To put the form on a page, you use the stock HTML `form` tag.
1557
+ The snippets will be used for rendering the form content,
1558
+ but the form itself and the submission buttons used are usually form specific anyway,
1559
+ so it is rarely worth factoring it out.
1560
+ Assuming the controller passes the form to the view in the `@form` variable,
1561
+ simple form using standard [Bootstrap] styling could look like this:
1562
+
1563
+ ``` slim
1564
+ form *form_attrs
1565
+ fieldset
1566
+ == snippet :form_simple, params: @form.params
1567
+ button.btn.btn-default type='submit' Submit
1568
+ ```
1569
+
1570
+ As you can see, the whole form is just a little bit of scaffolding,
1571
+ with the bulk rendered by the `form_simple` snippet.
1572
+ Choosing different snippets allows us to render the form content in different styles easily.
1573
+ In this case, we are passing in all form parameters as they are,
1574
+ but note that we could as easily split them or filter them as needed
1575
+ and render each group differently if necessary.
1576
+
1577
+ Note that we are also using the `form_attrs` helper to set the `action` and `method` tag attributes to their default values.
1578
+ It's a recommended practice to set these explicitly,
1579
+ so we may as well use a helper which does this consistently everywhere.
1580
+ For [Sinatra], the helper may look like this:
1581
+
1582
+ ``` ruby
1583
+ # Get hash with default form attributes, optionally overriding them as needed.
1584
+ def form_attrs( *args )
1585
+ opts = args.last.is_a?( Hash ) ? args.pop : {}
1586
+ url = args.shift.to_s unless args.empty?
1587
+ fail( ArgumentError, "Invalid arguments #{args.inspect}" ) unless args.empty?
1588
+ { action: url || request.path, method: :post }.merge( opts )
1589
+ end
1590
+ ```
1591
+
1592
+ If you want to use the CSRF protection provided by `Rack::Protection`,
1593
+ note that you will need to add something like this to the form fieldset:
1594
+
1595
+ ``` slim
1596
+ input type='hidden' name='authenticity_token' value=session[:csrf]
1597
+ ```
1598
+
1599
+ To save some typing and to keep things [DRY],
1600
+ you may turn this into a `form_token` helper and call that instead.
1601
+ Just make sure the token value is properly HTML escaped.
1602
+
1603
+ ### Simple Parameters
1604
+
1605
+ Now let's have a look at the snippet rendering the form parameters.
1606
+ It obviously needs to get the list of the parameters to render.
1607
+ We will pass them in in the `params` variable.
1608
+ For convenience, we will treat `nil` as an empty list, too.
1609
+
1610
+ In addition to that, you will want to control if the errors should be displayed or not.
1611
+ When the form is displayed for the first time, before the user posts anything,
1612
+ no errors should be displayed,
1613
+ but you may want to suppress it explicitly in other cases as well.
1614
+ We will control this by using the `report` variable.
1615
+ For convenience, we will provide reasonable default which automatically asks the current request if it was posted or not.
1616
+
1617
+ Finally, you will likely want some control over the form autofocus.
1618
+ For maximum control, we will allow passing in the parameter to focus on in the `focused` variable.
1619
+ For convenience, though, we will by default autofocus on the first invalid or unfilled parameter,
1620
+ unless focusing is explicitly disabled by setting `focus` to false.
1621
+
1622
+ The snippet prologue which does all this may look like this:
1623
+
1624
+ ``` slim
1625
+ - params ||= []
1626
+ - focus ||= focus.nil?
1627
+ - report ||= report.nil? && request.post?
1628
+ - focused ||= params.find{ |x| x.invalid? } || params.find{ |x| x.blank? } || params.first if focus
1629
+ ```
1630
+
1631
+ To demonstrate the basics, we will render only the simple scalar parameters.
1632
+ As for styling, we will choose a simple [Bootstrap] block style,
1633
+ with the parameter name rendered within the input field itself
1634
+ using the `placeholder` attribute.
1635
+ This is something you can often see on compact login pages,
1636
+ even though it's a practice which is not really [ARIA] friendly.
1637
+ But as an example it illustrates the possibility to tweak the rendering any way you see fit just fine:
1638
+
1639
+ ``` slim
1640
+ - for p in params
1641
+ - case p.type
1642
+ - when :ignore
1643
+ - when :hidden
1644
+ input type='hidden' name=p.form_name value=p.form_value
1645
+ - else
1646
+ .form-group
1647
+ input.form-control[
1648
+ type=p.type
1649
+ name=p.form_name
1650
+ value=p.form_value
1651
+ placeholder=p.form_title
1652
+ disabled=p.disabled?
1653
+ autofocus=(p == focused)
1654
+ ]
1655
+ - if report and error = p.error
1656
+ .help-block
1657
+ span.text-danger = error
1658
+ ```
1659
+
1660
+ As you can see, the snippet uses the `type` attribute to distinguish between the ignored, hidden, and visible parameters.
1661
+ In further chapters we will see how this can be used to add support for rendering of other parameter types,
1662
+ like check boxes or pull down menus.
1663
+ But first we will explore how to properly render array or hash parameters.
1664
+
1665
+ ### Hidden Parameters
1666
+
1667
+ The `FormInput` supports more than scalar parameter types.
1668
+ As described in the [Array and Hash Parameters](#array-and-hash-parameters) chapter,
1669
+ the parameters can also contain data stored as arrays or hashes.
1670
+ This chapter shows how to render such parameters properly.
1671
+
1672
+ To focus on the basics, without any complexities getting in the way,
1673
+ we will use a snippet rendering all parameters as hidden ones as an example.
1674
+ This is something which is used fairly often,
1675
+ basically whenever you need to pass some data along within the form without the user seeing them.
1676
+ The [Rendering Multi-Step Forms](#rendering-multi-step-forms) chapter is a nice example which builds upon this.
1677
+
1678
+ The prologue of this snippet is simple, as we need no error reporting nor autofocus handling:
1679
+
1680
+ ``` slim
1681
+ - params ||= []
1682
+ ```
1683
+
1684
+ The rendering itself is pretty simple as well.
1685
+ It is free of any styling,
1686
+ it's just the basic use of parameter's rendering methods as described in the [Form Helpers](#form-helpers) chapter.
1687
+ You may want to review it after you have seen them used in some context:
1688
+
1689
+ ``` slim
1690
+ - for p in params
1691
+ - next if p.ignored?
1692
+ - next unless p.filled?
1693
+ - if p.array?
1694
+ - for value in p.form_value
1695
+ input type='hidden' name=p.form_name value=value
1696
+ - elsif p.hash?
1697
+ - for key, value in p.form_value
1698
+ input type='hidden' name=p.form_name(key) value=value
1699
+ - else
1700
+ input type='hidden' name=p.form_name value=p.form_value
1701
+ ```
1702
+
1703
+ Having seen the basics, we are now ready to start expanding them towards more complex snippets.
1704
+
1705
+ ### Complex parameters
1706
+
1707
+ Forms are often more than just few simple text input fields,
1708
+ so it is necessary to render more complex parameters as well.
1709
+ To do that,
1710
+ we will be basically adding code to the `p.type` switch of the following rendering loop:
1711
+
1712
+ ``` slim
1713
+ - for p in params
1714
+ - next if p.ignored?
1715
+ - if p.hidden?
1716
+ == snippet :form_hidden, params: [p]
1717
+ - else
1718
+ .form-group
1719
+ - case p.type
1720
+ - when ...
1721
+ - else
1722
+ label
1723
+ = p.form_title
1724
+ input.form-control[
1725
+ type=p.type
1726
+ name=p.form_name
1727
+ value=p.form_value
1728
+ autofocus=(p == focused)
1729
+ disabled=p.disabled?
1730
+ placeholder=p[:placeholder]
1731
+ ]
1732
+ - if report and error = p.error
1733
+ .help-block
1734
+ span.text-danger = error
1735
+ ```
1736
+
1737
+ Note how it reuses the snippet we have just described in [Hidden Parameters](#hidden-parameters)
1738
+ to render all kinds of hidden parameters.
1739
+ Other than that, however, as it is, the loop renders just normal input fields, like `:text` or `:password`.
1740
+ So let's extend it right now.
1741
+
1742
+ #### Text Area
1743
+
1744
+ Text area is basically just a larger text input field with multiline support.
1745
+
1746
+ ``` slim
1747
+ - when :textarea
1748
+ label
1749
+ = p.form_title
1750
+ textarea.form-control[
1751
+ name=p.form_name
1752
+ autofocus=(p == focused)
1753
+ disabled=p.disabled?
1754
+ rows=p[:size]
1755
+ ] = p.form_value
1756
+ ```
1757
+
1758
+ Note how we use the `:size` option to control the size of the area.
1759
+
1760
+ #### Select and Multi-Select
1761
+
1762
+ Select and multi-select allow choosing one or many items from a list of options, respectively.
1763
+ They are rendered the same way, the only difference is the `multiple` tag attribute.
1764
+ Thanks to this we can choose between them easily -
1765
+ we use normal select for scalar parameters and multi-select for array parameters:
1766
+
1767
+ ``` slim
1768
+ - when :select
1769
+ label
1770
+ = p.form_title
1771
+ select.form-control[
1772
+ name=p.form_name
1773
+ multiple=p.array?
1774
+ autofocus=(p == focused)
1775
+ disabled=p.disabled?
1776
+ size=p[:size]
1777
+ ]
1778
+ - for value, name in p.data
1779
+ option selected=p.selected?(value) value=p.format_value(value) = name
1780
+ ```
1781
+
1782
+ The data to render comes from the `data` attribute of the parameter,
1783
+ see the [Array and Hash Parameters](#array-and-hash-parameters) chapter for details.
1784
+ Also note how the value is passed to the `selected?` method
1785
+ and how it is formated by the `format_value` method.
1786
+
1787
+ #### Radio Buttons
1788
+
1789
+ Radio buttons are for choosing one item from a list of options.
1790
+ In this regard they are similar to select parameters,
1791
+ just their appearance in the form is different:
1792
+
1793
+ ``` slim
1794
+ - when :radio
1795
+ = p.form_title
1796
+ - for value, name in p.data
1797
+ label
1798
+ input.form-control[
1799
+ type=p.type
1800
+ name=p.form_name
1801
+ value=p.format_value(value)
1802
+ autofocus=(p == focused)
1803
+ disabled=p.disabled?
1804
+ checked=p.selected?(value)
1805
+ ]
1806
+ = name
1807
+ ```
1808
+
1809
+ Like in case of select parameters,
1810
+ the data to render comes from the `data` attribute.
1811
+ The `selected?` and `format_value` methods are used the same way, too.
1812
+
1813
+ #### Checkboxes
1814
+
1815
+ Checkboxes can be used in two ways.
1816
+ You can either use them as individual on/off checkboxes,
1817
+ or use them as an alternative to multiselect.
1818
+ Their rendering follows this -
1819
+ we use the on/off approach for scalar parameters,
1820
+ and the multiselect one for array parameters:
1821
+
1822
+ ``` slim
1823
+ - when :checkbox
1824
+ - if p.array?
1825
+ = p.form_title
1826
+ - for value, name in p.data
1827
+ label
1828
+ input.form-control[
1829
+ type=p.type
1830
+ name=p.form_name
1831
+ value=p.format_value(value)
1832
+ autofocus=(p == focused)
1833
+ disabled=p.disabled?
1834
+ checked=p.selected?(value)
1835
+ ]
1836
+ = name
1837
+ - else
1838
+ label
1839
+ - if p.title
1840
+ = p.form_title
1841
+ input.form-control[
1842
+ type=p.type
1843
+ name=p.form_name
1844
+ value='true'
1845
+ autofocus=(p == focused)
1846
+ disabled=p.disabled?
1847
+ checked=p.value
1848
+ ]
1849
+ = p[:text]
1850
+ ```
1851
+
1852
+ As you can see, the multiselect case is basically identical to rendering of radio buttons,
1853
+ only the input type attribute changes.
1854
+ For on/off checkboxes, though, there are more changes.
1855
+
1856
+ First of all, you often want no title displayed in front of them,
1857
+ so we don't show the title if it is not explicitly set.
1858
+ Of course, this can be applied to rendering of all parameters,
1859
+ but here it is particularly useful.
1860
+
1861
+ Second, you often want some text displayed after them,
1862
+ like something you are agreeing to, or whatever.
1863
+ So we use the `:text` option of the parameter to pass this text along.
1864
+ Note that it is not limited to static text either -
1865
+ like any other parameter option it can be evaluated at runtime if needed,
1866
+ see [Defining Parameters](#defining-parameters) for details.
1867
+ And the [Localization](#localization) chapter will explain how to get the text localized easily.
1868
+
1869
+ ### Inflatable parameters
1870
+
1871
+ We have seen how to render array parameters as multi-select or multi-checkbox fields.
1872
+ Sometimes, however, you really want to render them as an array of text input fields.
1873
+ One way to do this is to render all current values,
1874
+ plus one extra field for adding new value,
1875
+ like this:
1876
+
1877
+ ``` slim
1878
+ label
1879
+ = p.form_title
1880
+ - if p.array?
1881
+ - values = p.form_value
1882
+ - for value in values
1883
+ .form-group
1884
+ input.form-control[
1885
+ type=p.type
1886
+ name=p.form_name
1887
+ value=value
1888
+ autofocus=(p.invalid? and p == focused)
1889
+ disabled=p.disabled?
1890
+ ]
1891
+ - unless limit = p[:max_count] and values.count >= limit
1892
+ input.form-control[
1893
+ type=p.type
1894
+ name=p.form_name
1895
+ autofocus=(p.valid? and p == focused)
1896
+ disabled=p.disabled?
1897
+ placeholder=p[:placeholder]
1898
+ ]
1899
+ - else
1900
+ // standard scalar parameter rendering goes here...
1901
+ ```
1902
+
1903
+ Note that if you use something like this,
1904
+ it makes sense to also add an extra submit button to the form
1905
+ which will just update the form,
1906
+ in addition to the standard submit button.
1907
+ This button will allow the user to add as many items as needed before submitting the form.
1908
+
1909
+ ### Extending parameters
1910
+
1911
+ The examples above show rendering of parameters which shall take care of most your needs,
1912
+ but it doesn't need to end there. Do you need some extra functionality?
1913
+ It's trivial to pass additional parameter options along and
1914
+ render them in the template the way you need.
1915
+ This chapter will show few examples of what can be done.
1916
+
1917
+ Do you need to render a subtitle for some parameters?
1918
+ Just add it after the title like this:
1919
+
1920
+ ``` slim
1921
+ = p.form_title
1922
+ - if subtitle = p[:subtitle]
1923
+ small =< subtitle
1924
+ ```
1925
+
1926
+ Do you want to render an extra help text?
1927
+ Add it after error reporting like this:
1928
+
1929
+ ``` slim
1930
+ - if report and error = p.error
1931
+ .help-block
1932
+ span.text-danger = error
1933
+ - if help = p[:help]
1934
+ .help-block = help
1935
+ ```
1936
+
1937
+ Do you want to render all required parameters as such automatically?
1938
+ Let the snippet add the necessary CSS class like this:
1939
+
1940
+ ``` slim
1941
+ input ... class=(:required if p.required?)
1942
+ ```
1943
+
1944
+ Do you want to disable autocomplete for some input fields?
1945
+ You can do it like this:
1946
+
1947
+ ``` slim
1948
+ input ... autocomplete=(:off if p[:autocomplete] == false)
1949
+ ```
1950
+
1951
+ Do you want to add arbitrary tag attributes to the input element, for example some data attributes?
1952
+ You can put them in a hash and add them using a splat operator like this:
1953
+
1954
+ ``` slim
1955
+ input ... *(p[:tag_attrs] || {})
1956
+ ```
1957
+
1958
+ And so on and on.
1959
+ As you can see, the templates make it really easy to adjust them any way you may need.
1960
+
1961
+ ### Grouped parameters
1962
+
1963
+ Sometimes you may want to group some parameters together and render the groups accordingly,
1964
+ say in their own subframe.
1965
+ It's easy to create a template snippet which does that:
1966
+
1967
+ ``` slim
1968
+ - params ||= []
1969
+ - focus ||= focus.nil?
1970
+ - report ||= report.nil? && request.post?
1971
+ - focused ||= params.find{ |x| x.invalid? } || params.find{ |x| x.blank? } || params.first if focus
1972
+
1973
+ - for group, params in params.group_by{ |x| x[:group] || x.name }
1974
+ h2 = form.group_name( group )
1975
+ .form-frame
1976
+ == snippet :form_standard, params: params, focus: focus, focused: focused, report: report
1977
+ ```
1978
+
1979
+ Note that it uses our standard prologue for focusing and error reporting and then passes those values along,
1980
+ so the autofocused parameter is selected correctly no matter what group it belongs to.
1981
+
1982
+ The example above uses the `group_name` method which is supposed to return the name for given group.
1983
+ You would have to provide that,
1984
+ but check out the [Localization Helpers](#localization-helpers) chapter for a tip how it can be done easily.
1985
+
1986
+ ### Chunked parameters
1987
+
1988
+ Occasionally,
1989
+ you may want to render some input fields on the same line.
1990
+ That's when the support for parameter chunking becomes handy.
1991
+
1992
+ When declaring the form, add the `:row` option to those parameters.
1993
+ The value can be anything you want - equal values mean the parameters should be rendered together on the same line.
1994
+ The `:cols` option can be used if you want specific span of those parameters
1995
+ in the 12 column grid instead of an evenly distributed split:
1996
+
1997
+ ``` ruby
1998
+ param :nick, "Nick", 32,
1999
+ row: 1, cols: 5
2000
+ param :email, "Email", EMAIL_ARGS,
2001
+ row: 1, cols: 7
2002
+ ```
2003
+
2004
+ To enable the chunking itself,
2005
+ use the `chunked_params` form method instead of the `params` method
2006
+ to group the parameters appropriately and pass them to the rendering snippet:
2007
+
2008
+ ``` slim
2009
+ form *form_attrs
2010
+ fieldset
2011
+ == snippet :form_chunked, params: @form.chunked_params
2012
+ button.btn.btn-default type='submit' Submit
2013
+ ```
2014
+
2015
+ The `chunked_params` method leaves all single parameters intact,
2016
+ but puts all those which belong together into an subarray.
2017
+
2018
+ Using for example [Bootstrap] and its standard 12 column grid,
2019
+ the rendering snippet itself can look like this:
2020
+
2021
+ ``` slim
2022
+ - chunked = params || []
2023
+ - params = chunked.flatten
2024
+ - focus ||= focus.nil?
2025
+ - report ||= report.nil? && request.post?
2026
+ - focused ||= params.find{ |x| x.invalid? } || params.find{ |x| x.blank? } || params.first if focus
2027
+
2028
+ - for p in chunked
2029
+ - if p.is_a?(Array)
2030
+ .row
2031
+ - for param in p
2032
+ div class="col-sm-#{param[:cols] || 12 / p.count}"
2033
+ == snippet :form_standard, params: [param], focus: focus, focused: focused, report: report
2034
+ - else
2035
+ == snippet :form_standard, params: [p], focus: focus, focused: focused, report: report
2036
+ ```
2037
+
2038
+ This example shows a separate snippet which is built on top of another rendering snippets.
2039
+ But note that the chunking functionality can be incorporated directly into all the other snippets presented so far.
2040
+ Also note that it works even if you use other means to create the subarrays of parameters to put on the same line.
2041
+ Again, you are welcome to experiment and tweak the snippets any way you like so they work exactly the way you want them to.
2042
+
2043
+ ## Multi-Step Forms
2044
+
2045
+ You have seen them for sure.
2046
+ Multi-step forms.
2047
+ Most shopping sites use them during the checkout.
2048
+ You confirm the items on one page,
2049
+ fill delivery information on another,
2050
+ add payment details on the next
2051
+ and finally confirm it all on the last one.
2052
+ Normally, these can be quite a chore to implement,
2053
+ but `FormInput` makes it all fairly easy.
2054
+
2055
+ The following chapters will describe
2056
+ how to define, use and render such forms in detail.
2057
+
2058
+ ### Defining Multi-Step Forms
2059
+
2060
+ Defining multi-step form is about as simple as defining normal form.
2061
+ The only difference is that you decide what steps you want,
2062
+ define them with the `define_steps` method,
2063
+ then tag each parameter with the step it belongs to using the `:tag` parameter option.
2064
+
2065
+ For example, consider the following form:
2066
+
2067
+ ``` ruby
2068
+ class PostForm < FormInput
2069
+ param! :email, "Email", EMAIL_ARGS
2070
+
2071
+ param :first_name, "First Name"
2072
+ param :last_name, "Last Name"
2073
+
2074
+ param :street, "Street"
2075
+ param :city, "City"
2076
+ param :zip, "ZIP Code"
2077
+
2078
+ param! :message, "Your Message", 1000
2079
+ param :comment, "Optional Comment"
2080
+
2081
+ param :url, type: :hidden
2082
+ end
2083
+ ```
2084
+
2085
+ That's a pretty standard form for posting some feedback message, with many fields optional.
2086
+ It can also track the URL where the user came from in the hidden `:url` field.
2087
+ Normally, you would display such form on a single page,
2088
+ but for the sake of the example, we will split it into multiple steps,
2089
+ having the user fill each block of information on a separate page:
2090
+
2091
+ ``` ruby
2092
+ class PostForm < FormInput
2093
+
2094
+ define_steps(
2095
+ email: "Email",
2096
+ name: "Name",
2097
+ address: "Address",
2098
+ message: "Message",
2099
+ summary: "Summary",
2100
+ post: nil,
2101
+ )
2102
+
2103
+ param! :email, "Email", EMAIL_ARGS, tag: :email
2104
+
2105
+ param :first_name, "First Name", tag: :name
2106
+ param :last_name, "Last Name", tag: :name
2107
+
2108
+ param :street, "Street", tag: :address
2109
+ param :city, "City", tag: :address
2110
+ param :zip, "ZIP Code", tag: :address
2111
+
2112
+ param! :message, "Your Message", tag: :message
2113
+ param :comment, "Optional Commment", tag: :message
2114
+
2115
+ param :url, type: :hidden
2116
+ end
2117
+ ```
2118
+
2119
+ The `define_steps` method takes a hash of symbols which define the steps
2120
+ along with their names which can be displayed to the user.
2121
+ It also extends the form with step-related functionality,
2122
+ which will be described in detail in the [Multi-Step Form Functionality](#multi-step-form-functionality) chapter.
2123
+ The steps are defined in the order which will be used to progress through the form -
2124
+ the user starts at the first step and finishes at the last
2125
+ (at least that's the most typical flow).
2126
+
2127
+ The `:tag` option is then used to assign each parameter to particular step.
2128
+ If you would need additional tags, remember that you can use the `:tags` array as well.
2129
+ Also note that the hidden parameter doesn't have to belong to any step,
2130
+ as it will be always rendered as a hidden field anyway.
2131
+
2132
+ As you can see, we have split the parameters among four steps.
2133
+ But we have defined more steps than that -
2134
+ there can be steps which have no parameters assigned to them.
2135
+ Such extra steps are still used for tracking progress through the form.
2136
+ In this example, the `:summary` step is used to display the form data
2137
+ gathered so far and giving the user the option of re-editing them before posting them.
2138
+ Similarly, you could have an initial `:intro` step or some other intermediate steps if you wanted.
2139
+ The final `:post` step serves as a terminator which we will use to actually process the form data.
2140
+ Note that it doesn't have a name,
2141
+ so it won't appear among the list of step names if we decide to display them somewhere
2142
+ (see [Rendering Multi-Step Form](#rendering-multi-step-form) for example of that).
2143
+
2144
+ We will soon delve into how to use such forms, but first let's discuss their enhanced functionality.
2145
+
2146
+ ### Multi-Step Form Functionality
2147
+
2148
+ Using `define_steps` in a form definition extends the range of the methods available,
2149
+ in addition to those described in the [Form Helpers](#form-helpers) chapter.
2150
+ It also adds several internal form parameters which are crucial for keeping track of the progress through the form.
2151
+
2152
+ The most important of those parameters is the `step` parameter.
2153
+ It is always set to the name of the current step,
2154
+ starting with the first step defined by default.
2155
+ If you need to, you can change the current step by simply assigning to it,
2156
+ we will see examples of that later in the [Using Multi-Step Form](#using-multi-step-form) chapter.
2157
+
2158
+ The second important parameter is the `next` parameter.
2159
+ It contains the desired step which the user wants to go to whenever he posts the form.
2160
+ This parameter is used to let the user progress throughout the form -
2161
+ we will see examples of that in the [Rendering Multi-Step Form](#rendering-multi-step-form) chapter.
2162
+ If it is set and there are no problems with the parameters of the currently submitted step,
2163
+ it will be used to update the value of the `step` parameter,
2164
+ effectively changing the current step to whatever the user wanted.
2165
+ Otherwise the `step` value is not changed and the current step is retained.
2166
+
2167
+ There are two more parameters which are internally used for keeping information about previously visited steps.
2168
+ The `last` parameter contains the highest step among the steps seen by the user, including the current step.
2169
+ The `seen` parameter contains the highest step among the steps seen by the user before the current step was displayed.
2170
+ Unlike the `last` parameter, which is always set, the `seen` parameter can be `nil` if no steps were displayed before yet.
2171
+ The current step is not included when it is displayed for the first time,
2172
+ it will become included only if it is displayed more than once.
2173
+ Neither of these parameters is used directly
2174
+ They are used by several helper methods for classifying the already visited steps,
2175
+ which we will see shortly.
2176
+
2177
+ There are three methods added which extend the list of methods
2178
+ which can be used for getting lists of form parameters.
2179
+ The `current_params` method provides the list of all parameters which belong to the current step,
2180
+ while the `other_params` method provides the list of those which do not.
2181
+ Then there is the `step_params` method which returns the list of all parameters for given step.
2182
+
2183
+ ``` ruby
2184
+ form = PostForm.new
2185
+ form.current_params.map( &:name ) # [:email]
2186
+ form.other_params.map( &:name ) # [:first_name, :last_name, ..., :comment, :url]
2187
+ form.step_params( :name ).map( &:name ) # [:first_name, :last_name]
2188
+ ```
2189
+
2190
+ The rest of the methods added is related to the steps themselves.
2191
+ The `steps` method returns the list of symbols defining the individual steps.
2192
+ The `step_names` method returns the hash of steps which have a name along with their names.
2193
+ The `step_name` method returns the name of current/given step, or `nil` if it has no name defined.
2194
+ The `next_step_name` and `previous_step_name` are handy shortcuts for getting
2195
+ the name of the next and previous step, respectively.
2196
+
2197
+ ``` ruby
2198
+ form.steps # [:email, :name, :address, :message, :summary, :post]
2199
+ form.step_names # {email: "Email", name: "Name", ..., message: "Message", summary: "Summary"}
2200
+ form.step_name # "Email"
2201
+ form.step_name( :address ) # "Address"
2202
+ form.step_name( :post ) # nil
2203
+ form.next_step_name # "Address"
2204
+ form.previous_step_name # nil
2205
+ ```
2206
+
2207
+ Then there are methods dealing with the step order.
2208
+ The `step_index` method returns the index of the current/given step.
2209
+ The `step_before?` and `step_after?` methods test
2210
+ if the current step is before or after given step, respectively.
2211
+ The `first_step` method returns the first step defined,
2212
+ and the `last_step` method returns the last step defined.
2213
+ If provided with a list of step names,
2214
+ these methods return the first/last valid step among them, respectively.
2215
+ The `next_step` method returns the step following the current/given step,
2216
+ or `nil` if there is no next step,
2217
+ and
2218
+ the `previous_step` method returns the step preceding the current/given step,
2219
+ or `nil` if there is no previous step.
2220
+ Finally,
2221
+ the `next_steps` method returns the list of steps following the current/given step,
2222
+ and the `previous_steps` method returns the list of steps preceding the current/given step.
2223
+
2224
+ ``` ruby
2225
+ form.step_index # 0
2226
+ form.step_index( :address ) # 2
2227
+ form.step_before?( :summary ) # true
2228
+ form.step_after?( :email ) # false
2229
+ form.first_step # :email
2230
+ form.first_step( :message, :name ) # :name
2231
+ form.last_step # :post
2232
+ form.last_step( :message, :name ) # :message
2233
+ form.next_step # :name
2234
+ form.next_step( :message ) # :summary
2235
+ form.next_step( :post ) # nil
2236
+ form.previous_step # nil
2237
+ form.previous_step( :message ) # :address
2238
+ form.previous_step( :email ) # nil
2239
+ form.next_steps # [:name, :address, :message, :summary, :post]
2240
+ form.next_steps( :address ) # [:message, :summary, :post]
2241
+ form.previous_steps # []
2242
+ form.previous_steps( :address ) # [:email, :name]
2243
+ ```
2244
+
2245
+ Then there is a group of boolean getter methods which
2246
+ can be used to query the current/given step about various things:
2247
+
2248
+ ``` ruby
2249
+ form.first_step? # Is this the first step?
2250
+ form.last_step? # Is this the last step?
2251
+ form.regular_step? # Does this step have some parameters assigned?
2252
+ form.extra_step? # Does this step have no parameters assigned?
2253
+ form.required_step? # Does this step have some required parameters?
2254
+ form.optional_step? # Does this step have no required parameters?
2255
+ form.filled_step? # Were some of the step parameters filled already?
2256
+ form.unfilled_step? # Were none of the step parameters filled yet?
2257
+ form.correct_step? # Are all of the step parameters valid?
2258
+ form.incorrect_step? # Are some of the step parameters invalid?
2259
+ form.enabled_step? # Are not all of the step parameters disabled?
2260
+ form.disabled_step? # Are all of the step parameters disabled?
2261
+
2262
+ form.first_step?( :email ) # true
2263
+ form.last_step?( :post ) # true
2264
+ form.regular_step?( :name ) # true
2265
+ form.extra_step?( :post ) # true
2266
+ form.required_step?( :message ) # true
2267
+ form.optional_step?( :post ) # true
2268
+ form.filled_step?( :post ) # true
2269
+ form.unfilled_step?( :name ) # true
2270
+ form.correct_step?( :post ) # true
2271
+ form.incorrect_step?( :email ) # true
2272
+ form.enabled_step?( :name ) # true
2273
+ form.disabled_step?( :post ) # false
2274
+ ```
2275
+
2276
+ Note that the extra steps, which have no parameters assigned to them,
2277
+ are always considered optional, filled, correct and enabled
2278
+ for the purpose of these methods.
2279
+
2280
+ Based on these getters,
2281
+ there is a group of methods which return a list of matching steps.
2282
+ In this case, however, the extra steps are excluded for convenience
2283
+ from all these methods (except the `extra_steps` method itself, of course):
2284
+
2285
+ ``` ruby
2286
+ form.regular_steps # [:email, :name, :address, :message]
2287
+ form.extra_steps # [:summary, :post]
2288
+ form.required_steps # [:email, :message]
2289
+ form.optional_steps # [:name, :address]
2290
+ form.filled_steps # []
2291
+ form.unfilled_steps # [:email, :name, :address, :message]
2292
+ form.correct_steps # [:name, :address]
2293
+ form.incorrect_steps # [:email, :message]
2294
+ form.enabled_steps # [:email, :name, :address, :message]
2295
+ form.disabled_steps # []
2296
+ ```
2297
+
2298
+ The first of the incorrect steps is of particular interest,
2299
+ so there is the shortcut method `incorrect_step` just for that:
2300
+
2301
+ ``` ruby
2302
+ form.incorrect_step # :email
2303
+ ```
2304
+
2305
+ Finally, there is a group of methods which deal with the progress through the form.
2306
+ Normally, the user starts at the first step,
2307
+ and proceeds to the next step whenever he submits valid parameters for the current step.
2308
+ If the submitted parameters contain some errors,
2309
+ the current step is not advanced and the errors are reported,
2310
+ allowing the user to fix them before moving on.
2311
+ Eventually the user reaches the last step,
2312
+ at which point the form is finally processed.
2313
+ That's the standard flow, but it can be changed by allowing the user
2314
+ to go back and forth to all the steps he had visited previously.
2315
+
2316
+ There are several methods for getting a list of steps in the corresponding range.
2317
+ The `finished_steps` method returns the list of steps
2318
+ which the user has visited and submitted before.
2319
+ The `unfinished steps` method returns the list of steps
2320
+ which the user has not visited yet, or visited for the first time.
2321
+ The `accessible_steps` method returns the list of steps
2322
+ which the user has visited already.
2323
+ The `inaccessible_steps` method returns the list of steps
2324
+ which the user has not visited yet at all.
2325
+ There is also the matching set of boolean getter methods
2326
+ which can be used to query the same information about individual steps.
2327
+
2328
+ ``` ruby
2329
+ form.finished_steps # []
2330
+ form.unfinished_steps # [:email, :name, :address, :message, :summary, :post]
2331
+ form.accessible_steps # [:email]
2332
+ form.inaccessible_steps # [:name, :address, :message, :summary, :post]
2333
+
2334
+ form.finished_step? # false
2335
+ form.unfinished_step? # true
2336
+ form.accessible_step? # true
2337
+ form.inaccessible_step? # false
2338
+
2339
+ form.finished_step?( :email ) # false
2340
+ form.unfinished_step?( :post ) # true
2341
+ form.accessible_step?( :email ) # true
2342
+ form.inaccessible_step?( :post ) # true
2343
+ ```
2344
+
2345
+ By default, only the first step is initially accessible,
2346
+ but you can change that by using the `unlock_steps` method,
2347
+ which makes all steps instantly accessible.
2348
+ This can be handy for example when the whole form is prefilled with some previously acquired data,
2349
+ so the user can access any step from the very beginning:
2350
+
2351
+ ``` ruby
2352
+ form = PostForm.new( user.latest_post.to_hash ).unlock_steps
2353
+ ```
2354
+
2355
+ If you decide to display the individual steps in the masthead or the sidebar,
2356
+ it is often desirable to mark them not only as accessible or inaccessible,
2357
+ but also as correct or incorrect.
2358
+ This last group of methods is intended for that.
2359
+ The `complete_step?` method can be used to test
2360
+ if the current/given step was finished and contains no errors.
2361
+ The `incomplete_step?` method can be used to test
2362
+ if the current/given step was finished but contains errors.
2363
+ The `good_step?` method tests if the current/given step should be visualized as good
2364
+ (green color, check sign, etc.).
2365
+ By default it returns `true` for finished, correctly filled regular steps,
2366
+ but your form can override it to provide different semantics.
2367
+ The `bad_step?` method tests if the current/given step should be visualized as bad
2368
+ (red color, cross or exclamation mark, etc.).
2369
+ By default it returns `true` for finished but incorrect steps,
2370
+ but again you can override it if you wish.
2371
+ As usual, there are the corresponding methods
2372
+ which can be used to get lists of all the matching steps at once:
2373
+
2374
+ ``` ruby
2375
+ form = PostForm.new.unlock_steps
2376
+ form.complete_steps # [:name, :address, :summary, :post]
2377
+ form.incomplete_steps # [:email, :message]
2378
+ form.good_steps # []
2379
+ form.bad_steps # [:email, :message]
2380
+
2381
+ form.complete_step? # false
2382
+ form.incomplete_step? # true
2383
+ form.good_step? # false
2384
+ form.bad_step? # true
2385
+
2386
+ form.complete_step?( :name ) # true
2387
+ form.incomplete_step?( :email ) # true
2388
+ form.good_step?( :name ) # false
2389
+ form.bad_step?( :email ) # true
2390
+ ```
2391
+
2392
+ And that's it.
2393
+ Now let's have a look at some practical use of these helpers.
2394
+
2395
+ ### Using Multi-Step Forms
2396
+
2397
+ Using the multi-step forms is not much different from the normal forms.
2398
+ Creating the form initially is the same, as is presetting the parameters:
2399
+
2400
+ ``` ruby
2401
+ get '/post' do
2402
+ @form = PostForm.new( email: user.email )
2403
+ slim :post_form
2404
+ end
2405
+ ```
2406
+
2407
+ Processing the submitted form is nearly the same, too.
2408
+ The only difference is that you keep doing nothing until the user reaches the last step.
2409
+ Then you validate the form, and if there are some problems,
2410
+ return the user to the appropriate step to fix them.
2411
+ Normally, the user shouldn't be able to proceed to the last step if there are errors,
2412
+ but people can always try to trick the form by submitting any parameters they want,
2413
+ so you should be ready for that.
2414
+ Returning them to the incorrect step is more polite than just failing with an error,
2415
+ but you could do that as well if you wanted.
2416
+ In either case,
2417
+ once the user gets to the last step and there are no errors,
2418
+ you use the form the same way you would use any regular form.
2419
+
2420
+ ``` ruby
2421
+ post '/post' do
2422
+ @form = PostForm.new( request )
2423
+
2424
+ # Wait for the last step.
2425
+ return slim :post_form unless @form.last_step?
2426
+
2427
+ # Make sure the form is really valid.
2428
+ unless @form.valid?
2429
+ @form.step = @form.incorrect_step
2430
+ return slim :post_form
2431
+ end
2432
+
2433
+ # Now somehow use the submitted data.
2434
+ user.create_post( @form )
2435
+ slim :post_created
2436
+ end
2437
+ ```
2438
+
2439
+ And that's it.
2440
+ I told you it was easy.
2441
+ Of course, you can utilize more of the helpers described in
2442
+ the [Multi-Step Form Functionality](#multi-step-form-functionality) chapter
2443
+ and do more complex things if you wish,
2444
+ but the example above is the basic boilerplate you will likely want to start with most of the time.
2445
+
2446
+ ### Rendering Multi-Step Forms
2447
+
2448
+ Rendering the multi-step form is similar to rendering of regular forms as well.
2449
+ The biggest difference is that you render the parameters for the current step normally,
2450
+ while all other parameters are rendered as hidden input elements.
2451
+ The most basic multi-step form could thus look like this:
2452
+
2453
+ ``` slim
2454
+ form *form_attrs
2455
+ fieldset
2456
+ == snippet :form_standard, params: @form.current_params, report: @form.finished_step?
2457
+ == snippet :form_hidden, params: @form.other_params
2458
+ button.btn.btn-default type='submit' name='next' value=@form.next_step Proceed
2459
+ ```
2460
+
2461
+ See how the submit button uses the `next` value to proceed to the next step.
2462
+ Without it, the form would be merely updated when submitted.
2463
+
2464
+ Also note how we control when to display the errors -
2465
+ we use the `finished_step?` method to supress the errors whenever
2466
+ the user sees certain step for the first time.
2467
+
2468
+ Note that it is also possible to divert the form rendering for individual steps if you need to.
2469
+ If we follow the `PostForm` example,
2470
+ you will want to render the `:summary` step accordingly, for example like this:
2471
+
2472
+ ``` slim
2473
+ h2 = @form.step_name
2474
+ - if @form.step == :summary
2475
+ == snippet :form_hidden, params: @form.params
2476
+ dl.dl-horizontal
2477
+ - for p in @form.visible_params
2478
+ dt = p.title
2479
+ dd = p.value
2480
+ - else
2481
+ == snippet :form_standard, params: @form.current_params, report: @form.finished_step?
2482
+ == snippet :form_hidden, params: @form.other_params
2483
+ ```
2484
+
2485
+ Of course, you will likely want your form to use more fancy submit buttons as well.
2486
+ For example, you can use more specific submit buttons for each step instead:
2487
+
2488
+ ``` slim
2489
+ .btn-toolbar.pull-right
2490
+ - if name = @form.next_step_name
2491
+ button.btn.btn-default type='submit' name='next' value=@form.next_step Next Step: #{name}
2492
+ - else
2493
+ button.btn.btn-primary type='submit' name='next' value=@form.next_step Send Post
2494
+ ```
2495
+
2496
+ If you want to provide button for updating the form content without proceeding to the next step,
2497
+ simply include something like this:
2498
+
2499
+ ``` slim
2500
+ -unless @form.extra_step?
2501
+ button.btn.btn-default type='submit' Update
2502
+ ```
2503
+
2504
+ To allow users to go back to the previous step, prepend something like this:
2505
+
2506
+ ``` slim
2507
+ - if name = @form.previous_step_name
2508
+ .btn-toolbar.pull-left
2509
+ button.btn.btn-default type='submit' name='next' value=@form.previous_step Previous Step: #{name}
2510
+ ```
2511
+
2512
+ Note that browsers nowadays automatically use the first submit button when the user hits the enter in the text field.
2513
+ If you have multiple buttons in the form and the first one is not guarnateed to be the one you want,
2514
+ you can add the following invisible button as the first button in the form to make the browser go to the step you want:
2515
+
2516
+ ``` slim
2517
+ button.invisible type='submit' name='next' value=@form.next_step tabindex='-1'
2518
+ ```
2519
+
2520
+ When rendering the multi-step form,
2521
+ it also makes sense to display the individual steps in some way
2522
+ so the user can see what steps there are and to see his progress.
2523
+ Common ways are a form specific masthead above the form or a sidebar next to the form.
2524
+ The basic masthead can be rendered like this:
2525
+
2526
+ ``` slim
2527
+ ul.form-masthead
2528
+ - for step, name in @form.step_names
2529
+ li[
2530
+ class=(:active if step == @form.step)
2531
+ class=(:disabled unless @form.accessible_step?( step ))
2532
+ ]
2533
+ = name
2534
+ ```
2535
+
2536
+ This uses the typical CSS classes to distinguish between the current, accessible and inaccessible steps.
2537
+ If you also want to somehow mark the correct and incorrect steps,
2538
+ you can append something like this:
2539
+
2540
+ ``` slim
2541
+ - if @form.bad_step?( step )
2542
+ span.pull-right.glyphicon.glyphicon-exclamation-sign.text-danger
2543
+ - elsif @form.good_step?( step )
2544
+ span.pull-right.glyphicon.glyphicon-ok-sign.text-success
2545
+ ```
2546
+
2547
+ Users also often expect to be able to click the individual steps to go directly to that step.
2548
+ To allow that, simply change the list elements to buttons which can be clicked like this:
2549
+
2550
+ ``` slim
2551
+ ul.form-masthead
2552
+ - for step, name in @form.step_names
2553
+ - if @form.accessible_step?( step )
2554
+ li class=(:active if step == @form.step)
2555
+ button type='submit' name='next' value=step tabindex='-1'
2556
+ = name
2557
+ - else
2558
+ li.disabled = name
2559
+ ```
2560
+
2561
+ Note that in this case you will almost certainly want to include the invisible button
2562
+ we have mentioned above as the first button in the form
2563
+ to make sure hitting the enter in the text field works as expected.
2564
+
2565
+ Of course, once again it is trivial to extend these examples with anything you need.
2566
+ Do you want to show tips about each form step as the user hovers over them?
2567
+ Simply add the `step_hint` method to your form to return the text to display
2568
+ and add the hint with the title attribute like this:
2569
+
2570
+ ``` slim
2571
+ li ... title=@form.step_hint( step )
2572
+ ```
2573
+
2574
+ The list of the possible enhancements could go on and on.
2575
+ As your multi-step form navigation gets more complex,
2576
+ you will likely want to factor it out to its own snippet.
2577
+ This will allow you to share it among multiple forms
2578
+ and even switch visual styles with ease.
2579
+
2580
+ ## Localization
2581
+
2582
+ Working with forms in one way or another implies showing lot of text to the user.
2583
+ Chances are that sooner or later you'll want that text to become localized.
2584
+ The good news is that the `FormInput` comes with full featured localization support already built in.
2585
+ These chapters explain in detail how to take advantage of all its features.
2586
+
2587
+ The `FormInput` localization is built on [R18n].
2588
+ R18n is a neat tiny gem for all your localization needs.
2589
+ It is a no-nonsense, right-to-the-point toolkit developed by people who understand the subject.
2590
+ It even comes with an [I18n] compatible drop-in replacement for [Rails],
2591
+ so you can switch to it with ease.
2592
+ If you are serious about localization,
2593
+ you should definitely check it out.
2594
+
2595
+ If your project already uses R18n,
2596
+ requiring `form_input` will detect it and make the localization support available automatically.
2597
+ Otherwise you can make it available explicitly by requiring `form_input/r18n` in your application:
2598
+
2599
+ ``` ruby
2600
+ require `form_input/r18n`
2601
+ ```
2602
+
2603
+ Then all you need to do is to set the desired R18n locale:
2604
+
2605
+ ``` ruby
2606
+ R18n.set('en') # For generic English.
2607
+ R18n.set('en-us') # For American English.
2608
+ R18n.set('en-gb') # For British English.
2609
+ R18n.set('cs') # For Czech.
2610
+ ```
2611
+
2612
+ Note that how exactly is this done can differ slightly depending on the framework you use.
2613
+ For example, if you use [Sinatra] together with the [sinatra-r18n] gem,
2614
+ the locale is set automatically for you for each request by the R18n helper.
2615
+ Likewise if you use [Rails] together with the [r18n-rails] gem.
2616
+ Please refer to the [R18n] documentation for more details.
2617
+
2618
+ Anyway, once the localization is enabled, the following features become available:
2619
+
2620
+ * All builtin error messages and other builtin strings become localized.
2621
+ * Full inflection support is enabled for all builtin error messages.
2622
+ * All string parameter options can be localized.
2623
+ * All multi-step form step names can be localized.
2624
+ * The R18n `t` and `l` helpers become available in both form and parameter contexts.
2625
+ * Additional `ft` helper becomes available in both form and parameter contexts.
2626
+ * Additional `pt` helper becomes available in the parameter context.
2627
+
2628
+ Note that the full inflection support alone can be useful on its own,
2629
+ so you may want to explore the following chapters
2630
+ even if you don't plan to translate the application to other languages yet.
2631
+
2632
+ ### Error Messages and Inflection
2633
+
2634
+ Validation and error reporting are major `FormInput` features.
2635
+ Normally, `FormInput` uses builtin English error messages.
2636
+ It pluralizes the names of the units it displays properly,
2637
+ but provides little inflection support beyond that.
2638
+ It assumes that all scalar parameter names use singular case
2639
+ and all array and hash parameter names use plural case.
2640
+ This is most often the case, but not always.
2641
+ If you need something more flexible, you may need to enable the localization support.
2642
+
2643
+ With localization enabled,
2644
+ the list of builtin error messages becomes replaced by
2645
+ string translations managed by [R18n] in the `form_input` namespace.
2646
+ You can find the available translation files in the `form_input/r18n` directory.
2647
+ The same path can be obtained at runtime from the `FormInput.translations_path` method.
2648
+ The translations of the error messages were created with inflection in mind
2649
+ and the message variants are chosen according to the inflection rules of the parameter names automatically -
2650
+ the [Inflection Filter](#inflection-filter) chapter will explain the gory details.
2651
+
2652
+ For English, the inflection rules are pretty simple.
2653
+ You only need to distinguish between singular and plural grammatical number.
2654
+ By default,
2655
+ `FormInput` uses plural for scalar parameters and singular for array and hash parameters.
2656
+ With localization enabled,
2657
+ you can override it by setting the `:plural` parameter option to `true` or `'p'` for plural,
2658
+ and to `false` or `'s'` for singular, respectively:
2659
+
2660
+ ``` ruby
2661
+ param :keywords, "Keywords, plural: true
2662
+ array :countries, "List of countries", plural: false
2663
+ ```
2664
+
2665
+ The boolean values make sense for most languages,
2666
+ but note that there are languages which have more than two grammatical numbers,
2667
+ and that's when the string values may become useful.
2668
+ Regardless of which way you use,
2669
+ the `plural` method can be used to get the string matching
2670
+ the grammatical number of given parameter for the currently active locale.
2671
+
2672
+ However, grammatical number is not the only thing to take care of.
2673
+ For many languages, the rules are more complex than that.
2674
+ You usually need to take the grammatical gender into account as well.
2675
+ Instead of using single set of error messages and forcing you to use single grammatical gender to fit them all,
2676
+ `FormInput` allows you to use the `:gender` option to specify the grammatical gender of each parameter explicitly.
2677
+ Its value is one of the shortcuts of the grammatical genders used by the translation file for given language -
2678
+ check the corresponding file in the `form_input/r18n` directory for specific details.
2679
+ The following gender values are typically available:
2680
+
2681
+ * `n` - neuter
2682
+ * `f` - feminine
2683
+ * `m` - masculine
2684
+ * `mi` - inanimate masculine
2685
+ * `ma` - animate masculine
2686
+ * `mp` - personal masculine
2687
+
2688
+ When not set, the default value is `n` for neuter gender,
2689
+ which is suitable for example for English,
2690
+ but other languages can set the default to be something else,
2691
+ typically `mi` for inanimate masculine gender.
2692
+ See the `default_gender` value in
2693
+ the corresponding file in the `form_input/r18n` directory.
2694
+ In either case,
2695
+ the `gender` method can be used to get the string containing
2696
+ the grammatical gender of given parameter for the currently active locale.
2697
+
2698
+ To see how this works in real life, here are several parameters with properly inflected Czech titles:
2699
+
2700
+ ``` ruby
2701
+ param :email, "Email"
2702
+ param :name, "Jméno", gender: "n"
2703
+ param :address, "Adresa", gender: "f"
2704
+ param :keywords, "Klíčová slova", plural: true, gender: "n"
2705
+ param :authors, "Autoři", plural: true, gender: "ma"
2706
+ ```
2707
+
2708
+ However, even if it is possible to use non-English names directly in the forms like this,
2709
+ assuming you also set the R18n locale to the corresponding value,
2710
+ it is not very common.
2711
+ The whole point of localization is usually to extract the texts to external files,
2712
+ so they can be translated and localized to different languages.
2713
+ The next chapter will explain how to do that.
2714
+
2715
+ ### Localizing Forms
2716
+
2717
+ If you have started creating your `FormInput` forms without thinking about localization,
2718
+ the good news are that the forms will not require much changes to become localized.
2719
+ In fact, most of your form strings will be taken care of automatically.
2720
+ Only strings which you might have used within the dynamically evaluated parameter options
2721
+ or parameter callbacks
2722
+ will need to be localized explicitly with the help of the [Localization Helpers](#localization-helpers).
2723
+
2724
+ To localize your project,
2725
+ you will first need to add the R18n translation files to it.
2726
+ See the [R18n] documentation for details on how is this done
2727
+ and where the files are supposed to be placed.
2728
+ Once you have the R18n directory structure set up, you just need to localize the forms themselves.
2729
+ To get you started, `FormInput` provides a localization helper to create the default translation file for you.
2730
+ All you need to do is to run `irb`, require all of your project files,
2731
+ then run the following:
2732
+
2733
+ ``` ruby
2734
+ require 'form_input/localize'
2735
+ File.write( 'en.yml', FormInput.default_translation )
2736
+ ```
2737
+
2738
+ This creates a [YAML] file called `en.yml` which contains
2739
+ the default translations for all your forms in the format directly usable by R18n.
2740
+ Of course, if for some reason your default project language is not English,
2741
+ adjust the name of the file accordingly.
2742
+
2743
+ For example, for a project containing only the `ContactForm` from the [Introduction](#form-input) section
2744
+ the content of the default translation file would look like this:
2745
+
2746
+ ``` yaml
2747
+ ---
2748
+ forms:
2749
+ contact_form:
2750
+ email:
2751
+ title: Email address
2752
+ name:
2753
+ title: Name
2754
+ company:
2755
+ title: Company
2756
+ message:
2757
+ title: Message
2758
+ ```
2759
+
2760
+ As you can see,
2761
+ all form related strings reside within the `forms` namespace,
2762
+ so it shall not interfere with your other translations.
2763
+ You can merge it with your main `en.yml` in the `/i18n/` directory,
2764
+ or, if you want to keep it separate,
2765
+ you can put it in its own subdirectory, say `/i18n/forms/en.yml`.
2766
+
2767
+ Once you have the `en.yml` file in place,
2768
+ R18n shall pick it up automatically.
2769
+ To make sure it is all working,
2770
+ temporarily change some of the form titles in the translation file to something else
2771
+ and it shall change on the rendered page accordingly.
2772
+ If it doesn't seem to work, try adding something like the following bit somewhere on some page
2773
+ and make it work first
2774
+ (of course, adjust the name of the form and parameter accordingly to match your real project):
2775
+
2776
+ ``` slim
2777
+ pre = t.forms.contact_form.email.title
2778
+ ```
2779
+
2780
+ Please refer to the [R18n] documentation for more troubleshooting help if you still need assistance.
2781
+
2782
+ Once you get the default translation file working,
2783
+ you are all set to start adding other translation files
2784
+ for all the languages you want.
2785
+ But first you should understand the content of those files and how to use it.
2786
+ The next chapters will explain it in detail.
2787
+
2788
+ ### Localizing Parameters
2789
+
2790
+ Each form derived from `FormInput` has its own namespace in the global `forms` [R18n] namespace.
2791
+ The name of this namespace is returned by the `translation_name` method of each form class.
2792
+ It's basically the snake_case conversion of the CamelCase name of the form class.
2793
+
2794
+ Each of the form parameters has its own namespace within the namespace of the form to which it belongs.
2795
+ The name of this namespace is the same as the `name` attribute of the parameter.
2796
+ Each of the parameter options can be localized by simply adding
2797
+ the appropriate translation within this parameter namespace.
2798
+ Remember that the `title` attribute of the parameter is nothing more than just one of the possible [Parameter Options](#parameter-options),
2799
+ so it applies to it as well.
2800
+
2801
+ The localization of the `ContactForm`
2802
+ from the [Introduction](#form-input) section
2803
+ thus looks like this:
2804
+
2805
+ ``` yaml
2806
+ forms:
2807
+ contact_form:
2808
+ email:
2809
+ title: Email address
2810
+ name:
2811
+ title: Name
2812
+ company:
2813
+ title: Company
2814
+ message:
2815
+ title: Message
2816
+ ```
2817
+
2818
+ To translate it say from English to Czech,
2819
+ copy it from the `en.yml` file to the `cs.yml` file
2820
+ and then adjust it for example like this:
2821
+
2822
+ ``` yaml
2823
+ forms:
2824
+ contact_form:
2825
+ email:
2826
+ title: Email
2827
+ name:
2828
+ title: Jméno
2829
+ gender: n
2830
+ company:
2831
+ title: Společnost
2832
+ gender: f
2833
+ message:
2834
+ title: Zpráva
2835
+ gender: f
2836
+ ```
2837
+
2838
+ Note the use of the `:gender` option to get properly inflected error messages.
2839
+
2840
+ Now let's say we decide to use a custom error message when the user forgets to fill in the content of the message field.
2841
+ To do this, we add the value of the `:required_msg` option of the `message` parameter directly to `en.yml` like this:
2842
+
2843
+ ``` yaml
2844
+ message:
2845
+ title: Message
2846
+ required_msg: Message with no content makes no sense.
2847
+ ```
2848
+
2849
+ Note that this works even if you don't add the `:required_msg` option to the parameter
2850
+ within the `ContactForm` definition itself.
2851
+ That's because as long as you have the locale support enabled,
2852
+ all applicable parameter options are automatically looked up in the translation files first.
2853
+ If the corresponding translation is found,
2854
+ it is used regardless of the parameter option value declared in the form.
2855
+ This allows you to completely remove the texts from the form definitions themselves
2856
+ and to keep them all in one place,
2857
+ which makes the localization easier to maintain in the long term.
2858
+
2859
+ The texts present in the class definition are used only as a fallback if no corresponding translation is found at all.
2860
+ This is particularly handy when you are adding new parameters which you haven't added into any of the translation files yet.
2861
+ However note that R18n normally provides its own default translation based on its builtin fallback sequence for each locale,
2862
+ usually falling back to English in the end.
2863
+ This means that once you add some translation to the `en.yml` file,
2864
+ the parameter option will get this English translation for all other locales as well,
2865
+ until you add the corresponding translation it to the other translations files as well.
2866
+
2867
+ So, to make sure we get the Czech version of the `:required_msg` for the example above,
2868
+ the `cs.yml` file should be updated as well like this:
2869
+
2870
+ ``` yaml
2871
+ message:
2872
+ title: Zpráva
2873
+ gender: f
2874
+ required_msg: Zpráva bez obsahu nemá žádný smysl.
2875
+ ```
2876
+
2877
+ And that's about it.
2878
+ This alone will allow you to localize and translate most if not all of your forms completely.
2879
+ However,
2880
+ if you were using some texts within the dynamically evaluated options or parameter callbacks
2881
+ like `:check` or `:test`,
2882
+ you will need to replace them with the use of the localization helpers which we will describe in next chapter.
2883
+
2884
+ ### Localization Helpers
2885
+
2886
+ The R18n provides two main shortcut methods, `t` and `l`,
2887
+ which are used for translating texts and localizing objects, respectively.
2888
+ See the [R18n] documentation for details.
2889
+ When the localization support is enabled,
2890
+ the `FormInput` makes these two methods available in both parameter and form contexts.
2891
+ It also adds two additional helper methods similar to the `t` method, called `ft` and `pt`.
2892
+ Either of these methods can be used to translate texts other than those automatically handled by the `FormInput` itself.
2893
+
2894
+ The `ft` method is usable in both parameter and form contexts.
2895
+ It works like the `t` method,
2896
+ except that all translations are automatically looked up in the form's own `forms.<form_translation_name>` namespace,
2897
+ rather than the global namespace.
2898
+ Note that it supports the `.`, `()`, and `[]` syntax alternatives for getting the desired translation.
2899
+ The latter two forms are handy when the text name is obtained programatically.
2900
+
2901
+ ``` ruby
2902
+ form = ContactForm.new
2903
+ form.ft.some_text # Returns forms.contact_form.some_text translation.
2904
+ form.ft( :some_text ) # Ditto.
2905
+ form.ft[ :some_text ] # Ditto.
2906
+ ```
2907
+
2908
+ Note that it is possible to pass arguments to the translation as usual:
2909
+
2910
+ ``` ruby
2911
+ form.ft.other_text( 1 ) # Returns forms.contact_form.other_text translation, using 1 as an argument.
2912
+ form.ft( :other_text, 1 ) # Ditto.
2913
+ form.ft[ :other_text, 1 ] # Ditto.
2914
+ ```
2915
+
2916
+ Of course, nesting of translations is possible as usual as well:
2917
+
2918
+ ``` ruby
2919
+ form.ft.errors.generic # Returns forms.contact_form.errors.generic translation.
2920
+ form.ft( :errors ).generic # Ditto.
2921
+ form.ft[ :errors ].generic # Ditto.
2922
+ ```
2923
+
2924
+ The `ft` method is typically used in methods which encapsulate the lookup of translated text within your form.
2925
+ For example, here is how the `group_name` method
2926
+ mentioned in the [Grouped Parameters](#grouped-parameters) chapter
2927
+ might look like:
2928
+
2929
+ ``` ruby
2930
+ def group_name( group )
2931
+ ft.groups[ group ]
2932
+ end
2933
+ ```
2934
+
2935
+ The `pt` method is usable only in the parameter context.
2936
+ It works similar to the `ft` method,
2937
+ except that all translations are automatically looked up in the parameter's own ``forms.<form_translation_name>.<parameter_name>` namespace,
2938
+ rather than the global namespace.
2939
+
2940
+ ``` ruby
2941
+ p = form.params.first
2942
+ p.pt.some_text # Returns forms.contact_form.email.some_text translation.
2943
+ p.pt( :some_text ) # Ditto.
2944
+ p.pt[ :some_text ] # Ditto.
2945
+ p.pt.other_text( 1 ) # Returns forms.contact_form.email.other_text translation,
2946
+ p.pt( :other_text, 1 ) # Ditto. using 1 as an argument.
2947
+ p.pt[ :other_text, 1 ] # Ditto.
2948
+ p.pt.errors.generic # Returns forms.contact_form.email.errors.generic translation.
2949
+ p.pt( :errors ).generic # Ditto.
2950
+ p.pt[ :errors ].generic # Ditto.
2951
+ ```
2952
+
2953
+ Like the `ft` method, it provides three syntax alternatives for getting the desired translation.
2954
+ It's worth mentioning that the `()` syntax automatically appends the parameter itself as the last arguments,
2955
+ making it available for the [Inflection Filter](#inflection-filter), which will be discussed later.
2956
+
2957
+ The `pt` method is typically used in parameter callbacks and
2958
+ in dynamically evaluated parameter options.
2959
+ For example, the text in the following callback
2960
+
2961
+ ``` ruby
2962
+ check: ->{ report( "This password is not secure enough" ) unless form.secure_password?( value ) }
2963
+ ```
2964
+
2965
+ can be replaced with properly translated parameter specific variant like this:
2966
+
2967
+ ``` ruby
2968
+ check: ->{ report( pt.insecure_password ) unless form.secure_password?( value ) }
2969
+ ```
2970
+
2971
+ It is also handy when the text requires some parameters:
2972
+
2973
+ ``` ruby
2974
+ param! :password, "Password", PASSWORD_ARGS,
2975
+ help: ->{ pt.help( self[ :min_size ], self[ :max_size ] ) }
2976
+ ```
2977
+
2978
+ Note that the texts translated like this are usually parameter specific
2979
+ so they can be inflected and adjusted as needed by the translator directly.
2980
+ However, if you are preparing something which is intended to be reused,
2981
+ you will likely want to have the messages automatically inflected according to the parameter used.
2982
+ This is when the [Inflection Filter](#inflection-filter) comes to help.
2983
+ All you need to do is to pass the form parameter as the last argument to the translation getter like this:
2984
+
2985
+ ``` ruby
2986
+ EVEN_ARGS = {
2987
+ test: ->( value ){ report( t.forms.errors.odd_value( self ) ) unless value.to_i.even? }
2988
+ }
2989
+ ```
2990
+
2991
+ You can even do something more fancy,
2992
+ for example distinguish between the scalar and array and hash parameters,
2993
+ or pass in the rejected value itself as an additional parameter,
2994
+ like this:
2995
+
2996
+ ``` ruby
2997
+ EVEN_ARGS = {
2998
+ test: ->( value ){
2999
+ report( t.forms.errors[ scalar? ? :odd_value : :odd_array, value, self ] ) unless value.to_i.even?
3000
+ }
3001
+ }
3002
+ ```
3003
+
3004
+ In either case,
3005
+ if set correctly,
3006
+ the inflection filter will pick up the last argument provided and choose the appropriatelly inflected message.
3007
+ Now let's see how exactly is this done.
3008
+
3009
+ ### Inflection Filter
3010
+
3011
+ The R18n suite includes flexible filtering support for additional processing of the translated texts.
3012
+ See the [R18n] documentation for example for the use of the `pl` pluralization filter.
3013
+ The `FormInput` provides its own `inflect` inflection filter which works similarly.
3014
+ To continue the `EVEN_ARGS` example from the previous chapter, consider the following translation file:
3015
+
3016
+ ``` yaml
3017
+ forms:
3018
+ errors:
3019
+ odd_value: !!inflect
3020
+ s: '%p is not even'
3021
+ p: '%p are not even'
3022
+ odd_array: !!inflect
3023
+ s: '%p contains number %1 which is not even'
3024
+ p: '%p contain number %1 which is not even'
3025
+ ```
3026
+
3027
+ This basically tells the R18n toolkit that it should use the `inflect` filter whenever
3028
+ someone asks for the value of the `odd_value` or `odd_array` translations.
3029
+ The value itself is not a string in this case,
3030
+ but a hash which contains several translations.
3031
+ The key is the longest prefix of the _inflection string_
3032
+ used to choose the desired translation.
3033
+ Now what is this inflection string and where does it come from?
3034
+
3035
+ Each parameter has the `inflection` method which returns
3036
+ the desired string used to choose the appropriatelly inflected error message.
3037
+ By default, it returns the grammatical number and grammatical gender strings combined,
3038
+ as returned by the `plural` and `gender` methods of the parameter, respectively.
3039
+ If needed, it can be also set directly by the `:inflect` parameter option.
3040
+ This can be handy for languages which have even more complex rules for inflection
3041
+ than the currently builtin ones.
3042
+
3043
+ The inflection filter checks the last parameter it was passed to the translation getter by its caller.
3044
+ If it is a string, it is used as it is.
3045
+ If it is a form parameter, its `inflection` method is used to get the inflection string.
3046
+ If no inflection string is provided, it falls back to the default string `'sn'` which stands for singular neuter.
3047
+ It than uses the form's `find_inflection` method to find the most appropriate translation for given inflection string.
3048
+ By default,
3049
+ it finds the longest prefix among the keys of the available translations which match the inflection string.
3050
+
3051
+ This may sound complex, but it allows minimizing the number of inflected translations.
3052
+ Instead of having to list translations for all inflection variants,
3053
+ only those which differ have to be listed and the other ones can be merged together.
3054
+ In English it makes little difference,
3055
+ as it only distinguishes between singular and plural grammatical case,
3056
+ but you can check the translation files for other languages in the `form_input/r18n` directory
3057
+ to see how this is used in practice.
3058
+ Here is an excerpt from the Czech translation file which demonstrates this:
3059
+
3060
+ ``` yaml
3061
+ required_scalar: !!inflect
3062
+ sm: '%p je povinný'
3063
+ sf: '%p je povinná'
3064
+ sn: '%p je povinné'
3065
+ p: '%p jsou povinné'
3066
+ pma: '%p jsou povinní'
3067
+ pn: '%p jsou povinná'
3068
+ ```
3069
+
3070
+ As you can see, there are three distinct variants in the singular case, one for each gender.
3071
+ The plural case on the other hand used the same variant for most genders, with only two specific exceptions defined.
3072
+
3073
+ Defining translations like this may seem complex,
3074
+ but it should feel fairly natural to people fluent in given language.
3075
+ If you intend to use some texts over and over again,
3076
+ paying attention to their proper inflection will definitely pay off in the long term.
3077
+
3078
+ ### Localizing Form Steps
3079
+
3080
+ If you are using the [Multi-Step Forms](#multi-step-forms),
3081
+ you will likely want to localize the step names themselves as well.
3082
+ Fortunately, it's very simple.
3083
+ Just add the translations of all step names
3084
+ to the `steps` namespace of the form in question.
3085
+
3086
+ For example,
3087
+ here is how the translation file of the `PostForm` form
3088
+ from the [Defining Multi-Step Forms](#defining-multi-step-forms) chapter
3089
+ would look like:
3090
+
3091
+ ``` yaml
3092
+ post_form:
3093
+ email:
3094
+ title: Email
3095
+ first_name:
3096
+ title: First Name
3097
+ last_name:
3098
+ title: Last Name
3099
+ street:
3100
+ title: Street
3101
+ city:
3102
+ title: City
3103
+ zip:
3104
+ title: ZIP Code
3105
+ message:
3106
+ title: Your Message
3107
+ comment:
3108
+ title: Optional Commment
3109
+ steps:
3110
+ email: Email
3111
+ name: Name
3112
+ address: Address
3113
+ message: Message
3114
+ summary: Summary
3115
+ ```
3116
+
3117
+ Trivial indeed, isn't it?
3118
+
3119
+ ### Supported Locales
3120
+
3121
+ The `FormInput` currently includes translations of builtin error messages for the following locales:
3122
+
3123
+ * English
3124
+ * Czech
3125
+ * Slovak
3126
+ * Polish
3127
+
3128
+ To add support for another locale,
3129
+ simply copy the content of one of the most similar files found in the `form_input/r18n` directory
3130
+ to the appropriate translation file in your project,
3131
+ and translate it as you see fit.
3132
+ Pay extra attention to the proper use of the inflection keys,
3133
+ see the [Inflection Filter](#inflection-filter) chapter for details.
3134
+ Once you are happy with the translation,
3135
+ please consider sharing it with the rest of the world.
3136
+ If you get in touch and make it available, it may become included in the future update of this gem.
3137
+ Thanks for that.
3138
+
3139
+ ## Credits
3140
+
3141
+ Copyright &copy; 2015-2016 Patrik Rak
3142
+
3143
+ The `FormInput` is released under the MIT license.
3144
+
3145
+
3146
+ [DSL]: http://en.wikipedia.org/wiki/Domain-specific_language
3147
+ [Sinatra]: http://www.sinatrarb.com/
3148
+ [Ramaze]: http://ramaze.net/
3149
+ [Slim]: https://github.com/slim-template/slim
3150
+ [HAML]: http://haml.info/
3151
+ [Bootstrap]: http://getbootstrap.com/
3152
+ [DRY]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
3153
+ [ARIA]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA
3154
+ [R18n]: https://github.com/ai/r18n
3155
+ [I18n]: https://github.com/svenfuchs/i18n
3156
+ [sinatra-r18n]: https://github.com/ai/r18n/tree/master/sinatra-r18n
3157
+ [r18n-rails]: https://github.com/ai/r18n/tree/master/r18n-rails
3158
+ [Rails]: http://rubyonrails.org/
3159
+ [YAML]: http://yaml.org/
3160
+ [Sequel]: http://sequel.jeremyevans.net/