Multistep form validations with Rails and Wicked
Note: This blog post was originally posted at the Rabid Brains blog (my employer’s blog).
Multistep forms are the bane of the developer’s existence. No matter how you cut it, the fact that multiple request/response cycles are required to create a single resource goes against the grain of a whole bunch of acronyms representing fairly popular patterns and specifications (e.g. HTTP). Despite that, they’re a pretty well established usability pattern when you just have a tonne of information to collect and not much space to do it in, so it’s well worth keeping a method on hand to throw these types of forms together when it’s gotta be done.
In this post, I’m going to talk about how to create a multi-step form process in Ruby on Rails without throwing all that the framework can offer out the window. In particular, I’m going to discuss the structure the form controller can take, and how to perform incremental validations to provide useful feedback to users as they move through the fields. If you’d rather just look through the code, I’ve developed a demo application using the instructions in the blog post. You can check out the source code, or the application on Heroku.
First of all, I suggest that you don’t set out and try and build the logic for managing the form steps yourself. This part in particular can end up quite messy in Rails, and is always consistent between applications, making it prime for extraction into a Rubygem. Luckily for us, a stable, maintained gem named wicked exists that includes this exact behaviour. Use the gem, read the README, and be glad that your controllers can remain free of the numerous helpers you would otherwise have to write to manage and move between steps.
With Wicked installed, you’re all set to create your controller structure. The key thing to realise here is that if you are adding a multistep form, you are really creating a first-class resource in your application - at least, as far as your controllers are concerned. What I’m getting at here is that you musn’t try and shoehorn the controller actions you will be adding to your controller for your model resource - the fact that there are multiple steps being presented, and different fieldsets being submitted, justifies an individual controller to contain all this logic.
Let’s say the top-level resource is named Pet
. We’ll go into the attributes of the Pet
model in just a minute, but for now, that tells us that our top-level resource controller should be named PetsController
, and be routed like so:
# config.routes.rb
resources :pets, only: [:new, :create, :show, :index]
root to: 'pets#index'
Now we have a controller for managing our Pet
resource, we now need to create a controller for managing our Pet
resource form. For clarity, this should probably be in it’s own namespace (otherwise, you’ll have a fairly generic-looking FormStepsController
). You can call this controller whatever you like, but something representative of what it does would be most useful - perhaps Pet::FormStepsController
, Pet::StepsController
, or something more high-level, such as Pet::BuildController
. For the rest of this post, I’m going to use Pet::StepsController
, but you can use what you like - just be sure to make appropriate replacements when necessary. Let’s create that controller now:
./bin/rails generate controller pet/steps_controller show update
You’ll notice that I’ve generated the controller with just two actions - show
and update
. This is actually all you need for this form - remember, Wicked is handling the step logic for us, and this controller is responsible for a form step, not the resource. This means that our actions map to show
-ing a form step, and update
-ing the record with the attributes for that step. Let’s add the routes for this new controller now - remember, it will be nested within our top-level resource controller:
# config/routes.rb
resources :pets, only: [:new, :create, :show, :index] do
resources :steps, only: [:show, :update], controller: 'pet/steps'
end
root to: 'pets#index'
Notice how we can still use resources
with our new controller? Rails is going to cleverly pass through the form step in the :id
parameter for this route, as this identifies which step to show
or update
. So, the route to a form step with these routes will look like this:
/pets/1/steps/identity
- where 1
is the :pet_id
, and identity
is the :id
. Nice!
Now that we’ve got our controller structure set up, let’s leave this area for now and go take a look at our Pet
model - this is where we’ll set up our form steps, and tweak our validation to let us just check validity for a particular step.
Let’s say (for the purposes of having a model complex enough to justify multiple steps), our Pet
model has the following schema:
create_table "pets", force: true do |t|
t.string "name"
t.string "colour"
t.string "owner_name"
t.text "identifying_characteristics"
t.text "special_instructions"
t.datetime "created_at"
t.datetime "updated_at"
end
We’re going to have three form steps for this model - ‘identity’ (which will collect name, and owner_name), ‘characteristics’ (which will collect identifying_characteristics and colour), and ‘instructions’ (which will collect special_instructions). We’ll define a class-level accessor for holding our form steps:
# app/models/pet.rb
class Pet < ActiveRecord::Base
cattr_accessor :form_steps do
%w(identity characteristics instructions)
end
end
Now we can access our form steps using the following method call: Pet.form_steps
.
Next, let’s set up our validations. We’ll be requiring that all the fields be filled in, but only if you’re on the appropriate step. To check this, we need to implement a method that checks whether certain validations should be run based on what step we’re on. The validation check isn’t too simple - it’s not enough to just check the current step, because validations should still be run on previous steps, and if the current form step is nil. Let’s go ahead and implement that method now:
# app/models/pet.rb
def required_for_step?(step)
# All fields are required if no form step is present
return true if form_step.nil?
# All fields from previous steps are required if the
# step parameter appears before or we are on the current step
return true if self.form_steps.index(step.to_s) <= self.form_steps.index(form_step)
end
…hang on a minute - how do we know what form step we’re on at the moment? We don’t! Let’s add an instance-level accessor to our model to store the current form step:
# app/models/pet.rb
class Pet < ActiveRecord::Base
# ...
attr_accessor :form_step
# ...
end
Great! Now we can detect whether we need to run validations for a set of attributes or not, based on which step we’re on. Conveniently, Rails allows us to pass in an if
option to each validation that determines whether it should be run or not. We could implement the validations for the first step like so then:
validates :name, :owner_name, presence: true,
if: -> { required_for_step?(:identity)
We can go ahead and implement the validations for the other steps the exact same way - I’ll leave that as an exercise to the reader.
Before we head back to our controller, I’m just going to make a very brief suggestion. If you’re building a multistep form yourself your validations are likely to be multiple lines for each step, and possibly even for each attribute. If you do find yourself having to do that, I would encourage you to look into the with_options helper - this will let you define your if
option in a single place, and then apply all the validations within the block. Here’s an example, showing each of the attributes for the first step on individual lines:
with_options if: -> { required_for_step?(:identity) do |step|
step.validates :name, presence: true
step.validates :owner_name, presence: true
end
As you can see, it’s overkill for one-liner, simple validations, but it’s quite a nice syntax if you’ve got more complex rules.
When we last looked at our controller, it had been generated with two actions - show
and update
. Now it’s time to add Wicked to our controller, and implement our actions.
The first, and most obvious thing to do, is to include Wicked’s helpers into our controller - as per their README:
class Pet::StepsController
include Wicked::Wizard
def show
# TODO
end
def update
# TODO
end
end
Once that’s done, we need to tell Wicked what our form steps are. Now, the README suggests putting this into the controller, but we’ve already gota collection of our form steps that we can access in our model. Let’s just use that!
class Pet::StepsController
include Wicked::Wizard
steps *Pet.form_steps
# ...
end
Now our controller is set up to use Wicked, and knows which form steps to use. There’s just one more thing we need to consider before we can test this out - how do we get into our wizard form? Remember, our routes rely on having a Pet
ID already present, so we’re going to need to create our Pet instance before we enter the wizard form.
The most appropriate place to implement this is back in our top-level controller. Remember, our wizard controller is responsible for showing and updating steps, but our top-level controller is still responsible for managing our Pet
models. Semantically, this means that our create
action of our PetsController
should create the new Pet
instance. This action will work a little differently from a normal create
action that you might be used to, as it doesn’t strictly need a new
action - we won’t be saving this Pet
model with any data - just putting it in the database so that our StepsController
can access that.
Your create action can be as simple as this:
class PetsController < ApplicationController
# ...
def create
@pet = Pet.new
@pet.save(validate: false)
redirect_to pet_step_path(@pet, Pet.steps.first)
end
# ...
end
In other words, create a new pet with no data, and redirect to the first step.
If you would like a new action, you can of course add one - this could render a view that explains the form steps, or something similar to that. It’s not required for this controller to work correctly though - you can just point any links to create a new pet straight to the create
action like so:
<%= link_to 'Record a Pet', pets_path, method: :post %>
So, that will handle creating a new Pet for us, and will redirect to the start of the form. We haven’t implemented our show
action yet though - let’s do that now.
Our show action needs to find the pet (so that we can use form_for @pet
in our templates), and then call a special Wicked method called render_wizard
- this will render a template with the same name of our step - for example, if we are on the identity
step, this will render “app/views/pet/steps/identity.html.erb”. Here’s what the show action looks like:
class Pet::StepsController < ApplicationController
def show
@pet = Pet.find(params[:pet_id])
render_wizard
end
def update
# TODO
end
end
Before we give this a test, let’s quickly implement the template for the first form step:
# app/views/pet/steps/identity.html.erb
<%= form_for @pet, method: :put, url: wizard_path do |f| %>
<% if f.object.errors.any? %>
<div class="error_messages">
<% f.object.errors.full_messages.each do |error| %>
<p><%= error %></p>
<% end %>
</div>
<% end %>
<fieldset>
<legend>Pet Identity</legend>
<div>
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<div>
<%= f.label :owner_name %>
<%= f.text_field :owner_name %>
</div>
<div>
<%= f.submit 'Next Step' %>
</div>
</fieldset>
<% end %>
If you visit your first form step now, you should see your identity form. If you see this - congratulations! If you don’t, go back and check that your routes are all correct - remember you need to access the wizard form using a Pet ID that exists in the database.
We can view the form now, but if you try and hit save, you’ll notice you run into an error. This is because we have not yet implemented our update
action. This action is actually going to be very simple, because Wicked handily takes up all the heavy lifting of working out where we need to go next in our form. Let’s implement this, pretty much based off the Wicked README:
# app/controllers/pet/steps_controller.rb
class Pet::StepsController < ApplicationController
# ...
def update
@pet = Pet.find(params[:pet_id])
@pet.update(pet_params(step))
render_wizard @pet
end
end
We’re using render_wizard
, just like we did in the show
action, but this time we’re passing it an object. In this case, Wicked will check the object - if it is valid, it will continue to the next step, but if it is invalid, it will not move onto the next step, but re-render the template for the current step (where errors will be shown as appropriate if you are rendering the error messages like the identity
template above).
This looks good, but you may have noticed that we’ve introduced another new method - this one’s to do with strong parameters. If you cast your mind back to when we were working on our model, you may also remember that our conditional validations rely on having the form_step
set in the model to work correctly, and we’re not setting that anywhere here. Luckily, we can fix both of these problems by implementing the pet_params
method!
This method will use a “case/when” statement (I’ve found Alan Skorkin’s blog post to be a good summary of case/when if you need a refresher) to return the different Pet
attributes that can be updated based on the step that is passed in. It will also ‘mix-in’ the current form step to the parameters it returns so that the model knows which step it is on and can run validations accordingly.
Here’s the implementation:
# app/controllers/pet/steps_controller.rb
class Pet::StepsController < ApplicationController
# ...
private
def pet_params(step)
permitted_attributes = case step
when "identity"
[:name, :owner_name]
when "characteristics"
[:colour, :identifying_characteristics]
when "instructions"
[:special_instructions]
end
params.require(:pet).permit(permitted_attributes).merge(form_step: step)
end
end
As an example, were this to be called with the first step, it would return: {name: 'Tinkerbell', owner_name: 'Bob Jones', form_step: 'identity'}
. It will raise an error and return an appropriate status code if someone attempts to submit an attribute that is not allowed on the current step.
We now have our model set up to perform validations for whichever step we are up to, and our controller set up with Wicked
to manage working through the steps. Our form is not yet complete - we are still missing templates for the “characteristics” and “instructions” steps, but the implementation of those is left as an exercise - they will very much resemble the template for the “identity” step, just with different fields.
In terms of where you go from here, there’s plenty of scope for improvement! There is some code in our steps controller that can be refactored to not be repeated, and you might want to split some of the common elements in your step templates into partials (for example, the submit button and/or the error rendering). You might also want to tweak when validation errors should/shouldn’t be run (as an example of this, I have implemented a variant of this form where validations are not run until the final step). You may also want to check out the following resources:
- Demo application developed using this blog post: App, Source
- Ryan Bates’ Railscast on Wicked
- Wicked README