#346 Wizard Forms with Wicked
- Download:
- source codeProject Files in Zip (92.6 KB)
- mp4Full Size H.264 Video (34.1 MB)
- m4vSmaller H.264 Video (14.9 MB)
- webmFull Size VP8 Video (16.8 MB)
- ogvFull Size Theora Video (31.8 MB)
Below is a long user sign-up form with a large number of fields. Forms like this can be quite intimidating to a potential user and might scare them away.
One option when faced with a complex form is to turn it into a multi-step form, also known as a wizard. The easiest way to do this is to use JavaScript; this way we can keep everything on the client and we don’t need to make any changes to our Rails application. This isn’t always the best option, however. We might want the data to be more persistent and to be able to store each step’s data in the database or we might want the form to be dynamic so the steps change depending on the Rails app. We’ll probably want to add validations to some of the fields in each step, too.
Introducing Wicked
If we want to manage a wizard through a Rails app we should consider using Richard Schneeman’s Wicked gem. This adds behaviour to a Rails controller to turn it into a multi-step form and we’ll show you how it works in this episode. The first step is to strip our long form down to as few fields as possible - a good rule of thumb is to only require the information necessary for the user to be able to access the record later. In the case of a signup form we can cut the form down to the fields related to authentication, i.e. the username and password fields. Everything else can go into separate steps in our wizard so we’ll reduce our form to just these fields.
<h1>Sign Up</h1> <%= form_for @user do |f| %> <% if @user.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :email %><br /> <%= f.text_field :email %> </div> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation %> </div> <div class="actions"> <%= f.submit "Sign Up" %> </div> <% end %>
When we reload the form now we’ll see just those fields. This will become the first step of our wizard.
At this point it’s a good idea to ask yourself if you really need a wizard. We could just have a separate “edit profile” page and have those fields there but we’ll assume that a wizard is the best approach here and carry on so we’ll add the Wicked gem to the gemfile and then run bundle to install it.
gem 'wicked'
Next we’ll generate a new controller called user_steps
. This will give us a controller dedicated to managing the wizard’s steps. We can call this controller anything we like as Wicked won’t care.
$ rails g controller user_steps
We’ll need to add a new user_steps
resource to our routes file for that controller.
Signup::Application.routes.draw do resources :users resources :user_steps root to: 'users#index' end
Now in our new controller we need to include the Wicked::Wizard
module. This gives us a steps
method that we can use to define the steps in our wizard after the user is created. We’ll add two steps to our form, one for personal information and another related to the social networks the user is a member of. This controller is expected to respond to the show
action and this action should render out a given step of the wizard. We have access to a render_wizard
method that will look for a form view template for each step.
class UserStepsController < ApplicationController include Wicked::Wizard steps :personal, :social def show render_wizard end end
We’ll need to create these views under the /app/views/user_steps
directory. We’ll create files here called personal.html.erb
and social.html.erb
. For now we’ll just add the words “personal” and “social” to these files so that we can distinguish the steps. We should now be able to access each step at /user_steps/<step_name>
.
This is how render_wizard
works. It renders out a template whose name is passed in through the URL. If we visit the UserStepsController
’s index
action this will redirect us to the first step in the process, in this case the personal
step. We need to have the /users/new
action redirect us to the first step of the wizard when we submit that form. Submitting the form triggers the UsersController
’s create
action.
def create @user = User.new(params[:user]) if @user.save session[:user_id] = @user.id redirect_to users_path, notice: "Thank you for signing up." else render :new end end
The action saves the new user then logs them in by saving their id
in a session variable. When this happens we want to redirect the user to the first step of the wizard instead of the page they’re currently redirected to.
def create @user = User.new(params[:user]) if @user.save session[:user_id] = @user.id redirect_to user_steps_path else render :new end end
This works. When we sign up now we’re redirected to the “personal” step of our new wizard. Now we need to add a form to that page that we’ll redirect to the second page of the wizard when it’s submitted. If we want a form for editing user details on each render_wizard
step we’ll need to fetch the current user in the UserStepsController
’s show action. We already have a current_user
method in our application and if you have some kind of authentication solution in your application you’ll generally have a method to fetch the current user. If you don’t you can pass in the user’s id
when you redirect to the UserStepsController
.
def show @user = current_user render_wizard end
We can now replace the placeholder text in the personal template with a form with the relevant fields.
<%= form_for @user, url: wizard_path do |f| %> <h2>Tell us a little about yourself</h2> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :date_of_birth %><br /> <%= f.date_select :date_of_birth, start_year: 1900, end_year: Date.today.year %> </div> <div class="field"> <%= f.label :bio %><br /> <%= f.text_area :bio, rows: 5 %> </div> <div class="actions"> <%= f.submit "Continue" %> </div> <% end %>
We’ve had to pass in a url
to this form. Normally it will be submitted to the UsersController
but we want it to go to the UserStepsController
. We can use the wizard_path
helper method here which will POST the form to the correct action. The form basically contains the first three fields that we removed from the original form along with a submit button. When we reload the page now we’ll see that new form.
Clicking “Continue” will trigger the update
action but we haven’t created that yet. It will be similar to the show
action but will update some of the current user’s attributes based on the values from the form.
def update @user = current_user @user.attributes = params[:user] render_wizard @user end
Note that we pass the current user to render_wizard
here. When we pass in a resource like this it will attempt to call save
on it and if this succeeds it will go on the next step. If saving the resource fails we’ll see the current step again.
We can add the form to the social
template now.
<%= form_for current_user, url: wizard_path do |f| %> <h2>Where can we find you?</h2> <div class="field"> <%= f.label :twitter_username %><br /> <%= f.text_field :twitter_username %> </div> <div class="field"> <%= f.label :github_username %><br /> <%= f.text_field :github_username %> </div> <div class="field"> <%= f.label :website %><br /> <%= f.text_field :website %> </div> <div class="actions"> <%= f.submit "Continue" %> </div> <% end %>
Submitting this form will trigger the update
action again but as there aren’t any further steps it will then redirect back to the application’s root page. If we want to change this behaviour we can override a redirect_to_finish_wizard
method in the controller. We’ll still redirect to the root URL but also show a message that thanks the user for signing up.
private def redirect_to_finish_wizard redirect_to root_url, notice: "Thanks for signing up." end
Skipping Steps
The next thing we’ll do is add a link next to the “Continue” button that allows the user to skip a step. If we look at the Quick Reference section2 of the README we’ll find a list of the methods that Wicked provides and included in these is next_wizard_path which returns the URL of the next step. We can use this to make our “skip” link.
<div class="actions"> <%= f.submit "Continue" %> or <%= link_to "skip this step", next_wizard_path %> </div>
We’ll do the same thing in the other step, too. Now we have a “skip this step” link on each of the wizard so that the user knows that the fields on that step are optional and can be skipped.
Validations
Next we’ll look at validations. Let’s say that we want to validate the format of the Twitter username on the “Social” step of the wizard. We’re currently not displaying any error validation messages so we’ll page in some code on each step to do so. If this application was going into production we’d probably move this code into a helper method to reduce the duplication but here we’ll let that slip.
<% if @user.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>
Now we can add validation to our User
model. We’ll use validates_format_of
and it’s important to set its allow_blank
option to true to make this an optional parameter so that the validation isn’t triggered when we save the user’s other details on a different step.
class User < ActiveRecord::Base attr_accessible :bio, :date_of_birth, :email, :github_username, :name, :password, :password_confirmation, :twitter_username, :website has_secure_password validates_format_of :twitter_username, :without => /\W/, :allow_blank => true end
What, though, if we do want to make sure that a Twitter username has been supplied? This is where things can get a little tricky as the user is saved when the first step is completed. It’s easy to validate the presence of the username and password fields on the first step but if we try to validate fields from later steps the new User
record will be invalid and won’t save on the first. To get around this we can make the validation conditional so that the Twitter username field is only validated on the “Social” step.
validates_presence_of :twitter_username, if: :on_social_step?
Now we need to write the on_social_step?
method which will check to see which step we’re on and make the validation conditional on its return value. We won’t write that method here, though. If you need to do something like this it’s worth taking a look at the Partial Validation of Active Record Objects page of the Wicked wiki which explains exactly this situation. For an alternative solution take a look at episode 217 which shows how to build a multi-step form from scratch.
Removing Duplication
Our wizard form is now almost complete but there’s a lot of duplication in the different steps of the code. Apart from the headline and field names the two steps are almost identical which makes this a perfect case for a partial layout. We can move the headline to the top of the page, outside the form, and then move the error messages and submit button in to a new form
partial. Instead of rendering the partial as partial, however, we’ll render it as a layout and pass the form builder to it.
<h2>Tell us a little about yourself</h2> <%= render :layout => 'form' do |f| %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :date_of_birth %><br /> <%= f.date_select :date_of_birth, start_year: 1900, end_year: Date.today.year %> </div> <div class="field"> <%= f.label :bio %><br /> <%= f.text_area :bio, rows: 5 %> </div> <% end %>
The new partial form will look like this. Note that we call yield
between the error messages and the button so that the form fields are rendered there.
<%= form_for @user, url: wizard_path do |f| %> <% if @user.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <%= yield f %> <div class="actions"> <%= f.submit "Continue" %> or <%= link_to "skip this step", next_wizard_path %> </div> <% end %>
We can now do the same thing inside the “Social” template to clean that template up, too. Our form will work just as it did before but now there’s no duplication across the different steps.