HomeRamblings  ⁄  ProgrammingRuby Language

A model-free wizard

Published: May 13, 2008 (over 9 years ago)
Updated: over 2 years ago

Maybe I’m taking the whole MVC thing too far, but I’ve been reading and learning both Ruby and Rails at a fairly fast clip and just when I thought I was getting the hang of what goes in models, views, and controllers respectively, along comes The Advanced Recipe for Rails book with a recipe for implementing a wizard that threw me for a loop here. This implementation just so happens to be based off acts_as_state_machine, which plugs into your model classes. And of course, the acts_as_wizard plugin wasn’t too far behind! Well, asking myself whether the wizard logic should be allowed to bleed over into the model prompted me to see just how hard it was to build a wizard in a web interface anyhow (this is my first attempt at a multi-step workflow bit on the web). Most of the standard Rails activities apply to setting up and getting things going, so I’m just going to dive straight into the details of of the particulars of how I made this wizard live with all its logic in the controller and views. First thing’s first was getting a layout that would sport the usual wizard accouterments such as the title, “Step X: You’re Here, do this,” The entry form for the step, and of course, the Previous and Next buttons. After some thoughts on the matter, I decided on the following conventions

  • The wizard steps would follow the naming convention of step_do_some_activity where do_some_activity was the shortest thing I could think of to describe the focus of the step at hand.
  • The views would have the name step_do_some_activity.html.erb to match the step name.
  • The wizard controller (lets call it MyWizardController for this article) had two methods added for each step: step_do_some_activity which rendered the wizard step and do_some_activity which executed the form submissions on that step’s wizard form.

After I did the first three steps_do_some_activity, I realized I was repeating the code, so I wrote one method and called it from all the actions to get a little DRY action going (hey, gotta start somewhere before you can get into all that meta-programming wizardry).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def render_wizard_steps
  @todo = Todolist.find(params[:id])
  respond_to do |format|
    format.html # show.html.erb
    format.xml  { render :text => @todolist.data_xml }
  end
end

def step_name_the_list
  render_wizard_steps
end

def step_add_item
  render_wizard_steps
end

def step_schedule_item
  render_wizard_steps
end

I pretty much just kept the existing update and create actions, but tweaked them just a tad so that they essentially launched you into the wizard. Notice how their end-response is to drop you onto the second step of the wizard, “step_add_item.” This is how I break away from the standard Rails processing to begin letting the wizard then control the flow. A fairly simple approach to both leveraging the Rails framework while extending it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def update
  @todolist = Todolist.find(params[:id])

  respond_to do |format|
    if @todolist.update_attributes(params[:todolist])
      format.html { redirect_to :action => "step_add_item" }
      format.xml  { head :ok }
    else
      format.html { render :action => "edit" }
      format.xml  { render :xml => @todolist.errors, :status => :unprocessable_entity }
    end
  end
end

def create
  @todolist = Todolist.new(params[:todolist])

  respond_to do |format|
    if @todolist.save
      format.html { redirect_to :id => @todolist, :action => "step_add_item" }
      format.xml  { render :xml => @todolist, :status => :created, :location => @todolist }
    else
      format.html { render :action => "new" }
      format.xml  { render :xml => @todolist.errors, :status => :unprocessable_entity }
    end
  end
end

To be sure I didn’t try to land in the middle of the wizard without an object ID, I just simply redirected those all to the index action if an ID didn’t exist in the params hash. Yet, I still wanted to go ahead and get a jump into the wizard’s first page, so I cooked up the following:

1
2
3
4
5
6
7
def show
  params[:id].nil? ? redirect_to(:action => "index") : redirect_to(:id => params[:id], :action => "step_name_the_list")
end

def edit
  params[:id].nil? ? redirect_to(:action => "index") : redirect_to(:id => params[:id], :action => "step_name_the_list")
end  

Since the wizard is mostly concerned with adding child objects to a parent object, I chose to let the parent object know how to add children to itself, and the actions on the wizards more or less reduced themselves to the following example:

1
2
3
4
5
def add_todo_item
  @todolist = Todolist.find(params[:id])
  @todolist.add_item(params[:todo_item])
  redirect_to :id => @todolist, :action => "step_add_item"
end

To pull it together, I had a wizard layout that I designed a title area, sidebar, content, and footer (all css div tags), then I was able to put wizard instructions in the sidebar, Next/Previous links in the footer, and “Step X: You’re here” titles on all the wizards. That left the contents for each wizard step, which simply reduced to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<h1>Step:  Name Your List</h1>

<% content_for :sidebar do %>
<p>To get started, provide a name for your TODO list.</p>
<% end %>

<% form_for @todolist, :url => {:id => @todolist.id, :controller => 'my_wizard', :action => 'update'} do |f| %>
  <p>
    <label for="todolist_name">Name</label><br/>
    <%= text_field 'todolist', 'name'  %>
  </p>
  <p>
    <%= f.submit "Update" %>
  </p>
<% end %>

<div class="actions">
  <div style="float:left"><%= link_to 'Back to Existing Queries', my_wizard_index_path %></div>
  <div style="float:right"><%= link_to 'Go to Next Step', my_wizard_add_fact_path %></div>
</div>

Notice that the forward and backward steps are in the views. This worked well for me conceptually as at the time I’m debugging navigation issues, I’m right there with the view in my editor and can fix those on the spot. I’m not having to find my model (or controller) and pull up that information. The controller was very straightforward with the naming convention established as in this wizard’s case, I was repeatedly doing some task on a step (i.e. adding items to a todo list) before going to next step. So, all I really had to remember for the next new step dreamed up was that the updating method name had the same name as the rendering action, sans the “step_” prefix. The one last thing you have to do to tie it all together so you can use the URL helper classes is to add the named routes to routes.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
map.my_wizard_new_query \
  'my_wizard/step_name_the_list/:id', 
  :controller => 'my_wizard', 
  :action => 'step_name_the_list'
  
map.my_wizard_add_fact \
  'my_wizard/step_add_item/:id', 
  :controller => 'my_wizard', 
  :action => 'step_add_item'

map.my_wizard_add_filter \
  'my_wizard/step_schedule_item/:id', 
  :controller => 'my_wizard', 
  :action => 'step_schedule_item'

Some may choose to build a hash that gets passed along as the wizard progresses and then save that big hash at the conclusion of the wizard. I chose to build persistent objects in the database and update them along the way. This really made things easy as I didn’t have a big huge hash at the end that I had to process and run the risk of “oops, something went wrong, could you go back and redo all the steps from scratch?” nor was I dealing with session timeouts and unsaved data.

Another pleasing aspect of this approach was that I was able to put a wizard link next to the usual show | edit | delete actions in the index page view for the Todo Lists and, with my named routes, easily relaunch into the wizard on an existing set of todo list objects. No extra coding or constructing a hash to get going. It was just there and through the magic of Rails, already knew how to pre-populate the forms of each wizard step with the data.

comments powered by Disqus