Form input
Form input is a gem which helps dealing with web request input and with the creation of HTML forms.
Install the gem:
gem install form_input
Describe your forms in a DSL like this:
# contact_form.rb
require 'form_input'
class ContactForm < FormInput
param! :email, "Email address", EMAIL_ARGS
param! :name, "Name"
param :company, "Company"
param! :message, "Message", 1000, type: :textarea, size: 16, filter: ->{ rstrip }
end
Then use them in your controllers/route handlers like this:
# app.rb
get '/contact' do
@form = ContactForm.new
@form.set( email: user.email, name: user.full_name ) if user?
slim :contact_form
end
post '/contact' do
@form = ContactForm.new( request )
return slim :contact_form unless @form.valid?
text = @form.params.map{ |p| "#{p.title}: #{p.value}\n" }.join
sent = Email.send( settings.contact_recipient, text, reply_to: @form.email )
slim( sent ? :contact_sent : :contact_failed )
end
Using them in your templates is as simple as this:
// contact_form.slim
.panel.panel-default
.panel-heading
= @title = "Contact Form"
.panel-body
form method='post' action=request.path
fieldset
== snippet :form_panel, params: @form.params
button.btn.btn-default type='submit' Send
The FormInput
class will take care of sanitizing the input,
converting it into any desired internal representation,
validating it, and making it available in a model-like structure.
The provided template snippets will take care of rendering the form parameters
as well as any errors detected back to the user.
You just get to use the input and control the flow the way you want.
In fact, it's not limited to form input only either -
it can be used with any web request input,
including that of AJAX or REST API end points.
The gem is completely framework agnostic,
comes with full test coverage,
and even supports multi-step forms and localization out of the box.
Sounds cool enough? Then read on.
Table of Contents
- Introduction
- Table of Contents
- Form Basics
- Defining Parameters
- Internal vs External Representation
- Input Filter
- Output Format
- Input Transform
- Array and Hash Parameters
- Reusing Form Parameters
- Creating Forms
- Errors and Validation
- Using Forms
- URL Helpers
- JSON Helpers
- Form Helpers
- Extending Forms
- Parameter Options
- Form Templates
- Form Template
- Simple Parameters
- Hidden Parameters
- Complex Parameters
- Text Area
- Select and Multi-Select
- Radio Buttons
- Checkboxes
- Inflatable Parameters
- Extending Parameters
- Grouped Parameters
- Chunked Parameters
- Multi-Step Forms
- Defining Multi-Step Forms
- Multi-Step Form Functionality
- Using Multi-Step Forms
- Rendering Multi-Step Forms
- Localization
- Error Messages and Inflection
- Localizing Forms
- Localizing Parameters
- Localization Helpers
- Inflection Filter
- Localizing Form Steps
- Supported Locales
- Credits
Form Basics
The following chapters explain how to describe your forms using a DSL, what's the difference between internal and external representation, how to create form instances and how to deal with errors, and, finally, how to access the form input itself.
And note that while forms get mentioned a lot, it can be all applied to any other web request input just as well. Even AJAX end points or REST API end points can describe their input and let it have converted and validated for them by the same means, which is worth keeping in mind.
Defining Parameters
To define a form, simply inherit from FormInput
and then
use the param
or param!
methods to define form parameters like this:
require 'form_input'
class MyForm < FormInput
param! :email, "Email Address"
param :name, "Full Name"
end
The param
method takes
parameter name
and
parameter title
as arguments.
The name is how you will address the parameter in your code,
while the optional title is the string which will be displayed to the user by default.
The param!
method works the same way but creates a required parameter.
Such parameters are required to appear in the input and to have a non-empty value.
Failure to do so will be automatically reported as an error
(discussed further in Errors and Validation).
Both methods actually take an optional options as their last argument, too.
The options argument is a hash which is used to control
most aspects of the parameter.
In fact, using the title argument is just a shortcut identical to
setting the parameter option :title
to the same value.
And using the param!
method is identical to setting the parameter option :required
to true
.
The following two declarations are therefore the same:
param! :email, "Email Address"
param :email, title: "Email Address", required: true
Parameters support many more parameter options, and we will discuss each one in turn as we go. Comprehensive summary for an avid reader is however available in Parameter Options.
The value of each parameter is a string by default (or nil
if the parameter is not set at all).
The string size is implicitly limited to 255 characters and bytes by default.
To limit the size explicitly, you can use an optional size parameter like this:
param! :title, "Title", 100
This limits the string to 100 characters and 255 bytes.
That's because
as long as the character size limit is less than or equal to 255,
the implicit 255 bytes limit is retained.
Such setting is most suitable for strings stored in a database as the varchar
type.
If the character size limit is greater than 255, no byte size limit is enforced by default.
Such setting is most suitable for strings stored in a database as the text
type.
Of course, you can set both character and byte size limits yourself like this:
param :text, "Text", 50000, max_bytesize: 65535
This is identical to setting the :max_size
and :max_bytesize
options explicitly.
Similarly, there are the :min_size
and :min_bytesize
counterparts,
which you can use to limit the minimum sizes like this:
param :nick, "Nick Name", min_size: 3, max_size: 8
The size limits are also often used for passwords. Those usually use a bit more options, though:
class PasswordForm
param :password, "Password", min_size: 8, max_size: 16, type: :password,
filter: ->{ chomp }
end
The :filter
option specifies a code block
which is used to preprocess the incoming string value.
By default, all parameters use a filter which squeezes any whitespace into a single space
and then strips the leading and trailing whitespace entirely.
This way the string input is always nice and clean even if the user types some extra spaces somewhere.
For passwords, though, we want to preserve the characters as they are, including spaces.
We could do that by simply setting the :filter
option to nil
.
However, at the same time we want to get rid of the trailing newline character
which is often appended by the browser
when the user cuts and pastes the password from somewhere.
Not doing this would make the password eventually fail for no apparent reason,
resulting in poor user experience.
That's why we use chomp
as the filter above.
The filter block is executed in the context of the string value itself,
so it actually executes String#chomp
to strip the trailing newline if present.
More details about filters will follow in the very next chapter
Internal vs External Representation.
The :type
option shown above is another common option used often.
The FormInput
class itself doesn't care much about it,
but it is passed through to the form templates to make the parameter render properly.
Similarly, the :disabled
option can be used to render the parameter as disabled.
In fact, any parameter option you specify is available in the templates,
so you can pass through arbitrary things like :subtitle
or :help
and use them in the templates any way you like.
It's also worth mentioning that the options
can be evaluated dynamically at runtime.
Simply pass a code block in place of any option value
(except those whose value is already supposed to contain a code block, like :filter
above)
and it will be called to obtain the actual value.
The block is called in context of the form parameter itself,
so it can access any of its methods and its form's methods easily.
For example, you can let the form automatically disable some fields
based on available user permissions by defining the is_forbidden?
method accordingly:
param :avatar, "Avatar",
disabled: ->{ form.is_forbidden?( :avatar ) }
param :comment, "Comment", type: :textarea,
disabled: ->{ form.is_forbidden?( :comment ) }
If you happen to use some option arguments often, you can factor them out and share them like this:
FEATURED_ARGS = { disabled: ->{ form.is_forbidden?( name ) } }
param :avatar, "Avatar", FEATURED_ARGS
param :comment, "Comment", FEATURED_ARGS, type: :textarea
This works since you can actually pass several hashes in place of the options argument and they all get merged together from left to right. This allows you to mix various options presets together and then tweak them further as needed.
Internal vs External Representation
Now when you know how to define some parameters, let's talk about the parameter values a bit. For this, it is important that you understand the difference between their internal and external representations.
The internal representation, as you might have guessed, are the parameter values which you will use in your application. The external representation is how the parameters are present to the browser via HTML forms or URLs and passed back to the server.
Normally, both representations are the same. The parameters are named the same way in both cases and their values are strings in both cases, too. But that doesn't have to be that way.
First of all, it is possible to change the external name of the parameter.
Both param
and param!
methods actually accept
an optional code argument,
which can be used like this:
param! :query, :q, "Query"
This lets you call the parameter query
in your application,
but in forms and URLs it will use its shorter code name q
instead.
This also comes handy when you need to change the external name for some reason,
but want to retain the internal name which your application uses all over the place.
Input Filter
Now the code name was the easy part. The cool part is that the parameter values can have different internal and external representations as well. The external representation is always a string, of course, but we can choose the internal representation at will.
We have already seen above that each parameter has a :filter
option
which is used to preprocess the input string the way we want.
If you don't specify any filter explicitly,
the parameter gets an implicit one which cleans up any whitespace in the input string like this:
filter: ->{ gsub( /\s+/, ' ' ).strip }
Note that parameters which are not present in the web request
are never passed through a filter and
simply remain set to their previous value, which is nil
by default.
The filter therefore only needs to deal with string input values,
not nil
or anything else.
Of course, the filter can do any string processing you need. For example, this filter converts typical product keys into their canonic form:
filter: ->{ gsub( /[\s-]+/, '' ).gsub( /.{5}(?=.)/, '\0-' ).upcase }
However, the truth is that the filter doesn't have to return a string. It can return any object type you want. For example, here is a naive filter which converts any input string into an integer value:
filter: ->{ to_i },
class: Integer
The :class
option is used to tell FormInput
what kind of object is the filter supposed to return.
When set, it is used to validate the input after the conversion,
and any mismatch is reported as an error.
The option accepts an array of object types, too.
This is handy for example when the filter returns boolean values:
filter: ->{ self == "true" },
class: [ TrueClass, FalseClass ]
The naive integer filter shown above works fine as long as the input is correct, but the problem is that it creates integers even from completely incorrect input. If you want to make sure the user didn't make a typo in their input, the following filter is more suitable:
filter: ->{ Integer( self, 10 ) rescue self },
class: Integer
This filter uses more strict conversion which fails in case of invalid input.
In such case the filter uses the rescue
clause
to keep the original string value intact.
This assures that it can be displayed to the user and edited again to fix it.
This is a really good practice -
making sure that even bad input can round trip back to the user -
so you should stick to it whenever possible.
There is one last thing to take care of - an empty input string.
Whenever the user submits the form without entering anything in the input fields,
the browser sends empty strings to the server as the parameter values.
In this regard an empty string is the same as no input as far as the form is concerned,
so both nil
value and empty string are considered as valid input for optional parameters.
The FormInput
normally preserves those values intact so you can distinguish the two cases if you wish.
But in case of the integer conversion it is much more convenient if the empty string gets converted to nil
.
It makes it easier to work with the input value afterwards,
testing for its presence, using the ||=
operator, and so on.
The complete filter for converting numbers to integers should thus look like this:
filter: ->{ ( Integer( self, 10 ) rescue self ) unless empty? },
class: Integer
You can even consider using strip.empty?
if you want to allow an all-whitespace input to be consumed silently.
Of course, all that would be a lot of typing for something as common as integer parameters.
That's why the FormInput
class comes with plenty standard filters predefined:
param :int, INTEGER_ARGS
param :float, FLOAT_ARGS
param :bool, BOOL_ARGS # pulldown style.
param :check, CHECKBOX_ARGS # checkbox style.
You can check the form_input/types.rb
source file to see how they are defined
and either use them directly as they are or use them as a starting point for your own variants.
And that's about it. However, as this chapter is quite important for understanding how the input filters work, let's reiterate:
- You can use filters to convert input parameters into any type you want.
- Make sure the filters keep the original string in case of errors so the user can fix it.
- You don't have to worry about
nil
input values in filters. - Just make sure you treat an empty or blank string as whatever you consider appropriate.
Output Format
Now you know how to convert external values into their internal representation, but that's only half of the story. The internal values have to be converted to their external representation as well, and that's what output formatters are for.
By default, the FormInput
class will use simple to_s
conversion to create the external value.
But you can easily change this by providing your own :format
filter instead:
param :scientific_float, FLOAT_ARGS,
format: ->{ '%e' % self }
The provided block will be called in the context of the parameter value itself
and its result will be passed to the to_s
conversion to create the final external value.
The use of a formatter is more than just mere cosmetics. You will often use the formatter to complement your input filter. For example, this is one possible way of how to map arbitrary external values to their internal representation and back:
SORT_MODES = { id: 'n', views: 'v', age: 'a', likes: 'l' }
SORT_MODE_PARAMETERS = SORT_MODES.invert
param :sort_mode, :s,
filter: ->{ SORT_MODE_PARAMETERS[ self ] || self },
format: ->{ SORT_MODES[ self ] },
class: Symbol
Note that once again the original value is preserved in case of error, so it can be passed back to the user for fixing.
Another example shows how to process a credit card expiration field:
EXPIRY_ARGS = {
placeholder: 'MM/YYYY',
filter: ->{
FormInput.parse_time( self, '%m/%y' ) rescue FormInput.parse_time( self, '%m/%Y' ) rescue self
},
format: ->{ strftime( '%m/%Y' ) rescue self },
class: Time,
}
param :expiry, EXPIRY_ARGS
Note that the formatter won't be called if the parameter value is nil
or if it is already a string when it should be some other type
(for example because the input filter conversion failed),
so you don't have to worry about that.
But it doesn't hurt to add the rescue clause like above
just in case the parameter value is set to something unexpected,
especially if the formatter is supposed to be reused at multiple places.
The FormInput.parse_time
is a helper method which works like Time.strptime
,
except that it fails if the input string contains trailing garbage.
Without this feature, input like 01/2016
would be parsed as 01/20
by '%m/%y'
and interpreted as 01/2020
, which is utterly wrong.
So better use this helper instead if you want your input validated properly.
An added bonus is that it can also ignore the -_^
modifiers after the %
sign,
so you can use the same time format string for both parsing and formatting.
To help you get started,
the FormInput
class comes with several time filters and formatters predefined:
param :time, TIME_ARGS # YYYY-MM-DD HH:MM:SS stored as Time.
param :us_date, US_DATE_ARGS # MM/DD/YYYY stored as Time.
param :uk_date, UK_DATE_ARGS # DD/MM/YYYY stored as Time.
param :eu_date, EU_DATE_ARGS # D.M.YYYY stored as Time.
param :hours, HOURS_ARGS # HH:MM stored as seconds since midnight.
You can use them as they are but feel free to create your own variants instead.
Input Transform
So, there are the :filter
and :format
options to convert the parameter values
from an external to internal representation and back. So far so good.
But the truth is that the FormInput
class supports one additional input transformation.
This transformation is set with the :transform
option
and is invoked after the :filter
filter.
So, what's the difference between :filter
and :transform
?
For scalar values, like normal string or integer parameters, there is none.
In that case the :transform
is just an additional filter,
and you are free to use either or both.
But FormInput
class also supports array and hash parameters,
as we will learn in the very next chapter,
and that's where it makes the difference.
The input filter is used to convert each individual element,
whereas the input transformation operates on the entire parameter value,
and can thus process the entire array or hash as a whole.
What you use the input transformation for is up to you.
The FormInput
class however comes with a predefined PRUNED_ARGS
transformation
which converts an empty string value to nil
and prunes nil
and empty elements from arrays and hashes,
ensuring that the resulting input is free of clutter.
This comes especially handy when used together with array parameters, which we will discuss next.
Array and Hash Parameters
So far we have been discussing only simple scalar parameters,
like strings or integers.
But web requests commonly support the array and hash parameters as well
using the array[]=value
and hash[key]=value
syntax, respectively,
and thus so does the FormInput
class.
To declare an array parameter, use either the array
or array!
method:
array :keywords, "Keywords"
Similarly to param!
, the array!
method creates a required array parameter,
which means that the array must be present and may not be empty.
The array
method on the other hand creates an optional array parameter,
which doesn't have to be filled in at all.
Note that like in case of scalar parameters,
array parameters not found in the input remain set to their default nil
value,
rather than becoming an empty array.
All the parameter options of scalar parameters can be used with array parameters as well.
In this case, however, they apply to the individual elements of the array.
The array parameters additionally support the :min_count
and :max_count
options,
which restrict the number of elements the array can have.
For example, to limit the keywords both in string size and element count, you can do this:
array :keywords, "Keywords", 35, max_count: 20
We have already discussed the input and output filters and input transformation.
The input :filter
and output :format
are applied to the elements of the array,
whereas the input :transform
is applied to the array as a whole.
For example, to get sorted array of integers you can do this:
array :ids, INTEGER_ARGS, transform: ->{ compact.sort rescue self }
The compact
method above takes care of removing any unfilled entries from the array prior sorting.
This is often desirable,
and if you don't need to use your own transformation,
you can use the predefined PRUNED_ARGS
transformation which does the same
and discards both nil
and empty elements:
array :ids, INTEGER_ARGS, PRUNED_ARGS
array :keywords, "Keywords", PRUNED_ARGS
The hash attributes are very much like the array attributes,
you just use the hash
or hash!
method to declare them:
hash :users, "Users"
The biggest difference from arrays is that the hash parameters use keys to address the elements.
By default, FormInput
accepts only integer keys and automatically converts them to integers.
Their range can be restricted by :min_key
and :max_key
options,
which default to 0 and 264-1, respectively.
Alternatively, if you know what are you doing,
you can allow use of non-integer string keys by using the :match_key
option,
which should specify a regular expression
(or an array of regular expressions)
which all hash keys must match.
This may not be the wisest move, but it's your call.
Just make sure you use the \A
and \z
anchors rather than ^
and $
,
so you don't leave yourself open to nasty surprises.
While practical use of hash parameters with forms is relatively limited, the array parameters are pretty common. The examples above could be used for gathering list of input fields into a single array, which is useful as well, but the most common use of array parameters is for multi-select or multi-checkbox fields.
To declare a select parameter, you can set the :type
to :select
and
use the :data
option to provide an array of values for the select menu.
The array contains pairs of parameter values to use and the corresponding text to show to the user.
For example, using a Sequel-like Country
model:
COUNTRIES = Country.all.map{ |c| [ c.code, c.name ] }
param :country, "Country", type: :select, data: COUNTRIES
To turn select into multi-select, basically just change param
into array
and that's it:
array :countries, "Countries", type: :select, data: COUNTRIES
Note that it also makes sense to change the parameter name into the plural form, so we did that.
Now if you want to render this as a list of radio buttons or checkboxes instead,
all you need to do is to change the parameter type to :radio:
or :checkbox
, respectively:
param :country, "Country", type: :radio, data: COUNTRIES
array :countries, "Countries", type: :checkbox, data: COUNTRIES
That's all it takes.
To validate the input, you will likely want to make sure the code received is really a valid country code.
In case of scalar parameters, this can be done easily by using the :check
callback,
which is executed in the context of the parameter itself and can examine the value and do any checks it wants:
check: ->{ report( "%p is not valid" ) unless Country[ value ] }
It can be also done by the :test
callback,
which is executed in the context of the parameter itself as well,
but receives the value to test as an argument.
In case of arrays and hashes, it is passed each element value in turn,
for as long as no error is reported and the parameter remains valid:
test: ->( value ){ report( "%p contain invalid code" ) unless Country[ value ] }
The advantage of the :test
callback is that it works the same way regardless of the parameter kind,
scalar or not,
so it is preferable to use it
if you plan to factor this into a COUNTRY_ARGS
helper which works with both kinds of parameters.
If you do this,
you should also know that each parameter can support multiple :check
and :test
callbacks,
and when the parameter options are merged together, all of them are preserved by default
(unless you set it to nil
, which resets all previously defined ones).
In either case, the report
method is used to report any problems about the parameter,
which marks the parameter as invalid at the same time.
More on this will follow in the chapter Errors and Validation.
Alternatively, you may want to convert the country code into the Country
object internally,
which will take the care of validation as well:
COUNTRY_ARGS = {
data: ->{ Country.all.map{ |c| [ c, c.name ] } },
filter: ->{ Country[ self ] },
format: ->{ code },
class: Country
}
param! :country, "Country", COUNTRY_ARGS, type: :select
Either way is fine, so choose whichever suits you best.
Just note that the data array now contains the Country
objects themselves rather than their country codes,
and that we have opted for creating that array dynamically instead of using a static one.
And remember that it is really wise to factor reusable things like this
into their own helper like the COUNTRY_ARGS
above for easier reuse.
Finally, a little bit of warning.
Note that the web request syntax supports arbitrarily nested hash and array attributes.
The FormInput
class will accept them and apply the input transformations appropriately,
but then it will refuse to validate anything but flat arrays and hashes,
as it is way too easy to shoot yourself in the foot with complex nested structures coming from untrusted source.
The word of advice is just to stay away from those
and let the FormInput
protect you from such input automatically.
But if you think you know what you are doing and really need such a complex input,
you can use the input transformation
to convert it to flat array or hash,
or intercept the validation and handle the parameter yourself,
which will very likely open a can of worms and leave you prone to many problems.
You have been warned.
Reusing Form Parameters
It happens fairly often that you will want to use some form parameters at multiple places.
The FormInput
class provides two ways of dealing with this - form inheritance and parameter copying.
The form inheritance is straightforward. Simply define some form, then inherit from it and add more parameters as needed:
class NewPasswordForm < PasswordForm
param! :password_check, "Repeated Password"
end
Obviously, the practical use of such approach is very limited.
Most often the parameters you want to reuse won't be the first parameters of the form.
For this reason, the FormInput
also supports parameter copying which is way more flexible.
You can copy either entire forms or just select parameters like this:
class SignupForm < FormInput
param! :first_name, "First Name"
param! :last_name, "Last Name"
param! :email, "Email"
copy PasswordForm
end
class ProfileForm < FormInput
copy SignupForm[ :first_name, :last_name ]
param :company, "Company"
param :country, "Country"
end
Parameter copying has another advantage - you can actually pass in options which you want to add or change in the copied versions:
class ChangePasswordForm < FormInput
param! :old_password, "Old Password"
copy PasswordForm, title: "New Password"
end
Just make sure the new options make sense for all the parameters copied.
Creating Forms
Now when you know how to create the FormInput
classes which describe your input parameters,
it's about time you learn how to create the instances of those classes themselves.
We will use the ContactForm
class from the Introduction as an example.
First of all, before there is any external input, you will want to create an empty form input instance:
form = ContactForm.new
Once you have it, you can preset its parameters from a hash with the set
method:
form.set( email: user.email, name: user.full_name ) if user?
If you want to preset the parameters unconditionally,
you may pass the hash directly to the new
method instead:
form = ContactForm.new( email: user.email, name: user.full_name )
You can even ask your models to prefill complex forms without knowing the details:
form = ProfileForm.new( user.profile_hash )
Later on, after you receive the web request containing the input parameters,
just instantiate the form and fill it with the request input
by passing it the Rack::Request
compatible request
argument:
form = ContactForm.new( request )
The initialize
method internally dispatches any Hash
argument to the set
method,
while any other argument is passed to the import
method,
so the above is equivalent to this:
form = ContactForm.new.import( request )
There is a fundamental difference between the set
and import
methods
which you must understand.
The former takes parameters in their internal representation and applies no input processing,
while the latter takes parameters in their external representation and applies input filtering and transformations to them.
It also conveniently ignores any input parameters which the form doesn't define.
On the contrary, the set
method can be used to set any attributes of the instance,
even those which are not the form parameters.
Normally, it is pretty safe to simply use the new
method alone.
You have to make sure to use the import
method explicitly only
if you have a hash with parameters in their external representation which you want processed.
This can happen for example if you want to use Sinatra's params
hash
to include parts of the URL as the form input:
get '/contact/:email' do
form = ContactForm.new( params ) # NEVER EVER DO THIS!
form = ContactForm.new.import( params ) # Do this instead (or use from_params).
...
end
Similarly, you want to use import
when feeding form input with JSON data (see JSON Helpers for details):
post '/api/v1/contacts' do
form = ContactForm.new( json_data ) # NEVER EVER DO THIS!
form = ContactForm.new.import( json_data ) # Do this instead (or use from_data).
...
end
If you are worried that you might make a mistake, you can use one of the four helper shortcuts which make it easier to remember which one to use when:
form = ContactForm.from_request( request ) # Like new.import, for Rack request with external values.
form = ContactForm.from_params( params ) # Like new.import, for params hash of external values.
form = ContactForm.from_data( json_data ) # Like new.import, for JSON hash of external values.
form = ContactForm.from_hash( some_hash ) # Like new.set, for hash of internal values.
Regardless of how you create the form instance,
if you later decide to clear some parameters, you can use the clear
method.
You can either clear the entire form, named parameters, or parameter subsets (which we will discuss in detail later):
form.clear
form.clear( :message )
form.clear( :name, :company )
form.clear( form.disabled_params )
The unset
method works the same way, except that it always requires an argument:
form.unset( :message )
form.unset( :name, :company )
form.unset( form.invalid_params )
Alternatively, you can create form copies with just a subset of parameters set:
form.only( :email, :message )
form.only( form.required_params )
form.except( :message )
form.except( form.hidden_params )
Of course, creating copies with either dup
or clone
works as well.
In either case, you now have your form with the input parameters set, and you are all eager to use it. But before we discuss how to do that, you need to learn about errors and input validation.
Errors and Validation
Input validation is a must. It's impossible to overstate how important it is. Many applications opt for letting the models do the validation for them, but that's often way too late. Besides, lot of input is not intended for models at all.
The FormInput
class therefore helps you validate all input as soon as possible instead,
before you even touch it.
All you need to do is to call the valid?
method
and refrain from using the input unless it returns true
:
return unless form.valid?
Of course, you don't have to give up right away.
The FormInput
class does all it can so even the invalid input is preserved intact
and can be fed back to the form template so the user can fix it.
The fact that the form says the input is not valid doesn't mean you can't access it.
It's perfectly safe to render the form parameters back in the form template
and it is the intended use.
Just make sure you don't use the invalid input the way you normally would, that's all.
The input validation works by testing the current value of each parameter against several validation criteria. As soon as any of these validation restrictions is not met, an error message describing the problem is reported and remembered for that parameter and next parameter is tested. Any parameter with an error message reported is considered invalid for as long as the error message remains on record. The entire form is considered invalid as long as any of its parameters are invalid.
It's important to realize that the input validation protection is only as effective as the individual validation restrictions you place on your parameters. When defining your parameters, always think of how you can restrict them. It's always better to add too many restrictions than too little and leave yourself open to exploits caused by unchecked input.
So, what kind of validations are available?
We have already discussed the required vs optional parameters.
The former are required to be present and non-empty.
Empty or nil
parameter values are allowed only if the parameters are optional.
Unless it is nil
, the value must also match the parameter kind (string, array or hash).
Note that the FormInput
provides default error messages for any problems detected,
but you can set a custom error message for required parameters with the :required_msg
option:
param! :login, "Login Name",
required_msg: "Please fill in your Login Name"
We have also discussed the string character and byte size limits,
which are controlled by :min_size
, :max_size
, :min_bytesize
, and :max_bytesize
options, respectively.
The array and hash parameters additionally support the :min_count
and :max_count
options,
which limit the number of elements.
The hash parameters also support the :min_key
and :max_key
limits to control the range of their integer keys,
plus the :match_key
pattern(s) to enable restricted use of non-integer string keys.
What we haven't discussed yet are the :min
and :max
limits.
When used, these enforce that the input values are
not less than or greater than given limit, respectively.
Similarly, the :inf
and :sup
limits (from infimum and supremum)
ensure that the input values are
greater than and less than given limit, respectively.
Note that any of these work with both strings and Numeric types,
as well as anything which responds to the to_f
method:
param :age, INTEGER_ARGS, min: 1, max: 200 # 1 <= age <= 200
param :rate, FLOAT_ARGS, inf: 0, sup: 1 # 0 < rate < 1
Additionally, you may specify a regular expression or an array of regular expressions
which the input values must match using the :match
option.
If you intend to match the input in its entirety,
make sure you use the \A
and \z
anchors rather than ^
and $
,
so a newline in the input doesn't let an unexpected input sneak in:
param :nick, match: /\A[a-z]+\z/i
Custom error message if the match fails can be set with the :msg
or :match_msg
options:
param :password,
match: [ /[A-Z]/, /[a-z]/, /\d/ ],
msg: "Password must contain one lowercase and one uppercase letter and one digit"
Similarly to :match
, you may specify a regular expression or an array of regular expressions
which the input values may not match using the :reject
option.
Custom error message if this fails can be set with the :msg
or :reject_msg
options:
param :password,
reject: /\P{ASCII}|[\t\r\n]/u,
reject_msg: "Password may contain only ASCII characters and spaces",
Of course, prior to all this, the FormInput
also ensures
that the strings are in valid encoding and don't contain weird control characters,
so you don't have to worry about that at all.
Alternatively,
for parameters which use a custom object type instead of a string,
the :class
option ensures that the object is of the correct type instead.
Now, any violation of these restrictions is automatically reported as an error.
Note that FormInput
normally reports only the first error detected per parameter,
but you can report arbitrary number of custom errors for given parameter
using the report
method.
This comes handy as it allows you to pass the form into your models
and let them report any belated additional errors which might get detected during the database transaction,
for example:
form.report( :email, "Email address is already taken" ) unless unique_email?( form.email )
If you want the error message to have priority over anything else what might have been reported before,
you can use the report!
method instead:
form.filled_params.each{ |p| p.report!( "Fill only one of these" ) } unless form.filled_params.one?
As we have already seen before, it is common to use the report
method from within the :check
or :test
callback of the parameter itself as well:
check: ->{ report( "%p is odd number" ) unless value.to_i.even? }
test: ->( value ){ report( "%p contain odd number" ) unless value.to_i.even? }
In this case the %p
string is replaced by the title
of the parameter.
If the parameter has the :error_title
option set, it is used preferably instead.
If neither is set, it fallbacks to the parameter code
name instead.
You can get hash of all errors reported for each parameter from the errors
method,
or list consisting of first error message for each parameter from the error_messages
method:
form.errors # { email: [ "Email address is already taken" ] }
form.error_messages # [ "Email address is already taken" ]
You can get all errors or first error for given parameter by using
the errors_for
or error_for
method, respectively:
form.errors_for( :email ) # [ "Email address is already taken" ]
form.error_for( :email ) # "Email address is already taken"
As we have seen, you can test the validity of the entire form with the valid?
or invalid?
methods.
You can use those methods for testing validity of given parameter or parameters, too:
form.valid?
form.invalid?
form.valid?( :email )
form.invalid?( :name, :message )
form.valid?( form.required_params )
form.invalid?( form.hidden_params )
The validation is run automatically when you first access any of
the validation related methods mentioned above,
so you don't have to worry about its invocation at all.
But you can also invoke it explicitly by calling validate
, validate?
or validate!
methods.
The validate
method is the standard variant which validates all parameters.
If any errors were reported before already, however, it leaves them intact.
The validate?
method is a lazy variant which invokes the validation only if it was not invoked yet.
The validate!
method on the other hand always invokes the validation,
wiping any previously reported errors first.
In either case any errors collected will remain stored
until you change any of the parameter values with set
, unset
, clear
, or []=
methods,
or you explicitly call validate!
.
Copies created with dup
(but not clone
), only
, and except
methods
also have any errors reported before cleared.
All this ensures you automatically get consistent validation results anytime you ask for them.
The only exception is when you set the parameter values explicitly using their setter methods.
This intentionally leaves the errors reported intact,
allowing you to adjust the parameter values
without interfering with the validation results.
Which finally brings us to the topic of accessing the parameter values themselves.
Using Forms
So, now when you have verified that the input is valid, let's finally use it.
The FormInput
classes use standard instance variables
for keeping the parameter values,
along with standard read and write accessors.
The simplest way is thus to access the parameters by their name as usual:
form.email
form.message ||= "Default text"
Note that the standard accessors are defined for you when you declare the parameter,
but you are free to provide your own if you want to.
For example, if you want a parameter to always have some default value instead of the default nil
,
this is the simplest way to do it:
param :sort_mode, :s
def sort_mode
@sort_mode || 'default'
end
Another way how to access the parameter values is to use the hash-like interface. Note that it can return an array of multiple attributes at once as well:
form[ :email ]
form[ :name ] = user.name
form[ :first_name, :last_name ]
Of course, this interface is often used when you need to access the parameter values programatically,
without knowing their exact name.
The form provides names of all parameters via its params_names
or parameters_names
methods,
so you can do things like this:
form.params_names.each{ |name| puts "#{name}: #{form[ name ].inspect}" }
Sometimes, you may want to use some chosen parameters as long as they are all valid,
even if the entire form may be not.
You can do this by using the valid
method,
which returns the requested values only if they are all valid.
Otherwise it returns nil
.
return unless email = form.valid( :email )
first_name, last_name = form.valid( :first_name, :last_name )
To find out if no parameter values are filled at all, you can use the empty?
method:
form.set( email: user.email ) if form.empty?
The parameters are more than their value, though,
so the FormInput
allows you to access the parameters themselves as well.
You can get a single named parameter from the param
or parameter
methods,
list of named parameters from the named_params
or named_parameters
methods,
or all parameters from the params
or parameters
methods,
respectively:
p = form.param( :message )
p1, p2 = form.named_params( :email, :name )
list = form.params
Once you get hold of the parameter, you can query it about lot of things.
First of all, you can ask it about its name
, code
, title
or value
:
p = form.params.first
puts p.name
puts p.code
puts p.title
puts p.value
All parameter options are available via its opts
hash.
However, it is preferable to query them via the []
operator,
which also resolves the dynamic options
and can support localized variants as well:
puts p[ :help ] # Use this ...
puts p.opts[ :help ] # ... rather than this.
The parameter also knows about the form it belongs to,
so you can get back to it using the form
method if you need to:
fail unless p.form.valid?
As we have seen, you can report errors about the parameter using its report
or report!
methods.
You can ask it about all its errors or just the first error using the errors
or error
methods, respectively:
p.report( "This is invalid" )
p.errors # [ "This is invalid" ]
p.error # "This is invalid"
p.report!( "Do not fill this!" )
p.errors # [ "Do not fill this!", "This is invalid" ]
p.error # "Do not fill this!"
You can also simply ask whether the parameter is valid or not by using the valid?
and invalid?
methods.
In fact, the parameter has a dozen of simple boolean getters like this which you can use to ask it about many things:
p.valid? # Does the parameter have no errors reported?
p.invalid? # Does the parameter have some errors reported?
p.blank? # Is the value nil or empty or whitespace only string?
p.empty? # Is the value nil or empty?
p.filled? # Is the value neither nil nor empty?
p.set? # Was the parameter value set to something?
p.unset? # Was the parameter value not set to anything?
p.required? # Is the parameter required?
p.optional? # Is the parameter not required?
p.disabled? # Is the parameter disabled?
p.enabled? # Is the parameter not disabled?
p.hidden? # Is the parameter type :hidden?
p.ignored? # Is the parameter type :ignore?
p.visible? # Is the parameter type neither :hidden nor :ignore?
p.array? # Was the parameter declared as an array?
p.hash? # Was the parameter declared as a hash?
p.scalar? # Was the parameter declared as a simple param?
p.correct? # Does the value match param/array/hash kind?
p.incorrect? # Doesn't the value match the parameter kind?
Building upon these boolean getters,
the FormInput
instance lets you get a list of parameters of certain type.
The following methods are available:
form.valid_params # Parameters with no errors reported.
form.invalid_params # Parameters with some errors reported.
form.blank_params # Parameters with nil, empty, or blank value.
form.empty_params # Parameters with nil or empty value.
form.filled_params # Parameters with some non-empty value.
form.set_params # Parameters whose value was set to something.
form.unset_params # Parameters whose value was not set to anything.
form.required_params # Parameters which are required and have to be filled.
form.optional_params # Parameters which are not required and can be nil or empty.
form.disabled_params # Parameters which are disabled and shall be rendered as such.
form.enabled_params # Parameters which are not disabled and are rendered normally.
form.hidden_params # Parameters to be rendered as hidden in the form.
form.ignored_params # Parameters not to be rendered at all in the form.
form.visible_params # Parameters rendered normally in the form.
form.array_params # Parameters declared as an array parameter.
form.hash_params # Parameters declared as a hash parameter.
form.scalar_params # Parameters declared as a simple scalar parameter.
form.correct_params # Parameters whose current value matches their kind.
form.incorrect_params # Parameters whose current value doesn't match their kind.
Each of them simply selects the parameters using their boolean getter of the same name.
Each of them is available in the *_parameters
form for as well,
for those who don't like the params
shortcut.
As you can see, this allows you to get many parameter subsets,
but sometimes even that is not enough.
For this reason, parameters also support the so-called tagging,
which allows you to group them by any additional criteria you need.
Simply tag a parameter with one or more tags using either the :tag
or :tags
option:
param :age, tag: :indecent
param :ratio, tags: [ :knob, :limited ]
Note that the parameter tags can be also generated dynamically the same way as any other option, but once accessed, their value is frozen for that parameter instance afterwards, both for performance reasons and to prevent their inconsistent changes.
You can ask the parameter for an array of its tags with the tags
method.
Note that it returns an empty array if the parameter was not tagged at all.
Rather than using the tags array directly, though,
it's easier to test parameter's tags using its tagged?
and untagged?
methods:
p.tagged? # Tagged with some tag?
p.untagged? # Not tagged at all?
p.tagged?( :indecent ) # Tagged with this tag?
p.untagged?( :limited ) # Not tagged with this tag?
p.tagged?( :indecent, :limited ) # Tagged with any of these tags?
p.untagged?( :indecent, :limited ) # Not tagged with any of these tags?
You can get the desired parameters using the form's tagged_params
and untagged_params
methods, too:
form.tagged_params # Parameters with some tag.
form.untagged_params # Parameters with no tag.
form.tagged_params( :indecent ) # Parameters tagged with this tag.
form.untagged_params( :limited ) # Parameters not tagged with this tag.
form.tagged_params( :indecent, :limited ) # Parameters with either of these tags.
form.untagged_params( :indecent, :limited ) # Parameters with neither of these tags.
What you use this for is up to you. We will see later that for example the Multi-Step Forms use this for grouping parameters which belong to individual steps, but it has plenty other uses as well.
URL Helpers
The FormInput
is primarily intended for use with HTML forms,
which we will discuss in detail in the Form Templates chapter,
but it can be used for processing any web request input,
regardless of if it comes from a form post or from the URL query string.
It is therefore quite natural that the FormInput
provides helpers for generating
URL query strings as well in addition to helpers used for form creation.
You can get a hash of filled parameters suitable for use in the URL query by using the url_params
(AKA to_params
) method,
or get them combined into the URL query string by using the url_query
method.
Note that the url_params
result differs considerably from the result of the to_hash
method,
as it uses parameter code rather than name for keys and their external representation for the values:
class MyInput < FormInput
param :query, :q
array :feeds, INTEGER_ARGS
end
input = MyInput.new( query: "abc", feeds: [ 1, 7 ] )
input.to_hash # { query: "abc", feeds: [ 1, 7 ] }
input.url_params # { q: "abc", feeds: [ "1", "7" ] }
input.url_query # "q=abc&feeds[]=1&feeds[]=7"
Unless you want to construct the URL yourself,
you can use the extend_url
method to let the FormInput
create the URL for you:
input.extend_url( "/search" ) # "/search?q=abc&feeds[]=1&feeds[]=7"
input.extend_url( "/search?e=utf8" ) # "/search?e=utf8&q=abc&feeds[]=1&feeds[]=7"
Note that this works well together with the only
and except
methods,
which allow you to control which arguments get included:
input.only( :query ).extend_url( "/search" ) # "/search?q=abc"
input.except( :query ).extend_url( "/search" ) # "/search?feeds[]=1&feeds[]=7"
You can use this for example to create an URL suitable for redirection which retains only valid parameters when some parameters are not valid:
# In your class:
def valid_url( url )
only( valid_params ).extend_url( url )
end
# In your route handler:
input = MyInput.new( request )
redirect input.valid_url( request.path ) unless input.valid?
If you want to temporarily adjust some parameters just for the creation of a single URL,
you can use the build_url
method, which combines current parameters with the provided ones:
input.build_url( "/search", query: "xyz" ) # "/search?q=xyz&feeds[]=1&feeds[]=7"
Finally, if you do not like the idea of parameter arrays in your URLs, you can use something like this instead:
param :feeds,
filter: ->{ split.map( &:to_i ) },
format: ->{ join( ' ' ) },
class: Array
input.url_params # { q: "abc", feeds: "1 7" }
input.url_query # "q=abc&feeds=1+7"
Just note that none of the standard array parameter validations apply in this case,
so make sure to apply your own validations using the :check
callback if you need to.
JSON Helpers
The Form Input
can also help a great deal with validating JSON data commonly used in REST API endpoints.
There are two methods which are quite useful in this case, from_data
and to_data
.
The former imports the data from an external input hash,
which may contain data in either internal or external form,
while the latter creates the hash containing the data in their canonic internal form.
When you create a form input from request or params, the external parameter values are always strings.
But in case of JSON data, the values can be also numbers, booleans or nil
.
Fortunately, the import
and from_data
methods can deal with such input as well.
The only difference is that the input filter is not run in such case.
The input transform is run as usual,
and so are all the validation methods.
Among other things this means that you can declare a parameter as integer using the INTEGER_ARGS
macro
and pass in the value as either string or integer, and you end up with integer in both cases, which is pretty convenient.
Of course, this works similarly for FLOAT_ARGS
and BOOL_ARGS
, too:
class NumericInput < FormInput
param :int, INTEGER_ARGS
param :float, FLOAT_ARGS
end
NumericInput.from_data( int: '10', float: 3.0 ).to_data # { int: 10, float: 3.0 }
NumericInput.from_data( int: 10, float: '3.0' ).to_data # { int: 10, float: 3.0 }
Another difference when dealing with JSON data is that
the empty strings have completely different significance than in the case of the web forms.
For this reason, the result of the to_data
method includes any set parameters,
even if the values are empty strings, arrays, or hashes
(unlike the to_hash
and to_params
methods).
Even the nil
values are included for parameters which were explicitly set to nil
:
class OptionalInput < FormInput
param :string
array :array
hash :hash
end
OptionalInput.from_data( string: '' ).to_data # { string: '' }
OptionalInput.from_data( string: nil ).to_data # { string: nil }
OptionalInput.from_data( array: [] ).to_data # { array: [] }
OptionalInput.from_data( hash: {} ).to_data # { hash: {} }
The whole JSON processing may in the end look something like this:
require 'oj'
def json_data
data = Oj.load( request.body, mode: :strict, symbolize_keys: true )
halt( 422, 'Invalid data' ) unless Hash === data
data
rescue Oj::Error
halt( 422, 'Invalid JSON' )
end
post '/api/v1/contacts' do
input = ContactForm.from_data( json_data )
halt( 422, "Invalid data: #{input.error_messages.first}" ) unless input.valid?
# Somehow use the input.
entry = Contact.create( input.to_data )
content_type :json
[ 201, Oj.dump( entry.to_hash ) ]
end
Not bad for having completely validated and converted input data, is it?
Form Helpers
It may come as a surprise, but FormInput
provides no helpers for creating HTML tags.
That's because doing so would be a completely futile effort.
No tag helper will suit all your needs when it comes to form creation.
Instead, FormInput
provides several helpers
which allow you to easily create the forms in the templating engine of your choice.
This has many advantages.
In particular, it allows you to nest the HTML tags exactly the way you want,
style it using whatever classes you want, and include any extra bits the way you want.
Furthermore, it allows you to have templates for rendering the parameters in several styles
and choose among them as you need.
All this and more will be discussed in detail in the Form Templates chapter, though.
This chapter just describes the form helpers themselves.
You can ask each form parameter about how it should be rendered by using its type
method,
which defaults to :text
if the option :type
is not set.
Furthermore,
you can ask each form parameter for the appropriate name and value attributes
to use in form elements by using the form_name
and form_value
methods.
The simplest form parameters can be thus rendered in Slim like this:
input type=p.type name=p.form_name value=p.form_value
For array parameters, the form_value
returns an array of values,
so it is rendered like this:
- for value in p.form_value
input type=p.type name=p.form_name value=value
Finally, for hash parameters, the form_value
returns an array of keys and values,
and keys are passed to form_name
to create the actual name:
- for key, value in p.form_value
input type=p.type name=p.form_name( key ) value=value
For parameters which require additional data,
like select, multi-select, or multi-checkbox parameters,
you can ask for the data using the data
method.
It returns pairs of allowed parameter values together with their names.
If the :data
option is not set, it returns an empty array.
The values can be passed to the selected?
method to test if they are currently selected,
and then must be passed to the format_value
method to turn them into their external representation.
To illustrate all this, a select parameter can be rendered like this:
select name=p.form_name multiple=p.array?
- for value, name in p.data
option selected=p.selected?( value ) value=p.format_value( value ) = name
Finally, you will likely want to render the parameter name in some way.
For this, each parameter has the form_title
method,
which returns the title to show in the form.
It defaults to its title, but can be overridden with the :form_title
option.
If neither is set, the code name will be used instead.
To render it, you will use something like this:
label
= p.form_title
input type=p.type name=p.form_name value=p.form_value
Of course, you are free to use any other parameter methods as well. Want to render the parameter disabled? Add some placeholder text? It's as simple as adding this to your template:
input ... disabled=p.disabled? placeholder=p[:placeholder]
And that's about it. Check out the Form Templates chapter if you want to see more form related tips and tricks.
Extending Forms
While the FormInput
comes with a lot of functionality built in,
you will eventually want to extend it further to better fit your project.
To do this, it's common to define the Form
class inherited from FormInput
,
put various helpers there, and base your own forms on that.
This is also the place where you can include your own FormInput
types extensions.
This chapter shows some ideas you may want to build upon to get you started.
Adding custom boolean getters which you may need:
# Test if the form input which the user can't fix is malformed.
def malformed?
invalid?( hidden_params )
end
Adding custom URL helpers, see URL Helpers for details:
# Add valid parameters to given URL.
def valid_url( url )
only( valid_params ).extend_url( url )
end
Keeping track of additional form state:
# Hook into the request import so we can test form posting.
def import( request )
@posted ||= request.respond_to?( :post? ) && request.post?
super
end
# Test if the form content was posted with a post request.
def posted?
@posted
end
# Explicitly mark the form as posted, to enforce the post action to be taken.
# Returns self for chaining.
def posted!
@posted = true
self
end
Adding support for your own types:
MONEY_ARGS = {
filter: ->{ ( Money( self ) rescue self ) unless empty? },
format: ->{ Money( self ) },
class: Money
}
The list could go on and on, as everyone might need slightly different tweaks. Eventually, though, you will come up with your own set of extensions which you will keep using across projects. Once you do, consider sharing them with the rest of the world. Thanks.
Parameter Options
This is a brief but comprehensive summary of all parameter options:
-
:name
- not really a parameter option, this can be used to change the parameter name and code name when copying form parameters. See Reusing Form Parameters. -
:code
- not really a parameter option, this can be used to change the parameter code name when copying form parameters. See Reusing Form Parameters. -
:title
- the title of the parameter, the default value shown in forms and error messages. -
:form_title
- the title of the parameter to show in forms. Overrides:title
when present. -
:error_title
- the title of the parameter to use in error messages containing%p
. Overrides:title
when present. -
:required
- flag set when the parameter is required. -
:required_msg
- custom error message used when the required parameter is not filled in. Default error message is used if not set. -
:disabled
- flag set when the parameter shall be rendered as disabled. Note that it doesn't affect it in any other way, in particular it doesn't prevent it from being set or being invalid. -
:array
- flag set for array parameters. -
:hash
- flag set for hash parameters. -
:type
- type of the form parameter used for form rendering. Defaults to:text
if not set. Other common values are:password
,:textarea
,:select
,:checkbox
,:radio
. Somewhat special values are:hidden
and:ignore
. -
:data
- array containing data for parameter types which need one, like select, multi-select, or multi-checkbox. Shall contain the allowed parameter values paired with the corresponding text to display in forms. Defaults to empty array if not set. -
:tag
or:tags
- arbitrary symbol or array of symbols used to tag the parameter with arbitrary semantics. See thetagged?
method in the Using Forms chapter. -
:filter
- callback used to cleanup or convert the input values. See Input Filter. -
:transform
- optional callback used to further convert the input values. See Input Transform. -
:format
- optional callback used to format the output values. See Output Format. -
:class
- object type (or array thereof) which the input filter is expected to convert the input value into. See Input Filter. -
:check
- optional callback (or array thereof) used to perform arbitrary checks when testing the parameter validity. See Errors and Validation. -
:test
- optional callback (or array thereof) used to perform arbitrary tests when testing validity of each parameter value. See Errors and Validation. -
:min_key
- minimum allowed value for keys of hash parameters. Defaults to 0. -
:max_key
- maximum allowed value for keys of hash parameters. Defaults to 264-1. -
:match_key
- regular expression (or array thereof) which all hash keys must match. Disabled by default. -
:min_count
- minimum allowed number of elements for array or hash parameters. -
:max_count
- maximum allowed number of elements for array or hash parameters. -
:min
- when set, value(s) of that parameter must be greater than or equal to this. -
:max
- when set, value(s) of that parameter must be less than or equal to this. -
:inf
- when set, value(s) of that parameter must be greater than this. -
:sup
- when set, value(s) of that parameter must be less than this. -
:min_size
- when set, value(s) of that parameter must have at least this many characters. -
:max_size
- when set, value(s) of that parameter may have at most this many characters. Defaults to 255. -
:min_bytesize
- when set, value(s) of that parameter must have at least this many bytes. -
:max_bytesize
- when set, value(s) of that parameter may have at most this many bytes. Defaults to 255 if:max_size
is a dynamic option or less than 256. -
:reject
- regular expression (or array thereof) which the parameter value(s) may not match. -
:reject_msg
- custom error message used when the:reject
check fails. Defaults to:msg
message. -
:match
- regular expression (or array thereof) which the parameter value(s) must match. -
:match_msg
- custom error message used when the:match
check fails. Defaults to:msg
message. -
:msg
- default custom error message used when either of:match
or:reject
checks fails. Default error message is used if not set. -
:inflect
- explicit inflection string used for localization. Defaults to combination of:plural
and:gender
options, see Localization for details. -
:plural
- explicit grammatical number used for localization. See Localization for details. Defaults tofalse
for scalar parameters and totrue
for array and hash parameters. -
:gender
- grammatical gender used for localization. See Localization for details. -
:row
- used for grouping several parameters together, usually to render them in a single row. See Chunked Parameters. -
:cols
- optional custom option used to set span of the parameter in a single row. See Chunked Parameters. -
:group
- custom option used for grouping parameters in arbitrary ways. See Grouped Parameters. -
:size
- custom option used to set size of:select
and:textarea
parameters. -
:subtitle
- custom option used to render an additional subtitle after the form title. -
:placeholder
- custom option used for setting the placeholder attribute of the parameter. -
:help
- custom option used to render a help block explaining the parameter. -
:text
- custom option used to render an arbitrary text associated with the parameter.
Note that the last few options listed above are not used by the FormInput
class itself,
but are instead used to pass additional data to snippets used for form rendering.
Feel free to extend this further if you need to pass additional data this way yourself.
Form Templates
The FormInput
form rendering is based on the power of standard templates,
the same ones which are used for page rendering.
It builds upon the set of form helpers described in the Form Helpers chapter.
This chapter shows several typical form templates and how they are supposed to be created and used.
First of all, the templates are based on the concept of snippets,
which allows the individual template pieces to be reused at will.
Chances are your framework already has support for snippets -
if not, it's usually trivial to build it upon the provided template rendering functionality.
For example, this is a snippet
helper based on Sinatra's partials:
# Render partial, our style.
def snippet( name, opts = {}, **locals )
partial( "snippets/#{name}", opts.merge( locals: locals ) )
end
And here is the same thing for Ramaze:
# Render partial, our way.
def snippet( name, *args )
render_partial( "snippets/#{name}", *args )
end
Whatever you decide to use, the following examples will simply assume that
the snippet
method renders the specified template,
while making the optionally provided hash of values accessible as local variables in that template.
We will use Slim templates in these examples,
but you could use the same principles in HAML or any other templating engine as well.
Also note that you can find the example templates discussed here in the form_input/example/views
directory.
Form Template
To put the form on a page, you use the stock HTML form
tag.
The snippets will be used for rendering of the form content,
but the form itself and the submission buttons used are usually form specific anyway,
so it is rarely worth factoring it out.
Assuming the controller passes the form to the view in the @form
variable,
simple form using standard Bootstrap styling could look like this:
form *form_attrs
fieldset
== snippet :form_simple, params: @form.params
button.btn.btn-default type='submit' Submit
As you can see, the whole form is just a little bit of scaffolding,
with the bulk rendered by the form_simple
snippet.
Choosing different snippets allows us to render the form content in different styles easily.
In this case, we are passing in all form parameters as they are,
but note that we could as easily split them or filter them as needed
and render each group differently if necessary.
Note that we are also using the form_attrs
helper to set the action
and method
tag attributes to their default values.
It's a recommended practice to set these explicitly,
so we may as well use a helper which does this consistently everywhere.
For Sinatra, the helper may look like this:
# Get hash with default form attributes, optionally overriding them as needed.
def form_attrs( url = request.path, **opts )
{ action: url.to_s, method: :post }.merge( opts )
end
If you want to use the CSRF protection provided by Rack::Protection
,
note that you will need to add something like this to the form fieldset:
input type='hidden' name='authenticity_token' value=session[:csrf]
To save some typing and to keep things DRY,
you may turn this into a form_token
helper and call that instead.
Just make sure the token value is properly HTML escaped.
Simple Parameters
Now let's have a look at the snippet rendering the form parameters.
It obviously needs to get the list of the parameters to render.
We will pass them in in the params
variable.
For convenience, we will treat nil
as an empty list, too.
In addition to that, you will want to control if the errors should be displayed or not.
When the form is displayed for the first time, before the user posts anything,
no errors should be displayed,
but you may want to suppress it explicitly in other cases as well.
We will control this by using the report
variable.
For convenience, we will provide reasonable default which automatically asks the current request if it was posted or not.
Finally, you will likely want some control over the form autofocus.
For maximum control, we will allow passing in the parameter to focus on in the focused
variable.
For convenience, though, we will by default autofocus on the first invalid or unfilled parameter,
unless focusing is explicitly disabled by setting focus
to false.
The snippet prologue which does all this may look like this:
- params ||= []
- focus ||= focus.nil?
- report ||= report.nil? && request.post?
- focused ||= params.find{ |x| x.invalid? } || params.find{ |x| x.blank? } || params.first if focus
To demonstrate the basics, we will render only the simple scalar parameters.
As for styling, we will choose a simple Bootstrap block style,
with the parameter name rendered within the input field itself
using the placeholder
attribute.
This is something you can often see on compact login pages,
even though it's a practice which is not really ARIA friendly.
But as an example it illustrates the possibility to tweak the rendering any way you see fit just fine:
- for p in params
- case p.type
- when :ignore
- when :hidden
input type='hidden' name=p.form_name value=p.form_value
- else
.form-group
input.form-control[
type=p.type
name=p.form_name
value=p.form_value
placeholder=p.form_title
disabled=p.disabled?
autofocus=(p == focused)
]
- if report and error = p.error
.help-block
span.text-danger = error
As you can see, the snippet uses the type
attribute to distinguish between the ignored, hidden, and visible parameters.
In further chapters we will see how this can be used to add support for rendering of other parameter types,
like check boxes or pull down menus.
But first we will explore how to properly render array or hash parameters.
Hidden Parameters
The FormInput
supports more than scalar parameter types.
As described in the Array and Hash Parameters chapter,
the parameters can also contain data stored as arrays or hashes.
This chapter shows how to render such parameters properly.
To focus on the basics, without any complexities getting in the way, we will use a snippet rendering all parameters as hidden ones as an example. This is something which is used fairly often, basically whenever you need to pass some data along within the form without the user seeing them. The Rendering Multi-Step Forms chapter is a nice example which builds upon this.
The prologue of this snippet is simple, as we need no error reporting nor autofocus handling:
- params ||= []
The rendering itself is pretty simple as well. It is free of any styling, it's just the basic use of parameter's rendering methods as described in the Form Helpers chapter. You may want to review it after you have seen them used in some context:
- for p in params
- next if p.ignored?
- next unless p.filled?
- if p.array?
- for value in p.form_value
input type='hidden' name=p.form_name value=value
- elsif p.hash?
- for key, value in p.form_value
input type='hidden' name=p.form_name(key) value=value
- else
input type='hidden' name=p.form_name value=p.form_value
Having seen the basics, we are now ready to start expanding them towards more complex snippets.
Complex Parameters
Forms are often more than just few simple text input fields,
so it is necessary to render more complex parameters as well.
To do that,
we will be basically adding code to the p.type
switch of the following rendering loop:
- for p in params
- next if p.ignored?
- if p.hidden?
== snippet :form_hidden, params: [p]
- else
.form-group
- case p.type
- when ...
- else
label
= p.form_title
input.form-control[
type=p.type
name=p.form_name
value=p.form_value
autofocus=(p == focused)
disabled=p.disabled?
placeholder=p[:placeholder]
]
- if report and error = p.error
.help-block
span.text-danger = error
Note how it reuses the snippet we have just described in Hidden Parameters
to render all kinds of hidden parameters.
Other than that, however, as it is, the loop renders just normal input fields, like :text
or :password
.
So let's extend it right now.
Text Area
Text area is basically just a larger text input field with multiline support.
- when :textarea
label
= p.form_title
textarea.form-control[
name=p.form_name
autofocus=(p == focused)
disabled=p.disabled?
rows=p[:size]
] = p.form_value
Note how we use the :size
option to control the size of the area.
Select and Multi-Select
Select and multi-select allow choosing one or many items from a list of options, respectively.
They are rendered the same way, the only difference is the multiple
tag attribute.
Thanks to this we can choose between them easily -
we use normal select for scalar parameters and multi-select for array parameters:
- when :select
label
= p.form_title
select.form-control[
name=p.form_name
multiple=p.array?
autofocus=(p == focused)
disabled=p.disabled?
size=p[:size]
]
- for value, name in p.data
option selected=p.selected?(value) value=p.format_value(value) = name
The data to render comes from the data
attribute of the parameter,
see the Array and Hash Parameters chapter for details.
Also note how the value is passed to the selected?
method
and how it is formatted by the format_value
method.
Radio Buttons
Radio buttons are for choosing one item from a list of options. In this regard they are similar to select parameters, just their appearance in the form is different:
- when :radio
= p.form_title
- for value, name in p.data
label
input.form-control[
type=p.type
name=p.form_name
value=p.format_value(value)
autofocus=(p == focused)
disabled=p.disabled?
checked=p.selected?(value)
]
= name
Like in case of select parameters,
the data to render comes from the data
attribute.
The selected?
and format_value
methods are used the same way, too.
Checkboxes
Checkboxes can be used in two ways. You can either use them as individual on/off checkboxes, or use them as an alternative to multi-select. Their rendering follows this - we use the on/off approach for scalar parameters, and the multi-select one for array parameters:
- when :checkbox
- if p.array?
= p.form_title
- for value, name in p.data
label
input.form-control[
type=p.type
name=p.form_name
value=p.format_value(value)
autofocus=(p == focused)
disabled=p.disabled?
checked=p.selected?(value)
]
= name
- else
label
- if p.title
= p.form_title
input.form-control[
type=p.type
name=p.form_name
value='true'
autofocus=(p == focused)
disabled=p.disabled?
checked=p.value
]
= p[:text]
As you can see, the multi-select case is basically identical to rendering of radio buttons, only the input type attribute changes. For on/off checkboxes, though, there are more changes.
First of all, you often want no title displayed in front of them, so we don't show the title if it is not explicitly set. Of course, this can be applied to rendering of all parameters, but here it is particularly useful.
Second, you often want some text displayed after them,
like something you are agreeing to, or whatever.
So we use the :text
option of the parameter to pass this text along.
Note that it is not limited to static text either -
like any other parameter option it can be evaluated at runtime if needed,
see Defining Parameters for details.
And the Localization chapter will explain how to get the text localized easily.
Inflatable Parameters
We have seen how to render array parameters as multi-select or multi-checkbox fields. Sometimes, however, you really want to render them as an array of text input fields. One way to do this is to render all current values, plus one extra field for adding new value, like this:
label
= p.form_title
- if p.array?
- values = p.form_value
- for value in values
.form-group
input.form-control[
type=p.type
name=p.form_name
value=value
autofocus=(p.invalid? and p == focused)
disabled=p.disabled?
]
- unless limit = p[:max_count] and values.count >= limit
input.form-control[
type=p.type
name=p.form_name
autofocus=(p.valid? and p == focused)
disabled=p.disabled?
placeholder=p[:placeholder]
]
- else
// standard scalar parameter rendering goes here...
Note that if you use something like this, it makes sense to also add an extra submit button to the form which will just update the form, in addition to the standard submit button. This button will allow the user to add as many items as needed before submitting the form.
Extending Parameters
The examples above show rendering of parameters which shall take care of most of your needs, but it doesn't need to end there. Do you need some extra functionality? It's trivial to pass additional parameter options along and render them in the template the way you need. This chapter will show few examples of what can be done.
Do you need to render a subtitle for some parameters? Just add it after the title like this:
= p.form_title
- if subtitle = p[:subtitle]
small =< subtitle
Do you want to render an extra help text? Add it after error reporting like this:
- if report and error = p.error
.help-block
span.text-danger = error
- if help = p[:help]
.help-block = help
Do you want to render all required parameters as such automatically? Let the snippet add the necessary CSS class like this:
input ... class=(:required if p.required?)
Do you want to disable autocomplete for some input fields? You can do it like this:
input ... autocomplete=(:off if p[:autocomplete] == false)
Do you want to add arbitrary tag attributes to the input element, for example some data attributes? You can put them in a hash and add them using a splat operator like this:
input ... *(p[:tag_attrs] || {})
And so on and on. As you can see, the templates make it really easy to adjust them any way you may need.
Grouped Parameters
Sometimes you may want to group some parameters together and render the groups accordingly, say in their own subframe. It's easy to create a template snippet which does that:
- params ||= []
- focus ||= focus.nil?
- report ||= report.nil? && request.post?
- focused ||= params.find{ |x| x.invalid? } || params.find{ |x| x.blank? } || params.first if focus
- for group, params in params.group_by{ |x| x[:group] || x.name }
h2 = form.group_name( group )
.form-frame
== snippet :form_standard, params: params, focus: focus, focused: focused, report: report
Note that it uses our standard prologue for focusing and error reporting and then passes those values along, so the autofocused parameter is selected correctly no matter what group it belongs to.
The example above uses the group_name
method which is supposed to return the name for given group.
You would have to provide that,
but check out the Localization Helpers chapter for a tip how it can be done easily.
Chunked Parameters
Occasionally, you may want to render some input fields on the same line. That's when the support for parameter chunking becomes handy.
When declaring the form, add the :row
option to those parameters.
The value can be anything you want - equal values mean the parameters should be rendered together on the same line.
The :cols
option can be used if you want specific span of those parameters
in the 12 column grid instead of an evenly distributed split:
param :nick, "Nick", 32,
row: 1, cols: 5
param :email, "Email", EMAIL_ARGS,
row: 1, cols: 7
To enable the chunking itself,
use the chunked_params
form method instead of the params
method
to group the parameters appropriately and pass them to the rendering snippet:
form *form_attrs
fieldset
== snippet :form_chunked, params: @form.chunked_params
button.btn.btn-default type='submit' Submit
The chunked_params
method leaves all single parameters intact,
but puts all those which belong together into an subarray.
Using for example Bootstrap and its standard 12 column grid, the rendering snippet itself can look like this:
- chunked = params || []
- params = chunked.flatten
- focus ||= focus.nil?
- report ||= report.nil? && request.post?
- focused ||= params.find{ |x| x.invalid? } || params.find{ |x| x.blank? } || params.first if focus
- for p in chunked
- if p.is_a?(Array)
.row
- for param in p
div class="col-sm-#{param[:cols] || 12 / p.count}"
== snippet :form_standard, params: [param], focus: focus, focused: focused, report: report
- else
== snippet :form_standard, params: [p], focus: focus, focused: focused, report: report
This example shows a separate snippet which is built on top of another rendering snippets. But note that the chunking functionality can be incorporated directly into all the other snippets presented so far. Also note that it works even if you use other means to create the subarrays of parameters to put on the same line. Again, you are welcome to experiment and tweak the snippets any way you like so they work exactly the way you want them to.
Multi-Step Forms
You have seen them for sure.
Multi-step forms.
Most shopping sites use them during the checkout.
You confirm the items on one page,
fill delivery information on another,
add payment details on the next
and finally confirm it all on the last one.
Normally, these can be quite a chore to implement,
but FormInput
makes it all fairly easy.
The following chapters will describe how to define, use and render such forms in detail.
Defining Multi-Step Forms
Defining multi-step form is about as simple as defining normal form.
The only difference is that you decide what steps you want,
define them with the define_steps
method,
then tag each parameter with the step it belongs to using the :tag
parameter option.
For example, consider the following form:
class PostForm < FormInput
param! :email, "Email", EMAIL_ARGS
param :first_name, "First Name"
param :last_name, "Last Name"
param :street, "Street"
param :city, "City"
param :zip, "ZIP Code"
param! :message, "Your Message", 1000
param :comment, "Optional Comment"
param :url, type: :hidden
end
That's a pretty standard form for posting some feedback message, with many fields optional.
It can also track the URL where the user came from in the hidden :url
field.
Normally, you would display such form on a single page,
but for the sake of the example, we will split it into multiple steps,
having the user fill each block of information on a separate page:
class PostForm < FormInput
define_steps(
email: "Email",
name: "Name",
address: "Address",
message: "Message",
summary: "Summary",
post: nil,
)
param! :email, "Email", EMAIL_ARGS, tag: :email
param :first_name, "First Name", tag: :name
param :last_name, "Last Name", tag: :name
param :street, "Street", tag: :address
param :city, "City", tag: :address
param :zip, "ZIP Code", tag: :address
param! :message, "Your Message", tag: :message
param :comment, "Optional Comment", tag: :message
param :url, type: :hidden
end
The define_steps
method takes a hash of symbols which define the steps
along with their names which can be displayed to the user.
It also extends the form with step-related functionality,
which will be described in detail in the Multi-Step Form Functionality chapter.
The steps are defined in the order which will be used to progress through the form -
the user starts at the first step and finishes at the last
(at least that's the most typical flow).
The :tag
option is then used to assign each parameter to particular step.
If you would need additional tags, remember that you can use the :tags
array instead as well.
Also note that the hidden parameter doesn't have to belong to any step,
as it will be always rendered as a hidden field anyway.
As you can see, we have split the parameters among four steps.
But we have defined more steps than that -
there can be steps which have no parameters assigned to them.
Such extra steps are still used for tracking progress through the form.
In this example, the :summary
step is used to display the form data
gathered so far and giving the user the option of re-editing them before posting them.
Similarly, you could have an initial :intro
step or some other intermediate steps if you wanted.
The final :post
step serves as a terminator which we will use to actually process the form data.
Note that it doesn't have a name,
so it won't appear among the list of step names if we decide to display them somewhere
(see Rendering Multi-Step Forms for example of that).
We will soon delve into how to use such forms, but first let's discuss their enhanced functionality.
Multi-Step Form Functionality
Using define_steps
in a form definition extends the range of the methods available,
in addition to those described in the Form Helpers chapter.
It also adds several internal form parameters which are crucial for keeping track of the progress through the form.
The most important of those parameters is the step
parameter.
It is always set to the name of the current step,
starting with the first step defined by default.
If you need to, you can change the current step by simply assigning to it,
we will see examples of that later in the Using Multi-Step Forms chapter.
The second important parameter is the next
parameter.
It contains the desired step which the user wants to go to whenever he posts the form.
This parameter is used to let the user progress throughout the form -
we will see examples of that in the Rendering Multi-Step Forms chapter.
If it is set and there are no problems with the parameters of the currently submitted step,
it will be used to update the value of the step
parameter,
effectively changing the current step to whatever the user wanted.
Otherwise the step
value is not changed and the current step is retained.
There are two more parameters which are internally used for keeping information about previously visited steps.
The last
parameter contains the highest step among the steps seen by the user, including the current step.
The seen
parameter contains the highest step among the steps seen by the user before the current step was displayed.
Unlike the last
parameter, which is always set, the seen
parameter can be nil
if no steps were displayed before yet.
The current step is not included when it is displayed for the first time,
it will become included only if it is displayed more than once.
Neither of these parameters is usually used directly, though.
Instead, they are used by several helper methods for classifying the already visited steps,
which we will see shortly.
There are three methods added which extend the list of methods
which can be used for getting lists of form parameters.
The current_params
method provides the list of all parameters which belong to the current step,
while the other_params
method provides the list of those which do not.
Then there is the step_params
method which returns the list of all parameters for given step.
form = PostForm.new
form.current_params.map( &:name ) # [:email]
form.other_params.map( &:name ) # [:first_name, :last_name, ..., :comment, :url]
form.step_params( :name ).map( &:name ) # [:first_name, :last_name]
The rest of the methods added is related to the steps themselves.
The steps
method returns the list of symbols defining the individual steps.
The step_names
method returns the hash of steps which have a name along with their names.
The step_name
method returns the name of current/given step, or nil
if it has no name defined.
The next_step_name
and previous_step_name
are handy shortcuts for getting
the name of the next and previous step, respectively.
form.steps # [:email, :name, :address, :message, :summary, :post]
form.step_names # {email: "Email", name: "Name", ..., message: "Message", summary: "Summary"}
form.step_name # "Email"
form.step_name( :address ) # "Address"
form.step_name( :post ) # nil
form.next_step_name # "Address"
form.previous_step_name # nil
Then there are methods dealing with the step order.
The step_index
method returns the index of the current/given step.
The step_before?
and step_after?
methods test
if the current step is before or after given step, respectively.
The first_step
method returns the first step defined,
and the last_step
method returns the last step defined.
If provided with a list of step names,
these methods return the first/last valid step among them, respectively.
The next_step
method returns the step following the current/given step,
or nil
if there is no next step,
and
the previous_step
method returns the step preceding the current/given step,
or nil
if there is no previous step.
Finally,
the next_steps
method returns the list of steps following the current/given step,
and the previous_steps
method returns the list of steps preceding the current/given step.
form.step_index # 0
form.step_index( :address ) # 2
form.step_before?( :summary ) # true
form.step_after?( :email ) # false
form.first_step # :email
form.first_step( :message, :name ) # :name
form.last_step # :post
form.last_step( :message, :name ) # :message
form.next_step # :name
form.next_step( :message ) # :summary
form.next_step( :post ) # nil
form.previous_step # nil
form.previous_step( :message ) # :address
form.previous_step( :email ) # nil
form.next_steps # [:name, :address, :message, :summary, :post]
form.next_steps( :address ) # [:message, :summary, :post]
form.previous_steps # []
form.previous_steps( :address ) # [:email, :name]
Then there is a group of boolean getter methods which can be used to query the current/given step about various things:
form.first_step? # Is this the first step?
form.last_step? # Is this the last step?
form.regular_step? # Does this step have some parameters assigned?
form.extra_step? # Does this step have no parameters assigned?
form.required_step? # Does this step have some required parameters?
form.optional_step? # Does this step have no required parameters?
form.filled_step? # Were some of the step parameters filled already?
form.unfilled_step? # Were none of the step parameters filled yet?
form.correct_step? # Are all of the step parameters valid?
form.incorrect_step? # Are some of the step parameters invalid?
form.enabled_step? # Are not all of the step parameters disabled?
form.disabled_step? # Are all of the step parameters disabled?
form.first_step?( :email ) # true
form.last_step?( :post ) # true
form.regular_step?( :name ) # true
form.extra_step?( :post ) # true
form.required_step?( :message ) # true
form.optional_step?( :post ) # true
form.filled_step?( :post ) # true
form.unfilled_step?( :name ) # true
form.correct_step?( :post ) # true
form.incorrect_step?( :email ) # true
form.enabled_step?( :name ) # true
form.disabled_step?( :post ) # false
Note that the extra steps, which have no parameters assigned to them, are always considered optional, filled, correct and enabled for the purpose of these methods.
Based on these getters,
there is a group of methods which return a list of matching steps.
In this case, however, the extra steps are excluded for convenience
from all these methods (except the extra_steps
method itself, of course):
form.regular_steps # [:email, :name, :address, :message]
form.extra_steps # [:summary, :post]
form.required_steps # [:email, :message]
form.optional_steps # [:name, :address]
form.filled_steps # []
form.unfilled_steps # [:email, :name, :address, :message]
form.correct_steps # [:name, :address]
form.incorrect_steps # [:email, :message]
form.enabled_steps # [:email, :name, :address, :message]
form.disabled_steps # []
The first of the incorrect steps is of particular interest,
so there is a shortcut method incorrect_step
just for that:
form.incorrect_step # :email
Finally, there is a group of methods which deal with the progress through the form. Normally, the user starts at the first step, and proceeds to the next step whenever he submits valid parameters for the current step. If the submitted parameters contain some errors, the current step is not advanced and the errors are reported, allowing the user to fix them before moving on. Eventually the user reaches the last step, at which point the form is finally processed. That's the standard flow, but it can be changed by allowing the user to go back and forth to all the steps he had visited previously.
There are several methods for getting a list of steps in the corresponding range.
The finished_steps
method returns the list of steps
which the user has visited and submitted before.
The unfinished steps
method returns the list of steps
which the user has not visited yet, or visited for the first time.
The accessible_steps
method returns the list of steps
which the user has visited already.
The inaccessible_steps
method returns the list of steps
which the user has not visited yet at all.
There is also the matching set of boolean getter methods
which can be used to query the same information about individual steps.
form.finished_steps # []
form.unfinished_steps # [:email, :name, :address, :message, :summary, :post]
form.accessible_steps # [:email]
form.inaccessible_steps # [:name, :address, :message, :summary, :post]
form.finished_step? # false
form.unfinished_step? # true
form.accessible_step? # true
form.inaccessible_step? # false
form.finished_step?( :email ) # false
form.unfinished_step?( :post ) # true
form.accessible_step?( :email ) # true
form.inaccessible_step?( :post ) # true
By default, only the first step is initially accessible,
but you can change that by using the unlock_steps
method,
which makes all steps instantly accessible.
This can be handy for example when the whole form is prefilled with some previously acquired data,
so the user can access any step from the very beginning:
form = PostForm.new( user.latest_post.to_hash ).unlock_steps
If you decide to display the individual steps in the masthead or the sidebar,
it is often desirable to mark them not only as accessible or inaccessible,
but also as correct or incorrect.
This last group of methods is intended for that.
The complete_step?
method can be used to test
if the current/given step was finished and contains no errors.
The incomplete_step?
method can be used to test
if the current/given step was finished but contains errors.
The good_step?
method tests if the current/given step should be visualized as good
(green color, check sign, etc.).
By default it returns true
for finished, correctly filled regular steps,
but your form can override it to provide different semantics.
The bad_step?
method tests if the current/given step should be visualized as bad
(red color, cross or exclamation mark, etc.).
By default it returns true
for finished but incorrect steps,
but again you can override it if you wish.
As usual, there are the corresponding methods
which can be used to get lists of all the matching steps at once:
form = PostForm.new.unlock_steps
form.complete_steps # [:name, :address, :summary, :post]
form.incomplete_steps # [:email, :message]
form.good_steps # []
form.bad_steps # [:email, :message]
form.complete_step? # false
form.incomplete_step? # true
form.good_step? # false
form.bad_step? # true
form.complete_step?( :name ) # true
form.incomplete_step?( :email ) # true
form.good_step?( :name ) # false
form.bad_step?( :email ) # true
And that's it. Now let's have a look at some practical use of these helpers.
Using Multi-Step Forms
Using the multi-step forms is not much different from the normal forms. Creating the form initially is the same, as is presetting the parameters:
get '/post' do
@form = PostForm.new( email: user.email )
slim :post_form
end
Processing the submitted form is nearly the same, too. The only difference is that you keep doing nothing until the user reaches the last step. Then you validate the form, and if there are some problems, return the user to the appropriate step to fix them. Normally, the user shouldn't be able to proceed to the last step if there are errors, but people can always try to trick the form by submitting any parameters they want, so you should be ready for that. Returning them to the incorrect step is more polite than just failing with an error, but you could do that as well if you wanted. In either case, once the user gets to the last step and there are no errors, you use the form the same way you would use any regular form.
post '/post' do
@form = PostForm.new( request )
# Wait for the last step.
return slim :post_form unless @form.last_step?
# Make sure the form is really valid.
unless @form.valid?
@form.step = @form.incorrect_step
return slim :post_form
end
# Now somehow use the submitted data.
user.create_post( @form )
slim :post_created
end
And that's it. I told you it was easy. Of course, you can utilize more of the helpers described in the Multi-Step Form Functionality chapter and do more complex things if you wish, but the example above is the basic boilerplate you will likely want to start with most of the time.
Rendering Multi-Step Forms
Rendering the multi-step form is similar to rendering of regular forms as well. The biggest difference is that you render the parameters for the current step normally, while all other parameters are rendered as hidden input elements. The most basic multi-step form could thus look like this:
form *form_attrs
fieldset
== snippet :form_standard, params: @form.current_params, report: @form.finished_step?
== snippet :form_hidden, params: @form.other_params
button.btn.btn-default type='submit' name='next' value=@form.next_step Proceed
See how the submit button uses the next
value to proceed to the next step.
Without it, the form would be merely updated when submitted.
Also note how we control when to display the errors -
we use the finished_step?
method to suppress the errors whenever
the user sees certain step for the first time.
Note that it is also possible to divert the form rendering for individual steps if you need to.
If we follow the PostForm
example,
you will want to render the :summary
step accordingly, for example like this:
h2 = @form.step_name
- if @form.step == :summary
== snippet :form_hidden, params: @form.params
dl.dl-horizontal
- for p in @form.visible_params
dt = p.title
dd = p.value
- else
== snippet :form_standard, params: @form.current_params, report: @form.finished_step?
== snippet :form_hidden, params: @form.other_params
Of course, you will likely want your form to use more fancy submit buttons as well. For example, you can use more specific submit buttons for each step instead:
.btn-toolbar.pull-right
- if name = @form.next_step_name
button.btn.btn-default type='submit' name='next' value=@form.next_step Next Step: #{name}
- else
button.btn.btn-primary type='submit' name='next' value=@form.next_step Send Post
If you want to provide button for updating the form content without proceeding to the next step, simply include something like this:
-unless @form.extra_step?
button.btn.btn-default type='submit' Update
To allow users to go back to the previous step, prepend something like this:
- if name = @form.previous_step_name
.btn-toolbar.pull-left
button.btn.btn-default type='submit' name='next' value=@form.previous_step Previous Step: #{name}
Note that browsers nowadays automatically use the first submit button when the user hits the enter in the text field. If you have multiple buttons in the form and the first one is not guaranteed to be the one you want, you can add the following invisible button as the first button in the form to make the browser go to the step you want:
button.invisible type='submit' name='next' value=@form.next_step tabindex='-1'
When rendering the multi-step form, it also makes sense to display the individual steps in some way so the user can see what steps there are and to see his progress. Common ways are a form specific masthead above the form or a sidebar next to the form. The basic masthead can be rendered like this:
ul.form-masthead
- for step, name in @form.step_names
li[
class=(:active if step == @form.step)
class=(:disabled unless @form.accessible_step?( step ))
]
= name
This uses the typical CSS classes to distinguish between the current, accessible and inaccessible steps. If you also want to somehow mark the correct and incorrect steps, you can append something like this:
- if @form.bad_step?( step )
span.pull-right.glyphicon.glyphicon-exclamation-sign.text-danger
- elsif @form.good_step?( step )
span.pull-right.glyphicon.glyphicon-ok-sign.text-success
Users also often expect to be able to click the individual steps to go directly to that step. To allow that, simply change the list elements to buttons which can be clicked like this:
ul.form-masthead
- for step, name in @form.step_names
- if @form.accessible_step?( step )
li class=(:active if step == @form.step)
button type='submit' name='next' value=step tabindex='-1'
= name
- else
li.disabled = name
Note that in this case you will almost certainly want to include the invisible button we have mentioned above as the first button in the form to make sure hitting the enter in the text field works as expected.
Of course, once again it is trivial to extend these examples with anything you need.
Do you want to show tips about each form step as the user hovers over them?
Simply add the step_hint
method to your form to return the text to display
and add the hint with the title attribute like this:
li ... title=@form.step_hint( step )
The list of the possible enhancements could go on and on. As your multi-step form navigation gets more complex, you will likely want to factor it out to its own snippet. This will allow you to share it among multiple forms and even switch visual styles with ease.
Localization
Working with forms in one way or another implies showing lot of text to the user.
Chances are that sooner or later you'll want that text to become localized.
The good news is that the FormInput
comes with full featured localization support already built in.
These chapters explain in detail how to take advantage of all its features.
The FormInput
localization is built on R18n.
R18n is a neat tiny gem for all your localization needs.
It is a no-nonsense, right-to-the-point toolkit developed by people who understand the subject.
It even comes with an I18n compatible drop-in replacement for Rails,
so you can switch to it with ease.
If you are serious about localization,
you should definitely check it out.
If your project already uses R18n,
requiring form_input
will detect it and make the localization support available automatically.
Otherwise you can make it available explicitly by requiring form_input/r18n
in your application:
require `form_input/r18n`
Then all you need to do is to set the desired R18n locale:
R18n.set('en') # For generic English.
R18n.set('en-us') # For American English.
R18n.set('en-gb') # For British English.
R18n.set('cs') # For Czech.
Note that how exactly is this done can differ slightly depending on the framework you use. For example, if you use Sinatra together with the sinatra-r18n gem, the locale is set automatically for you for each request by the R18n helper. Likewise if you use Rails together with the r18n-rails gem. Please refer to the R18n documentation for more details.
Anyway, once the localization is enabled, the following features become available:
- All builtin error messages and other builtin strings become localized.
- Full inflection support is enabled for all builtin error messages.
- All string parameter options can be localized.
- All multi-step form step names can be localized.
- The R18n
t
andl
helpers become available in both form and parameter contexts. - Additional
ft
helper becomes available in both form and parameter contexts. - Additional
pt
helper becomes available in the parameter context.
Note that the full inflection support alone can be useful on its own, so you may want to explore the following chapters even if you don't plan to translate the application to other languages yet.
Error Messages and Inflection
Validation and error reporting are major FormInput
features.
Normally, FormInput
uses builtin English error messages.
It pluralizes the names of the units it displays properly,
but provides little inflection support beyond that.
It assumes that all scalar parameter names use singular case
and all array and hash parameter names use plural case.
This is most often the case, but not always.
If you need something more flexible, you may need to enable the localization support.
With localization enabled,
the list of builtin error messages becomes replaced by
string translations managed by R18n in the form_input
namespace.
You can find the available translation files in the form_input/r18n
directory.
The same path can be obtained at runtime from the FormInput.translations_path
method.
The translations of the error messages were created with inflection in mind
and the message variants are chosen according to the inflection rules of the parameter names automatically -
the Inflection Filter chapter will explain the gory details.
For English, the inflection rules are pretty simple.
You only need to distinguish between singular and plural grammatical number.
By default,
FormInput
uses singular for scalar parameters and plural for array and hash parameters.
With localization enabled,
you can override it by setting the :plural
parameter option to true
or 'p'
for plural,
and to false
or 's'
for singular, respectively:
param :keywords, "Keywords", plural: true
array :countries, "List of countries", plural: false
The boolean values make sense for most languages,
but note that there are languages which have more than two grammatical numbers,
and that's when the string values may become useful.
Regardless of which way you use,
the plural
method can be used to get the string matching
the grammatical number of given parameter for the currently active locale.
However, grammatical number is not the only thing to take care of.
For many languages, the rules are more complex than that.
You usually need to take the grammatical gender into account as well.
Instead of using single set of error messages and forcing you to use single grammatical gender to fit them all,
FormInput
allows you to use the :gender
option to specify the grammatical gender of each parameter explicitly.
Its value is one of the shortcuts of the grammatical genders used by the translation file for given language -
check the corresponding file in the form_input/r18n
directory for specific details.
The following gender values are typically available:
-
n
- neuter -
f
- feminine -
m
- masculine -
mi
- inanimate masculine -
ma
- animate masculine -
mp
- personal masculine
When not set, the default value is n
for neuter gender,
which is suitable for example for English,
but other languages can set the default to be something else,
typically mi
for inanimate masculine gender.
See the default_gender
value in
the corresponding file in the form_input/r18n
directory.
In either case,
the gender
method can be used to get the string containing
the grammatical gender of given parameter for the currently active locale.
To see how this works in real life, here are several parameters with properly inflected Czech titles:
param :email, "Email"
param :name, "Jméno", gender: "n"
param :address, "Adresa", gender: "f"
param :keywords, "Klíčová slova", plural: true, gender: "n"
param :authors, "Autoři", plural: true, gender: "ma"
However, even if it is possible to use non-English names directly in the forms like this, assuming you also set the R18n locale to the corresponding value, it is not very common. The whole point of localization is usually to extract the texts to external files, so they can be translated and localized to different languages. The next chapter will explain how to do that.
Localizing Forms
If you have started creating your FormInput
forms without thinking about localization,
the good news are that the forms will not require much changes to become localized.
In fact, most of your form strings will be taken care of automatically.
Only strings which you might have used within the dynamically evaluated parameter options
or parameter callbacks
will need to be localized explicitly with the help of the Localization Helpers.
To localize your project,
you will first need to add the R18n translation files to it.
See the R18n documentation for details on how is this done
and where the files are supposed to be placed.
Once you have the R18n directory structure set up, you just need to localize the forms themselves.
To get you started, FormInput
provides a localization helper to create the default translation file for you.
All you need to do is to run irb
, require all of your project files,
then run the following:
require 'form_input/localize'
File.write( 'en.yml', FormInput.default_translation )
This creates a YAML file called en.yml
which contains
the default translations for all your forms in the format directly usable by R18n.
Of course, if for some reason your default project language is not English,
adjust the name of the file accordingly.
For example, for a project containing only the ContactForm
from the Introduction section
the content of the default translation file would look like this:
---
forms:
contact_form:
email:
title: Email address
name:
title: Name
company:
title: Company
message:
title: Message
As you can see,
all form related strings reside within the forms
namespace,
so it shall not interfere with your other translations.
You can merge it with your main en.yml
in the /i18n/
directory,
or, if you want to keep it separate,
you can put it in its own subdirectory, say /i18n/forms/en.yml
.
Once you have the en.yml
file in place,
R18n shall pick it up automatically.
To make sure it is all working,
temporarily change some of the form titles in the translation file to something else
and it shall change on the rendered page accordingly.
If it doesn't seem to work, try adding something like the following bit somewhere on some page
and make it work first
(of course, adjust the name of the form and parameter accordingly to match your real project):
pre = t.forms.contact_form.email.title
Please refer to the R18n documentation for more troubleshooting help if you still need assistance.
Once you get the default translation file working, you are all set to start adding other translation files for all the languages you want. But first you should understand the content of those files and how to use it. The next chapters will explain it in detail.
Localizing Parameters
Each form derived from FormInput
has its own namespace in the global forms
R18n namespace.
The name of this namespace is returned by the translation_name
method of each form class.
It's basically the snake_case conversion of the CamelCase name of the form class.
Each of the form parameters has its own namespace within the namespace of the form to which it belongs.
The name of this namespace is the same as the name
attribute of the parameter.
Each of the parameter options can be localized by simply adding
the appropriate translation within this parameter namespace.
Remember that the title
attribute of the parameter is nothing more than just one of the possible Parameter Options,
so it applies to it as well.
The localization of the ContactForm
from the Introduction section
thus looks like this:
forms:
contact_form:
email:
title: Email address
name:
title: Name
company:
title: Company
message:
title: Message
To translate it say from English to Czech,
copy it from the en.yml
file to the cs.yml
file
and then adjust it for example like this:
forms:
contact_form:
email:
title: Email
name:
title: Jméno
gender: n
company:
title: Společnost
gender: f
message:
title: Zpráva
gender: f
Note the use of the :gender
option to get properly inflected error messages.
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.
To do this, we add the value of the :required_msg
option of the message
parameter directly to the en.yml
file like this:
message:
title: Message
required_msg: Message with no content makes no sense.
Note that this works even if you don't add the :required_msg
option to the parameter
within the ContactForm
class definition itself.
That's because as long as you have the locale support enabled,
all applicable parameter options are automatically looked up in the translation files first.
If the corresponding translation is found,
it is used regardless of the parameter option value declared in the form.
This allows you to completely remove the texts from the form definitions themselves
and to keep them all in one place,
which makes the localization easier to maintain in the long term.
The texts present in the class definition are used only as a fallback if no corresponding translation is found at all.
This is particularly handy when you are adding new parameters which you haven't added into any of the translation files yet.
However note that R18n normally provides its own default translation based on its builtin fallback sequence for each locale,
usually falling back to English in the end.
This means that once you add some translation to the en.yml
file,
the parameter option will get this English translation for all other locales as well,
until you add the corresponding translation to the other translations files as well.
So, to make sure we get the Czech version of the :required_msg
for the example above,
the cs.yml
file should be updated as well like this:
message:
title: Zpráva
gender: f
required_msg: Zpráva bez obsahu nemá žádný smysl.
And that's about it.
This alone will allow you to localize and translate most if not all of your forms completely.
However,
if you were using some texts within the dynamically evaluated options or parameter callbacks
like :check
or :test
,
you will need to replace them with the use of the localization helpers which we will describe in next chapter.
Localization Helpers
The R18n provides two main shortcut methods, t
and l
,
which are used for translating texts and localizing objects, respectively.
See the R18n documentation for details.
When the localization support is enabled,
the FormInput
makes these two methods available in both parameter and form contexts.
It also adds two additional helper methods similar to the t
method, called ft
and pt
.
Either of these methods can be used to translate texts other than those automatically handled by the FormInput
itself.
The ft
method is usable in both parameter and form contexts.
It works like the t
method,
except that all translations are automatically looked up in the form's own forms.<form_translation_name>
namespace,
rather than the global namespace.
Note that it supports the .
, ()
, and []
syntax alternatives for getting the desired translation.
The latter two forms are handy when the text name is obtained programatically.
form = ContactForm.new
form.ft.some_text # Returns forms.contact_form.some_text translation.
form.ft( :some_text ) # Ditto.
form.ft[ :some_text ] # Ditto.
Note that it is possible to pass arguments to the translation as usual:
form.ft.other_text( 1 ) # Returns forms.contact_form.other_text translation, using 1 as an argument.
form.ft( :other_text, 1 ) # Ditto.
form.ft[ :other_text, 1 ] # Ditto.
Of course, nesting of translations is possible as usual as well:
form.ft.errors.generic # Returns forms.contact_form.errors.generic translation.
form.ft( :errors ).generic # Ditto.
form.ft[ :errors ].generic # Ditto.
The ft
method is typically used in methods which encapsulate the lookup of translated text within your form.
For example, here is how the group_name
method
mentioned in the Grouped Parameters chapter
might look like:
def group_name( group )
ft.groups[ group ]
end
The pt
method is usable only in the parameter context.
It works similar to the ft
method,
except that all translations are automatically looked up in the parameter's own forms.<form_translation_name>.<parameter_name>
namespace,
rather than the global namespace.
p = form.params.first
p.pt.some_text # Returns forms.contact_form.email.some_text translation.
p.pt( :some_text ) # Ditto.
p.pt[ :some_text ] # Ditto.
p.pt.other_text( 1 ) # Returns forms.contact_form.email.other_text translation,
p.pt( :other_text, 1 ) # Ditto. using 1 as an argument.
p.pt[ :other_text, 1 ] # Ditto.
p.pt.errors.generic # Returns forms.contact_form.email.errors.generic translation.
p.pt( :errors ).generic # Ditto.
p.pt[ :errors ].generic # Ditto.
Like the ft
method, it provides three syntax alternatives for getting the desired translation.
It's worth mentioning that the ()
syntax automatically appends the parameter itself as the last argument,
making it available for the Inflection Filter, which will be discussed later.
The pt
method is typically used in parameter callbacks and
in dynamically evaluated parameter options.
For example, the text in the following callback
check: ->{ report( "This password is not secure enough" ) unless form.secure_password?( value ) }
can be replaced with properly translated parameter specific variant like this:
check: ->{ report( pt.insecure_password ) unless form.secure_password?( value ) }
It is also handy when the text requires some parameters:
param! :password, "Password", PASSWORD_ARGS,
help: ->{ pt.help( self[ :min_size ], self[ :max_size ] ) }
Note that the texts translated like this are usually parameter specific so they can be inflected and adjusted as needed by the translator directly. However, if you are preparing something which is intended to be reused, you will likely want to have the messages automatically inflected according to the parameter used. This is when the Inflection Filter comes to help. All you need to do is to pass the form parameter as the last argument to the translation getter like this:
EVEN_ARGS = {
test: ->( value ){ report( t.forms.errors.odd_value( self ) ) unless value.to_i.even? }
}
You can even do something more fancy, for example distinguish between the scalar and array and hash parameters, or pass in the rejected value itself as an additional parameter, like this:
EVEN_ARGS = {
test: ->( value ){
report( t.forms.errors[ scalar? ? :odd_value : :odd_array, value, self ] ) unless value.to_i.even?
}
}
In either case, if set correctly, the inflection filter will pick up the last argument provided and choose the appropriately inflected message. Now let's see how exactly is this done.
Inflection Filter
The R18n suite includes flexible filtering support for additional processing of the translated texts.
See the R18n documentation for example for the use of the pl
pluralization filter.
The FormInput
provides its own inflect
inflection filter which works similarly.
To continue the EVEN_ARGS
example from the previous chapter, consider the following translation file:
forms:
errors:
odd_value: !!inflect
s: '%p is not even'
p: '%p are not even'
odd_array: !!inflect
s: '%p contains number %1 which is not even'
p: '%p contain number %1 which is not even'
This basically tells the R18n toolkit that it should use the inflect
filter whenever
someone asks for the value of the odd_value
or odd_array
translations.
The value itself is not a string in this case,
but a hash which contains several translations.
The key is the longest prefix of the inflection string
used to choose the desired translation.
Now what is this inflection string and where does it come from?
Each parameter has the inflection
method which returns
the desired string used to choose the appropriately inflected error message.
By default, it returns the grammatical number and grammatical gender strings combined,
as returned by the plural
and gender
methods of the parameter, respectively.
If needed, it can be also set directly by the :inflect
parameter option.
This can be handy for languages which have even more complex rules for inflection
than the currently builtin ones.
The inflection filter checks the last parameter it was passed to the translation getter by its caller.
If it is a string, it is used as it is.
If it is a form parameter, its inflection
method is used to get the inflection string.
If no inflection string is provided, it falls back to the default string 'sn'
which stands for singular neuter.
It than uses the form's find_inflection
method to find the most appropriate translation for given inflection string.
By default,
it finds the longest prefix among the keys of the available translations which match the inflection string.
This may sound complex, but it allows minimizing the number of inflected translations.
Instead of having to list translations for all inflection variants,
only those which differ have to be listed and the other ones can be merged together.
In English it makes little difference,
as it only distinguishes between singular and plural grammatical case,
but you can check the translation files for other languages in the form_input/r18n
directory
to see how this is used in practice.
Here is an excerpt from the Czech translation file which demonstrates this:
required_scalar: !!inflect
sm: '%p je povinný'
sf: '%p je povinná'
sn: '%p je povinné'
p: '%p jsou povinné'
pma: '%p jsou povinní'
pn: '%p jsou povinná'
As you can see, there are three distinct variants in the singular case, one for each gender. The plural case on the other hand uses the same variant for most genders, with only two specific exceptions defined.
Defining translations like this may seem complex, but it should feel fairly natural to people fluent in given language. If you intend to use some texts over and over again, paying attention to their proper inflection will definitely pay off in the long term.
Localizing Form Steps
If you are using the Multi-Step Forms,
you will likely want to localize the step names themselves as well.
Fortunately, it's very simple.
Just add the translations of all step names
to the steps
namespace of the form in question.
For example,
here is how the translation file of the PostForm
form
from the Defining Multi-Step Forms chapter
would look like:
post_form:
email:
title: Email
first_name:
title: First Name
last_name:
title: Last Name
street:
title: Street
city:
title: City
zip:
title: ZIP Code
message:
title: Your Message
comment:
title: Optional Comment
steps:
email: Email
name: Name
address: Address
message: Message
summary: Summary
Trivial indeed, isn't it?
Supported Locales
The FormInput
currently includes translations of builtin error messages for the following languages:
- English
- Czech
- Slovak
- Polish
To add support for another language,
simply copy the content of one of the most similar files found in the form_input/r18n
directory
to the appropriate translation file in your project,
and translate it as you see fit.
Pay extra attention to the proper use of the inflection keys,
see the Inflection Filter chapter for details.
Once you are happy with the translation,
please consider sharing it with the rest of the world.
If you get in touch and make it available, it may become included in the future update of this gem.
Thanks for that.
Credits
Copyright © 2015-2019 Patrik Rak
Translations contributed by Maroš Rovňák (Slovak) and Eryk Dwornicki (Polish).
The FormInput
is released under the MIT license.