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 name
, date_of_birth
or 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 save!
, create!
or update!
an 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
Every time 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__(“validate_stage_#{@record.create_stage}”)
This __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 validate_stage_2
.
The real substance of the validations is carried out in the validate_present
method. We’re using Ruby metaprogramming techniques again in the line
unless @record.__send__(attr).present?
Here, were calling a method on the @record
instance variable using attr
as the name of the method, so if attr
were :name
, this would be the equivalent of
unless @record.name.present?
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.
Conclusion
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 enjoyed this article, please feel free to hit the👏 clap button and leave a response below. You also can follow us on Twitter, read our other blog or check us out on LinkedIn.
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)!
~~~~~~~~~~~~~~~~~~~~