form_input 0.9.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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/
|