Welcome to Giant Robots Smashing Into Other Giant Robots — a weblog about development, business, design and technology — written by thoughtbot.

it's the wiz and nooobody beats it

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.
1
2
3
4
5
6
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:
1
2
3
4
  def edit
    @step = params[:step]
    @user = User.find params[:id]
  end
The view is more important. 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.

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| -%>
  <p>
    <label for="user_name">Name</label>
    <%= form.text_field :name %>
  </p>
  <p>
    <%= submit_tag 'Submit' %>
  </p>
<% 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  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:
1
2
3
4
5
6
7
  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:
1
2
  User.find :all,
    :conditions => 'created = true'

Here’s the last step in the wizard:

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| -%>
  <p>
    <label for="user_bio">Bio</label>
    <%= form.text_area :bio %>
  </p>
  <p>
    <%= submit_tag 'Submit' %>
  </p>
<% 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.


About this entry

 

thoughtbot is hiring

We are hiring web developers and web designers in both Boston and New York, NY.

What are we up to?

We built Shoulda, an eclectic set of additions to Test::Unit; Paperclip to manage uploaded files without hassle; Jester, a REST/ActiveResource client library written in Javascript, and Squirrel, an enhancement for ActiveRecord's find syntax; — amongst some other projects.


Chad (President) and Jon (CTO) co-authored a technical book titled Pro Active Record: Databases with Ruby and Rails, which explores the ins and outs of the ActiveRecord ruby library. You can buy it today at Amazon.com.

About thoughtbot, inc.

We are a small web application development consulting business, with offices in Boston, MA and New York, NY. If you're looking to find a team for your next web development project or your new web application — get in touch.