/wizardly

create a functioning wizard for any model in three steps

Primary LanguageRubyMIT LicenseMIT

wizardly

wizardly creates a multi-step wizard for any ActiveRecord model in three steps.

Resources

Source

  • git://github.com/jeffp/wizardly.git

Install

Examples

Contributions

Thanks for feedback from Roland Schulz, Steve Hoeksema, Morgan Christiansson, Jonathan Clarke, Nate Delage

Description

wizardly builds on Alex Kira’s validation_group plugin code to DRY up the Rails MVC implementation of a wizard. In three easy steps, wizardly produces the scaffolding and controller of a multi-step wizard.

Features include:

  • Model-based Wizard Definition

  • Wizard Controller Macros

  • Wizard Scaffold Generators

  • Sizard Form Helpers

  • Configurable Buttons

  • Callbacks

  • Paperclip Support

Example

Create a working controller wizard for any model in 3 steps. Here’s how:

Step 1: Define validation groups for your model.

class User < ActiveRecord::Base    
  validation_group :step1, :fields=>[:first_name, :last_name]
  validation_group :step2, :fields=>[:age, :gender]
  ...
end

Step 2: Tell your controller to act ‘wizardly’.

class SignupController < ApplicationController
  act_wizardly_for :user
end

Step 3: Generate a ‘wizardly’ scaffold for your controller.

./script/generate wizardly_scaffold signup

You are ready to go: start your servers.

General usage and configuration of wizardly follows. See the examples at

http://github.com/jeffp/wizardly-examples

Setup

Put the following in your application’s config block in config/environment.rb

config.gem 'wizardly'

The temporarily (or permanently depending on github’s reinstatement of gems) resides at gemcutter.org. Make sure you have added gemcutter.org to your gem sources

and run the install gems rake task on your application

rake gems:install

Setup Recommendations

Wizardly uses sessions. It is recommended you use a session store other than the

default cookies store to eliminate the 4KB size restriction. To use the active record store, add this line to your environment.rb

config.action_controller.session_store = :active_record_store

And set up the sessions table in the database

rake db:sessions:create
rake db:migrate

Use the MemCached session store for higher performance requirements.

Inspecting A Wizard Controller

This is optional, but for any rails app, you can install wizardly rake tasks by running

./script/generate wizardly_app

Once installed, you can run the config rake task to inspect a wizardly controller

rake wizardly:config name=controller_name

The controller_name you give it must have a act_wizardly_for declaration for an existing active record model and database table. (Don’t forget to migrate your database) The rake task will display the wizard’s buttons, pages and fields along with other configuration information.

The Basic Wizard

Wizardly creates a controller action for each validation group in the model. Likewise, the wizardly scaffold generates a view named for each corresponding action.

Action Instance Variables

The actions automatically instantiate a few instance variables: @step, @wizard, @title and @{model_name}. @step contains the symbol corresponding to the action name and is used in the wizard view helpers. @wizard references the wizard configuration object and is primarily used by the view helpers.

@title contains the string name of the action by default and is used in the scaffolding. Finally and most importantly of all, the action instantiates an instance of the model. For instance, if the model is :account_user for AccountUser class, the action instantiates an @account_user instance.

Flow

The page-to-page flow of the wizard is assumed to occur in the order that the validation groups were declared in the model. A ‘next’ button progresses to the next page in the order; a ‘back’ button progresses to the previous page in the order. The first page in the order will have no ‘back’ button. The final page in the order will no ‘next’ button, but instead have a ‘finish’ button which completes the wizard by saving the model instance to the database. Every page will have a ‘cancel’ button.

Automatic Page Progression Tracking

The default progression is linear, progressing from one page to the next in the defined order. In the default case, the ‘back’ button simply jumps back to the previous page. However, the developer can create more complex flows by jumping and skipping pages in the action callback methods. Wizardly tracks the forward progression through a wizard and back tracks accordingly for the ‘back’ button.

Wizard Entrance Guarding

Since there is an assumed order for the wizard, entering the wizard at a later page makes no sense. Wizardly, however, guards entry to the wizard. If the user has never entered the wizard, then entry is always redirected to the first page. If the user has partially completed the wizard and is allowed to re-enter the wizard to complete it, wizardly only allows entry at or before the page previously completed.

The guarding feature can be disabled for testing or other purposes.

Basic Usage

Completing and Canceling

Once a user has entered a wizard, there are two ways they can leave, either by completing or canceling the wizard. The redirects for these two cases need to be defined. If neither are defined, then the HTTP_REFERER on first entry of the wizard is used as the redirect for both cases.

The 3-step example given above is very simple. In fact, no redirects have been defined for completing or canceling the wizard so the wizardly controller will use the HTTP_REFERER for both cases. If there is no HTTP_REFERER, the controller raises a RedirectNotDefined error. Let’s remedy this problem by adding redirect options to the macro.

class SignupController < ApplicationController
  act_wizardly_for :user, 
    :completed=>'/main/finished', 
    :canceled=>{:controller=>:main, :action=>:canceled}
end

Now whether the user completes or cancels the wizard, the controller knows how to redirect. If both canceling and completing the wizard redirect to the same place, the following option takes care of both

class SignupController < ApplicationController
  act_wizardly_for :user, :redirect=>'/main'
end

These redirects are static, and may not suffice for all cases. In the event that the redirect needs to be determined at run-time, the developer can use a number of page callback macros to intercede in the wizard logic and redirect as needed on canceling or completion. See Callbacks below.

Controller Options

Here’s a list of options for the act_wizardly_for controller macro

:completed => '/main/finished'
:canceled => {:controller=>'main', :action=>'canceled'}
:skip => true
:cancel => false
:guard => false
:mask_fields => [:password, :password_confirmation] (by default)
:persist_model => {:per_page|:once}
:form_data => {:session|:sandbox}

Setting the :skip option to true tells the scaffold helpers to include a skip button on each page. The :cancel option set to false removes the ‘cancel’ button from the wizard views. The :mask_fields options tells the scaffold generator which fields to generate as ‘type=password’ fields. Remaining options are explained below.

Preserving Form Field Data

The :form_data option controls how the form data is preserved when a user leaves or navigates to a page outside of the wizard before completing the wizard.

The default setting, :session, maintains the form data until the wizard is complete regardless of whether the user leaves the wizard and returns later. The form data is preserved for the life of the session or until the user completes the wizard.

The other setting, :sandbox, clears the form data whenever the user leaves the wizard before the wizard is complete. This includes pressing a :cancel button, a hyperlink or plainly navigating somewhere else. Upon returning to the wizard, the form is reset and the user starts fresh from the first page.

The form data is always cleared once the user has completed the wizard and the record has been committed.

Guarding Wizard Entry

The :guard option controls how a user may enter the wizard. If set to true, the default, the wizard is guarded from entry anywhere except the first page. The wizard controller will automatically redirect to the first page. When set to false, entry may occur at any point. This may be useful for testing purposes and instances where the application needs to navigate away and return to the wizard.

The guarding behavior works a little differently depending on the :form_data setting. When :form_data is set to :session (the default behavior), guarding only occurs for the initial entry. Once a user has entered the form and started it, while form data is being kept, the application may thereafter enter anywhere. On the contrary, if :form_data is set to :sandbox, entry is always guarded, and once the user leaves the wizard, entry may only occur at the initial page (as the form data has been reset).

Persisting The Model

The :persist_model option controls how the model is saved, either :once or :per_page. The default setting :per_page, saves the model incrementally for each page that validates according to its validation group. This setting may result in invalid and incomplete records in the database.

This problem can be remedied by using the :once setting, in which case, form data is kept in the session until the wizard is complete and then saved out to the database.

The :once option does not work in all cases, particularly for any attribute that can not be marshalled and dumped in the session. In particular, this will not work for forms with file uploads like any forms using Paperclip. When using Paperclip, use the :per_page setting.

Buttons

The wizardly controller defines five default button functions: next, back, skip, cancel, and finish. All but :skip are used in the scaffolding by default. You can add :skip functionality to all pages by adding an option to the macro

class SignupController < ApplicationController
  act_wizardly_for :user, :redirect=>'/main', :skip=>true
end

You can create, customize and change buttons for any controller and any page. See the Button Customization section.

Callbacks

There are two kinds of callbacks – general and action. Action callbacks occur based on a controller action. General callbacks occur based on a general wizard event.

Action Callback Macros

Action callback macros are available for injecting code and control into the wizard logic for each page. For instance, say our model declares the following validation group

validation_group :step4, :fields=>[:username, :password, :password_confirmation]

In the case that there’s a validation error, we’d like to clear the password fields before re-rendering when one of the fields is invalid. For this purpose, we can use the on_errors action callback macro.

class SignupController < ApplicationController
  act_wizardly_for :user, :redirect=>'/main'

  on_errors(:step4) do
    @user[:password] = ''
    @user[:password_confirmation] = ''
  end
end

Here’s the list of action callbacks macros that the developer can use for any action

on_back(:step)        # called when the :back button is pressed
on_skip(:step)        # called when the skip button is pressed
on_cancel(:step)      # called when the :cancel button is pressed
on_next(:step)        # called when the :next button is pressed for a valid form (post only)
on_finish(:step)      # called when the :finish button is pressed for a valid form (post only)

on_post(:step)        # called at the beginning of a POST request
on_get(:step)         # called before rendering a GET request
on_errors(:step)      # called before re-rendering the form after form invalidation (on a POST request)

The first five callbacks are related to the wizard buttons. Each callback gives the developer a chance to intervene before the impending render or redirect caused by a button click.

Each callback macro consists of a list of actions or the symbol :all as the argument and a block defining the code. For example

on_post(:step1) do
  ...
end
on_back(:step2, :step3, :step4) do
  ...
end
on_get(:all) do
  ...
end

Passing a non-existing action name raises a CallbackError.

The block in a callback is called in the controller context, thus giving it access to all controller variables and methods including the model instance, controller methods like redirect_to, and controller variables like params, request, response and session.

The model instance variable is available for all action callback macros.

The Wizard’s Action Request Cycle

The wizard page is first requested through a GET to an action. In this GET request, the wizard action creates the instance variables @step, @title and @wizard, and builds the model instance variable @{model_name}. Action callbacks may occur in the following order.

#GET request callback order

on_back, on_skip, on_cancel
on_get
render_wizard_form

If the wizard detects that a back, skip or cancel button has been pressed, the corresponding callback is called if implemented. If the developer does nothing in the callbacks, default handlers will redirect accordingly and the on_get and render_wizard_form callbacks will not be called (Note: render_wizard_form is a general callback and is included for completeness) Once rendered, the page is presented to the user with a selection of fields and wizard buttons for posting the form.

When the form data is returned by a POST request, the action creates the instance variables and builds the model instance using the form data. The on_post callback is called at the beginning of the post, then the wizard checks for back, skip and cancel buttons. If neither of those buttons were pressed, it proceeds to validate the form, calling the on_errors callback if form validation fails, re-rendering and sending the page with errors. If validation succeeds, the action determines whether the POST request signifies a ‘next’ or a ‘finish’ request and calls the corresponding callback if implemented. The callback order for a POST request is as indicated below. (The on_completed callback is a general callback called once the wizard is completed and the model has been committed to the database)

#POST request callback order

on_post
on_back, on_skip, on_cancel
on_errors
  render_wizard_form   # only if errors
on_next
on_finish
  on_completed   # only if completed

Rendering with on_get and on_errors

The on_get and on_errors callbacks are called just before rendering the form. These callbacks are a good place to declare extra variables needed to render the form.

on_get(:step2) do
  setup_step2_form
end

on_errors(:step2) do
  setup_step2_form
end

def setup_step2_form
  @contact_options = [%w(phone 1), %w(email 2), %w(mail, 3)]
end

If you have a variable that goes in every page, render_wizard_form is called for every page.

Modifying form data with on_post

The on_post callback is the first callback in the chain of a POST request and is a good place to modify form input such as adding capitalization to a form. Modification should happen through the model instance variable and not the controller’s params variable.

Redirecting and rendering are not allowed in the on_post callback. Doing so will raise an error.

Modifying Flow with on_next

on_next is called when a form has posted validly and the wizard is ready to move to the next page. This is a good opportunity to modify form flow for more complex forms by redirecting and skipping pages. See the STI Model example in github.com/jeffp/wizardly-examples for an example of a wizard with two paths based on user input.

on_next(:step1) do
  redirect_to(:action=>:step3) if @contributor.is_volunteer?
end
on_next(:step2) do
  redirect_to(:action=>:step4)
end

In the above example, :step 3 is a page for a volunteer, and :step2 is a page for a non-volunteer.

Completing the wizard with on_next

Sometimes you may want to complete the wizard based on user input rather than a ‘finish’ button. You can call the complete_wizard method. See the completing wizard programmatically example below.

Final modifications with on_finish

on_finish callback is called when the user presses a ‘finish’ button and form validation is successful (for the validation_group). on_finish is a good place to make any final modifications before the model instance is committed to the database.

Alternatively, if you want to stop the completion process, you can call the do_not_complete method in the on_finish callback.

General Callback Macros

There are two general callback macros: render_wizard_form and on_completed. These are not tied to any action or set of actions.

render_wizard_form

For anyone needing to handle rendering in a special way, wizardly provides a render call back for this.

class SignupController < ApplicationController
  act_wizardly_for :user, :redirect=>'/main'

  def render_wizard_form
    respond_to do |format|
      format.html
      format.xml { render_xml(@step) }
    end
  end

  def render_xml(step)
    ...
  end
end

Dynamic redirecting with on_completed

The on_completed callback is called once the model instance has been committed to the database. If you need any fields generated from committing the model, such as an ID, to redirect on completion, the on_completed callback is the place to do this.

on_completed do
  redirect_to post_path(@post)
end

Helper methods

Wizardly provides some helper methods which are useful in callbacks.

Completing the Wizard Programmatically

Perhaps you want to complete a wizard based off of a test instead of a button click. You can do this in your callbacks by calling the complete_wizard method.

on_next(:step4) do
  if (test_radio_button)
    complete_wizard
  end
end

Complete wizard will save the model and redirect to the :completed redirect setting. You can change the redirect dynamically by passing it to the method.

complete_wizard(some_model_path)

Rerendering from a callback

Sometimes it is useful to re-render the form and send the response to the user immediately. Wizardly provides a render_and_return method for this purpose. If a callback is triggered from a POST request, and the callback needs to re-render, this is the method.

on_back(:step2) do
  if (something_mandatory_not_selected)
    flash[:notice] = 'Please make a selection before going back'
    render_and_return
  end
end

Creating Scaffolds

Wizard scaffolds can be created for any wizardly controller (one using the acts_wizardly_for macro).

./script/generate wizardly_scaffold controller_name --haml

The wizardly_scaffold generator will create HTML view scaffolds by default. Append a –haml option to create scaffolds in HAML.

Sometimes you have already edited views from a scaffold but want to regenerate the scaffold because of changes to your model without overwriting the current views.

Use the –underscore option to create corresponding views with an underscore prefixing each page.

./script/generate wizardly_scaffold controller_name --underscore

You can create a scaffold using image_submit_tag by doing the following:

./script/generate wizardly_scaffold controller_name --image_submit

Default button images are provided under the public/images/wizardly/ directory.

Button Customization

The buttons used by the wizard and the view helpers can be customized as follows.

Changing Names of Default Wizard Buttons

The wizard supports five default button actions– :next, :back, :cancel, :skip and :finish. The default names for the buttons are the corresponding capitalized string – ‘Next’, ‘Back’, ‘Cancel’, ‘Skip’ and ‘Finish’.

The default button names can be customized in the act_wizardly_for code block

class UserSignupController < ApplicationController
  act_wizardly_for :user, :redirect=>'/main/index' do
    change_button(:back).name_to('Previous Page')
    change_button(:finish).name_to('Save and Return')
  end
end

With the above code, the ‘Back’ button will now be named ‘Previous Page’ and the ‘Finish’ button is renamed to the longer name ‘Save and Return’.

Notice that symbols are used to determine the default button, but the customized name is passed. A new symbol representing the change is created for each change, hence, for our buttons above :back is now referred to as :previous_page and :finish is now referred to as :save_and_return.

Action Callbacks for Customized Buttons

Changing the name of a default button does not change the name of its callback macro. For instance, the renamed :back and :finish buttons above (to ‘Previous Page’ and ‘Save and Return’, respectively) still use the on_back() and on_finish() callback macros despite the name change.

Perhaps though you want to change the symbol used to represent the button for consistancy across your MVC. You can use the :id option when renaming to do so.

class UserSignupController < ApplicationController
  act_wizardly_for :user, :redirect=>'/main/index' do
    change_button(:back).name_to('Previous Page', :id=>:previous_page)
    change_button(:finish).name_to('Save and Return', :id=>save_and_return)
  end
end

Coding the above causes the :back button’s ID to be replaced with :previous_page and so forth for the :finish button. Thereafter, each button is referred to with the new ID. For instance, the corresponding callback macros would be

on_previous_page(:step3) do
  ...
end
on_save_and_return(:step3) do
  ...
end

Creating New Buttons

Completely new buttons can be added to the wizard by passing a button name to the create_button method in the act_wizardly_for code block

act_wizardly_for :user, :redirect=>'/main/index' do
  change_button(:back).name_to('Previous Page')
  change_button(:finish).name_to('Save and Return')
  ...
  create_button('Help')
end

This creates a new button names ‘Help’ represented by the :help symbol. Actions for this button must be defined by the on_help() callback macros for each and every page.

on_help(:all) do
  case @step
  when :step1 then ...
  when :step2 then ...
  end
end

Sometimes you may want to use the default ID given to the button. You can specify the ID with the :id option. This is particularly useful for international languages.

create_button('Helfen', :id=>:help)

Now the help button will be called ‘Helfen’ in German and will be represented by :help in the code.

Setting Buttons for a Wizard Page

Any new button needs to be explicitly added to every page it will show up on. Each pages button set can be set in the act_wizardly_for code as follows

act_wizardly_for :user, :redirect=>'/main/index' do
  change_button(:back).name_to('Previous Page')
  change_button(:finish).name_to('Save and Return')
  ...
  create_button('Help')
  ...
  set_page(:step1).buttons_to :next, :cancel, :help
  set_page(:step2).buttons_to :next, :previous_page, :cancel, :help
  set_page(:step3).buttons_to :save_and_return, :previous_page, :cancel, :help
end

Viewing the Configuration

Use the wizardly rake tools to view the configuration of any wizard changes you make.

./script/generate wizardly_app  # if not already called for the project
rake wizardly:config name=controller_name

See the section ‘Inspecting a Wizard Controller’ above.

Testing

Testing uses RSpec and Webrat. Make sure you have the gems installed. Then to test the development code run the following:

rake spec:all

Dependencies

  • validation_group is currently integrated (plugin not required)

  • ActiveRecord

  • ActionController