it's the wiz and nooobody beats it

Jared Carroll

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.

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.

Jared Carroll
upcase

Sharpen your programming skills by completing coding exercises that are reviewed by other developers at Upcase today.