Rails validations and multi-page forms
by Stephen Richards (Software Development Profession)
Standard Rails validations
I was recently running a small training session where we were talking about the Rails validation helpers. These are one-liners in a Rails model that enable you to carry out common validations, for example:
This statement will cause the validations to be run either when you call the
#valid? method on a person record, or when you create, save or update a record to the database. If either
ni_number is nil or an empty string, then two things happen: an errors object on the person record will be populated, and
#valid? will return false. In the case of create, save or update the record will not be saved to the database, and false will be returned. If you used
ActiveRecord::RecordInvalid exception will be raised.
The Active Record Validations Rails Guide provides an excellent reference source for the various validations that can be done in this way.
Options for conditional validations
So far so good. But how, asked one of the attendees, do you use Rails validations when you have a typical Government Digital Services (GDS) form where one question is asked per page in a series of pages?
For those of you unfamiliar with GDS form guidelines, they prefer one question per page to build up a body of information before finally creating a record. Take a look at the Register to Vote service for a good example of this pattern.
For Rails validations, this presents a bit of a problem. The
validates helper methods are blunt instruments — in their simplest form they always carry out the validations. I’ve seen various solutions to this dilemma, for example:
Using the session to hold partial data: As you progress through the pages, the data is held in the session, and only validated and written to the database once all the information has been entered. I think this has several downsides:
- The record has to be serialised and de-serialised each time
- Validations don’t take placed until the very end of the process, so you have to handle moving to the correct page in order to display the error messages
- It’s tricky to implement “save and return”, in other words fill in the first few pages, save your progress so far and return at a later date
Using the standard validation methods, but with conditions: for example
This requires the controller to set a
create_stage variable on the model to record how far along the process it is. This works well enough for a very simple model and form, but can quickly become unmanageable.
Use a custom validation class: This is my favourite method when validations become complex and conditional. We abstract all the validation code out of the model into a specialised class, which helps keep the model from getting too bloated.
Custom Validation Classes
Let’s work through how we’d go about creating a custom validation class.
First of all, we’ll add another column to the database table to record what stage of validation we’re at:
rails generate migration AddCreateStageToPerson create_stage:integer
And then run
rake db:migrate to update the database.
Next, we’ll remove the
validates line from the model, and replace it with one that tells it to use a validator class.
I put the validators in the
/app/validators folder. Any files in subdirectories of
/app are automatically loaded by Rails, so we don’t have to do anything special to make sure they are loaded. The validator class must derive from
ActiveModel::Validator, and have a method called
validate taking the record to be validated as a param. This is where our validation code goes.
So our validator class will look something like this:
The Code Explained
valid? is called on a
Person record, an instance of
PersonValidator is instantiated, and the validate method is called with the person record passed in as a parameter. In the
validate method above, we first save the person record as an instance variable to save passing it around all the methods, and then use a bit of Ruby metaprogramming magic:
__send__ simply means call the method on the current object. In this case, if the
create_stage attribute on the record is 2, this will call the method
The real substance of the validations is carried out in the
validate_present method. We’re using Ruby metaprogramming techniques again in the line
Here, were calling a method on the
@record instance variable using
attr as the name of the method, so if
:name, this would be the equivalent of
If it is invalid, we simply add an error message into the errors hash against the attribute.
If at the end of the
PersonValidator#validate method there are no messages in the error hash, Rails will consider the record to be valid.
All of this is obviously a lot more work than simply adding a validates line to the model, but it has several advantages for complex validations:
- It abstracts all of the validations out of the model. We should all strive for Thin Controllers and Fat Models. But don’t just move everything into a model class — in a complex app you’ll end up with huge classes. This is one technique to avoid having model classes with hundreds of lines of code.
- It makes the validations very easy to test. Just pass in a Person object with the variables set accordingly and assert that the right error messages are set. Here’s a snippet:
None of these records in the above tests have been persisted to the database, so tests run snappily (slow running tests are a real bugbear of large projects, so anything you can do to keep the tests running quickly is to be welcomed).
- Validation rules can be easily changed and tested in isolation — the ability to make changes easily and quickly is always a major plus in my book.
Obviously, the example we used here is a trivial validation problem, but validations can get extremely complicated, and extracting the validations and the validation tests out of the model can help in keeping the code clean and easy to understand.
If you’d like to come and work with us, please check current vacancies on our job board (filter on Organisation::Ministry of Justice, Job Role::Digital)!