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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/LICENSE +19 -0
- data/README.md +3160 -0
- data/Rakefile +19 -0
- data/example/controllers/ramaze/press_release.rb +104 -0
- data/example/controllers/ramaze/profile.rb +38 -0
- data/example/controllers/sinatra/press_release.rb +114 -0
- data/example/controllers/sinatra/profile.rb +39 -0
- data/example/forms/change_password_form.rb +17 -0
- data/example/forms/login_form.rb +14 -0
- data/example/forms/lost_password_form.rb +14 -0
- data/example/forms/new_password_form.rb +15 -0
- data/example/forms/password_form.rb +18 -0
- data/example/forms/press_release_form.rb +153 -0
- data/example/forms/profile_form.rb +21 -0
- data/example/forms/signup_form.rb +25 -0
- data/example/views/press_release.slim +65 -0
- data/example/views/profile.slim +28 -0
- data/example/views/snippets/form_block.slim +27 -0
- data/example/views/snippets/form_chunked.slim +25 -0
- data/example/views/snippets/form_hidden.slim +21 -0
- data/example/views/snippets/form_panel.slim +89 -0
- data/form_input.gemspec +32 -0
- data/lib/form_input/core.rb +1165 -0
- data/lib/form_input/localize.rb +49 -0
- data/lib/form_input/r18n/cs.yml +97 -0
- data/lib/form_input/r18n/en.yml +70 -0
- data/lib/form_input/r18n/pl.yml +122 -0
- data/lib/form_input/r18n/sk.yml +120 -0
- data/lib/form_input/r18n.rb +163 -0
- data/lib/form_input/steps.rb +365 -0
- data/lib/form_input/types.rb +176 -0
- data/lib/form_input/version.rb +12 -0
- data/lib/form_input.rb +5 -0
- data/test/helper.rb +21 -0
- data/test/localize/en.yml +63 -0
- data/test/r18n/cs.yml +60 -0
- data/test/r18n/xx.yml +51 -0
- data/test/reference/cs.txt +352 -0
- data/test/reference/cs.yml +14 -0
- data/test/reference/en.txt +76 -0
- data/test/reference/en.yml +8 -0
- data/test/reference/pl.txt +440 -0
- data/test/reference/pl.yml +16 -0
- data/test/reference/sk.txt +352 -0
- data/test/reference/sk.yml +14 -0
- data/test/test_core.rb +1272 -0
- data/test/test_localize.rb +27 -0
- data/test/test_r18n.rb +373 -0
- data/test/test_steps.rb +482 -0
- data/test/test_types.rb +307 -0
- 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 © 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/
|