Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Specify request sequences in Rails

branch: master
README.rdoc

ActionFlow

This Rails plugin provides a simple way to model multi-step processes such as wizards, sign-up flows, checkout processes and the like. It allows sequences to be easily composed and reordered, as well as letting you alter the behaviour and appearance of pages based on which flow they have been accessed through.

Installation

script/plugin install git://github.com/jcoglan/action_flow.git

class ApplicationController < ActionController::Base
  include ActionFlow::Filters
end

module ApplicationHelper
  include ActionFlow::Helpers
end

Specifying flows

Setup is done using a configure block which you can place anywhere is your application, though I'd recomment config/environment.rb or app/controllers/application.rb. The block is a list of flows, where each flow has a name and a list of controller actions. For example let's create a flow for importing contacts from Gmail. The flow allows you to import contacts, invite people to your site, and add people as friends.

ActionFlow.configure do
  flow :import_contacts, contacts.import,
                         contacts.invite,
                         users.add_friends,
                         contacts.done
end

This tells ActionFlow that the import_contacts flow is the sequence of actions ContactsController#import, ContactsController#invite, UsersController#add_friends and ContactsController#done. On each request, ActionFlow will watch the user's session and monitor which flows they are in and how far through each flow they've progressed.

(The request-matching expressions contacts.import etc use the Consent request expression language. More information here: github.com/jcoglan/consent)

A user is judged to have begun a flow when they hit the first action in that flow. They exit the flow when they've hit every page in the flow in sequence. Multiple flows may be in play at once, and requests that don't match any of the current flows are simply ignored: they do not cause a flow to exit.

Using flows to drive your app

ActionFlow provides a few methods to controllers and views handle sending the user to the next step in the flow and detecting which flow they are in.

next_in_flow(name) returns a params hash for the next action in the named flow, based on the user's session history. If ActionFlow cannot figure out the next action automatically, this method returns nil.

In views it simply returns the params hash for making links:

<%= link_to "Skip this step", next_in_flow(:import_contacts) %>

in_flow?(name) returns true iff the user is in the named flow, allowing you to alter the behaviour of controllers or the appearance of views based on which flow(s) the user is currently in. This is especially useful when reusing flows through composition, as described below.

Composing flows

Flows can be spliced together, allowing you to embed one sequence of actions inside another. Say we have a sign-up flow and we want to embed our contact importer from above within it.

ActionFlow.configure do
  flow :import_contacts, contacts.import,
                         contacts.invite,
                         users.add_friends,
                         contacts.done

  flow :signup,          users.new,
                         post(users.create),
                         :import_contacts,
                         dashboard.show
end

The signup flow begins with a request to UsersController#new, followed by a POST request to UsersController#create. The symbol :import_contacts that follows tells ActionFlow to direct users to the start of the import_contacts flow after UsersController#create, whose implementation might look like this:

class UsersController < ApplicationController
  def create
    @user = User.new(params[:user])
    return redirect_to next_in_flow(:signup) if @user.save
  end
end

If the new user is valid, we go to the next step, otherwise we fall through and render views/users/create.html.erb for them to correct the errors.

While in the import_contacts flow, ActionFlow will keep track of both flows that are in play, allowing the user to skip ahead within each flow. In your views, you can make use of this as follows:

# Links to ContactsController#invite
<%= link_to "Skip", next_in_flow(:import_contacts) %>

# Links to DashboardController#show
<%= link_to "Skip", next_in_flow(:signup) %>

Flow conditions

Flow conditions give you finer control over whether a request should progress a flow. You can attach a block of arbitrary Ruby code to request expressions to provide more matching control. For example you could only allow a certain flow to be entered if the first request is not an Ajax request:

ActionFlow.configure do
  flow :signup, users.new { not request.xhr? },
                users.create,
                users.message
end

Mutually exclusive flows

Sometimes you want two more flows to be mutually exclusive, for example if you're trying a few different orderings of a process in different parts of your application. The mutex command takes a list of flow names and makes sure that only one flow in that list can be active at once.

ActionFlow.configure do
  flow :signup, users.new,
                users.create,
                users.message

  flow :alternate_signup, users.message,
                          users.new,
                          users.create

  mutex :signup, :alternate_signup
end

This makes it easier to reason about which step ActionFlow will pick next since you know only one of these flows will apply at any time.

Flow variables

Each flow a user is in gets its own session-like variable store, which allows data to be passed forward through a flow without having to store them as passthrough parameters in URLs. For example, say that I want to modify my app so that we go to /users/:username/profile at the end of the signup flow. We don't know the value of :username in advance, as it's determined during signup. But we can get ActionFlow to redirect to the correct place using the find wrapper within our config and storing the needed data in the flow's “session”.

ActionFlow.configure do
  flow :signup,          users.new,
                         post(users.create),
                         profiles.show(:user_id => find(:username))
end

class UsersController
  def create
    @user = User.new(params[:user])
    if @user.save
      flow[:username] = @user.username
      redirect_to next_in_flow(:signup)
    end
  end
end

Setting flow[:username] gives ActionFlow the information it needs to fill in the details for the next request: assuming I signed up with the username “jcoglan”, here next_in_flow redirects to {:controller => "profiles", :action => "show", :user_id => "jcoglan"}.

License

Copyright © 2009 James Coglan, released under the MIT license

Something went wrong with that request. Please try again.