Back to Blog

Validate multiple contexts together in Rails 5

on April 8, 2016
This blog is part of our Rails 5 series.

Active Record validation is a well-known and widely used functionality of Rails. Slightly lesser popular is Rails's ability to validate on custom context.

If used properly, contextual validations can result in much cleaner code. To understand validation context, we will take example of a form which is submitted in multiple steps:

1class MultiStepForm < ActiveRecord::Base
2  validate :personal_info
3  validate :education, on: :create
4  validate :work_experience, on: :update
5  validate :final_step, on: :submission
6
7  def personal_info
8    # validation logic goes here..
9  end
10
11  # Smiliary all the validation methods go here.
12end

Let's go through all the four validations one-by-one.

1. personal_info validation has no context defined (notice the absence of on:). Validations with no context are executed every time a model save is triggered. Please go through all the triggers here.

2. education validation has context of :create. It is executed only when a new object is created.

3. work_experience validation is in :update context and gets triggered for updates only. :create and :update are the only two pre-defined contexts.

4. final_step is validated using a custom context named :submission. Unlike above scenarios, it needs to be explicitly triggered like this:

1form = MultiStepForm.new
2
3# Either
4form.valid?(:submission)
5
6# Or
7form.save(context: :submission)

valid? runs the validation in given context and populates errors. save would first call valid? in the given context and persist the changes if validations pass. Otherwise populates errors.

One thing to note here is that when we validate using an explicit context, Rails bypasses all other contexts including :create and :update.

Now that we understand validation context, we can switch our focus to validate multiple context together enhancement in Rails 5.

Let's change our contexts from above example to

1class MultiStepForm < ActiveRecord::Base
2  validate :personal_info, on: :personal_submission
3  validate :education, on: :education_submission
4  validate :work_experience, on: :work_ex_submission
5  validate :final_step, on: :final_submission
6
7  def personal_info
8    # code goes here..
9  end
10
11  # Smiliary all the validation methods go here.
12end

For each step, we would want to validate the model with all previous steps and avoid all future steps. Prior to Rails 5, this can be achieved like this:

1class MultiStepForm < ActiveRecord::Base
2  #...
3
4  def save_personal_info
5    self.save if self.valid?(:personal_submission)
6  end
7
8  def save_education
9    self.save if self.valid?(:personal_submission)
10              && self.valid?(:education_submission)
11  end
12
13  def save_work_experience
14    self.save if self.valid?(:personal_submission)
15              && self.valid?(:education_submission)
16              && self.valid?(:work_ex_submission)
17  end
18
19  # And so on...
20end

Notice that valid? takes only one context at a time. So we have to repeatedly call valid? for each context.

This gets simplified in Rails 5 by enhancing valid? and invalid? to accept an array. Our code changes to:

1class MultiStepForm < ActiveRecord::Base
2  #...
3
4  def save_personal_info
5    self.save if self.valid?(:personal_submission)
6  end
7
8  def save_education
9    self.save if self.valid?([:personal_submission,
10                              :education_submission])
11  end
12
13  def save_work_experience
14    self.save if self.valid?([:personal_submission,
15                              :education_submission,
16                              :work_ex_submission])
17  end
18end

A tad bit cleaner I would say.


You might also like

If you liked this blog post, check out similar ones from BigBinary