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.


class User < ActiveRecord::Base

validatespresenceof :email, :password validatesconfirmationof :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 redirectto edituser_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.

app/views/users/_b.rhtml


Step Two

<%= error_messages_for :user %> <% form_for :user, :url => user_path(:id => @user, :step => @step), :html => { :method => :put } do |form| -%>

<%= 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 laststep?
      if @user.updateattributes params[:user].merge(:created => true)
        redirectto userpath(@user)
      else
        render :action => :edit
      end
    else
      if @user.updateattributes params[:user]
        redirectto edituserpath(: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 laststep?
    if @user.updateattributes params[:user].merge(:created => true)
      redirectto userpath(@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:

app/views/users/_c.rhtml


Step 3 (last step)

<%= errormessagesfor :user %> <% formfor :user, :url => userpath(:id => @user, :step => @step), :html => { :method => :put } do |form| -%>

<%= form.textarea :bio %>

<%= submittag '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