form_input 0.9.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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/