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.
Contributing to action_flow
-
Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet
-
Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it
-
Fork the project
-
Start a feature/bugfix branch
-
Commit and push until you are happy with your contribution
-
Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.
-
Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
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.
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.
flow :alternate_signup, users.,
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