We all love wizards.
Here’s a common pattern I use for wizards.
It sucks and the code’s ugly but it works.
We’re going to create a 3-step wizard for creating a User
.
class User < ActiveRecord::Base
validates_presence_of :email, :password
validates_confirmation_of :password
end
schema:
users (email, password, name, bio, created)
So in the first step we’ll collect all the required User
information: email
and password.
We can do this using #new and #create in our UsersController
.
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new params[:user]
if @user.save
redirect_to edit_user_path(:id => @user,
:step => 'b')
else
render :action => :new
end
end
end
But instead of redirecting to #show
after a POST to #create
we redirect to
#edit
. I also pass in the fact that I’m now on step ‘b’ (the second step) of
this wizard.
Here’s #edit
:
def edit
@step = params[:step]
@user = User.find params[:id]
end
The view is more important.
In app/views/users/edit.rhtml
:
<%= render :partial => @step, :locals => { :user => @user } %>
That’s going to look for a partial named _b.rhtml
when going to the second
step in the wizard. This way we avoid conditional logic in this view.
In app/views/users/_b.rhtml
:
<h2>Step Two</h2>
<%= error_messages_for :user %>
<% form_for :user,
:url => user_path(:id => @user,
:step => @step),
:html => { :method => :put } do |form| -%>
<label for="user_name">Name</label>
<%= form.text_field :name %>
<%= submit_tag 'Submit' %>
<% end -%>
Here we collect some optional User
information in a form that PUTs to #update,
passing along the current step in the wizard.
Here’s #update:
def update
@step = params[:step]
@user = User.find params[:id]
if last_step?
if @user.update_attributes params[:user].merge(:created => true)
redirect_to user_path(@user)
else
render :action => :edit
end
else
if @user.update_attributes params[:user]
redirect_to edit_user_path(:id => @user,
:step => @step.succ)
else
render :action => :edit
end
end
end
private
def last_step?
params[:step] == 'c'
end
Now #update says if its the last step update it and redirect to #show, else update it and redirect back to edit and increase the step by 1.
So you say What if I don’t want a User
to be shown on my site that hasn’t
fully completed the wizard? That is the purpose of the created
boolean in the
users
table. The following block of code from #update sets created
to true
if its the last step in the wizard:
if last_step?
if @user.update_attributes params[:user].merge(:created => true)
redirect_to user_path(@user)
else
render :action => :edit
end
else
That’s the hack. Having a boolean in your model to prevent incomplete models
from showing up on your site. It’s terrible because now all your User
queries
will have to include the boolean:
User.find :all,
:conditions => 'created = true'
Here’s the last step in the wizard:
In app/views/users/_c.rhtml
:
<h2>Step 3 (last step)</h2>
<%= error_messages_for :user %>
<% form_for :user,
:url => user_path(:id => @user,
:step => @step),
:html => { :method => :put } do |form| -%>
<label for="user_bio">Bio</label>
<%= form.text_area :bio %>
<%= submit_tag 'Submit' %>
<% end -%>
It collects more optional User
in a form that PUTs to #update just like step
‘b’ (2) did.
You might be wondering why I chose letters instead of numbers for my wizard stages. Rails complains if you try to have a partial named 1.rhtml or 2.rhtml for some reason.