DataShift Journey
A Rails software Wizard
Quickly create a sequence of forms (dialogs) that lead a visitor through a series of defined steps - ideal for questionnaires, application forms, checkouts, configurations, surveys, registration processes etc.
Provides a simple DSL to quickly define a multi page journey through your site, including complex branching, and rejoining, dependent on collected values.
State is maintained in one of the backends, with different storage models being provided out of the box, or use your own model structure.
Full server-side processing can be delayed until the submission of the final form.
The DSL provides a simplified layer on top of a State Machine, with the main underlying gems being :
- https://github.com/state-machines/state_machines
- https://github.com/state-machines/state_machines-activerecord
- https://github.com/apotonick/reform
Getting started
This is a Rails engine so simply add this line to your application's Gemfile:
gem 'datashift_journey', git: 'https://github.com/autotelik/datashift_journey'
And then execute:
$ bundle install
DatashiftJourney needs a model on which to store the Wizard or Journey Plan through a state machine definition.
See below if you already have model you wish to decorate.
If you're starting from scratch, a generator - rails generate datashift_journey:setup
- is provided to setup everything for you.
For example, to create a Checkout model, that will collect the data entered during a checkout journey, such as confirm order, billing address, shipping address and payment data.
rails generate datashift_journey:setup Checkout
This will generate a number of files, including a model file, migration to create the model table with a single column called state
If using an existing model, it's vital that this journey class has a string column called
state
i.e If you need to add an associated migration yourself it should containt.string :state
In this example this model would be located at : app/models/checkout.rb
A stub for entering the journey plan is added to the model.
You should edit this model and configure your required steps in the plan.
Details of the API are supplied in comments in the file and TODO:
An initializer will also be created at : config/initializers/datashift_journey.rb
Inside the initializer you can change which model to use as the plan and set various configuration options.
Defining the Journey Plan
A skeleton journey definition is added as a comment seciton to the plan model.
In here you defines the steps of the apps journey and set the initial step.
Here's a simple example for a basic checkout, on an ActiveRecord model, Checkout
class Checkout < ApplicationRecord
DatashiftJourney::Journey::MachineBuilder.create_journey_plan(initial: :ship_address) do
# Two simple sequential steps
sequence [:ship_address, :bill_address]
# At the next step, we will have a branch so first define the branch nodes - they also can
# be sequences of multiple steps, a single step, or nothing (skip straight to branch recombination step)
branch_sequence :visa_sequence, [:visa_page1]
branch_sequence :mastercard_sequence, [:page_mastercard1, :page_mastercard2]
branch_sequence :paypal_sequence, []
# Define the next state (after :bill_address, and parent state of the branch)
# and the routing criteria to each sequence
# So after bill address we reach payment - then we split to a single step, depending on the card type entered
split_on_equality( :payment,
"payment_card", # Helper method on Checkout that returns card type from Payment
visa_sequence: 'visa',
mastercard_sequence: 'mastercard',
paypal_sequence: 'paypal'
)
# All different card type branches, recombine here at review
sequence [:review, :complete ]
end
....
A state machine will be generated with all steps starting at :ship_address and finishing at :complete, and forward and backwards navigation between them.
A backing Reform style Form and associated view partial, will be expected for each state.
View Forms
The default controllers expect the Forms pattern to back each view.
DSJ includes the Reform gem and utilises their Form - see - https://github.com/trailblazer/reform
These tend to do the work traditionally performed in the Controller, so our controller can stay generic and focused on navigation.
Forms give you the flexability to implement your own strategies for dealing with the presentation data required for a view, managing params, validating entreed values, and saving the data entered into forms.
A generator is provided that can create skeleton Forms for you, one per state(page).
Options
[--base-class=ClassName] # Class to use as the Base class for generated Forms
rails generate datashift_journey:forms
Generated Forms derive from datashift_journey/app/forms/datashift_journey/base_form.rb
And ultimately from Reform::Form
Views
Each step will need a view, usually a form to collect information from, but can be a static page or any content you like really.
A generator is provided that can create a starter set of partial views for you, one per state(page).
So, once the journey plan has been fully defined run :
rails generate datashift_journey:views
The Controller will expect a view partial, for each related Form.
The partials are rendered passing in the Form as a local variable.
The location of the partial to use for a certain state is given by helper
def journey_plan_partial_location( state )
The default is app/views
but path can be changed using Configuration option partial_location
This will be required in the path format, if you are using multiple namespaces/folders
DatashiftJourney::Configuration.configure do |config|
config.partial_location = "checkout_engine"
end
Data Collection
Ultimately the views and forms are there to collect data from a User, validate and store it.
Generate and use associated backing Reform forms to validate and store data, collected from your visitors.
The Reform form expects to be backed by a model, and can write data back to the model via sync and save methods, enabling you to populate the data however you choose within your Forms - see section 'Custom Data Collector'
Alternatively a generic SQL based data collector is provided for use with the generic generated Forms to save the data.
Data Collector
This setup will add a migration, creating a number of tables to manage the collection of data, on a form by form basis, stored as a series of nodes, essentially keyed on the form class and name (state).
The journey plan class is decorated with an association the the data nodes, so each instance of the journey holds all the data for that joureny, as a collection of fields (name/value) i.e one database row per question/answer.
To use this concern as your main data collection agent, simply run the installer
rails generate datashift_journey:collector
This will create relevant migrations and decorate your journey plan class with data collection attributes.
To access manually, derive your form from DatashiftJourney::Collector::BaseCollectorForm
.
Mongo Data Collector
TODO
An optional MongoDB based Collector is under development, which will collect data in a single document per journey.
To use this model as your main data collection class, simply run the installer to setup the model.
rails generate datashift_journey:install_mongo_collector
Journeys End
The controller should identify when the last state has been submitted and there are no further states to be rendered.
After processing the last state, the controller will redirect to the JourneyEndsController and its default new view will be rendered.
To provide your own end page, in your app simply override this view - app/views/datashift_journey/journey_ends/new.html.erb
You can also implement an on_journey_end
hook, in your JourneyPlan class, which will be called by the controller
(if implemented) before the view is rendered.
For example, you can spin off jobs that parse and process the data
class Checkout < ApplicationRecord
def on_journey_end
Apoc::CreateOrderWorker.perform_async(self.id)
end
Internals
The Classified version of the state name, plus "Form", so a
:billing_address
state should have an associated form calledBillingAddressForm
The DSJ Controller will search for a matching Form for each state using the Factory class/method
OUT OF DATE
DatashiftState::FormObjectFactory.form_object_for(journey_plan)
In code, the expected form Class name is defined as :
"#{modules}::#{journey_plan.state.classify}Form"
When using namespaces the module structure , can be set via the DSJ Configuration object, which can be set, using a standard block format, in an initializer, as so:
DatashiftJourney::Configuration.configure do |config|
config.forms_module_name = 'MyCheckoutEngine'
end
So given a module name configuration setting of
MyCheckoutEngine::States
And a current state of :address - then the Controller will attempt to use Form class
MyCheckoutEngine::States::AddressForm
END OUT OF DATE
Null Forms
When no form is required for a specific HTML page, you an specify that NullForm is to be used, either globally for ALL missing forms, or for specific named forms.
The null form means no params are validated, and no save performed, for example for a mid sequence, text only helper page, or for a text only branch terminating page.
To set globally
DatashiftJourney::Configuration.configure do |config|
config.use_null_form_when_no_form = true
end
To set for individual states, with no data collection requirements, add to list of null_form
states
DatashiftJourney::Configuration.configure do |config|
config.null_form_list = [:confirm_page_with_no_data, :brexit]
end
DatashiftJourney::Collector
This option uses the journey plan class itself to store the data collected from each state/page in data_nodes
This collection is a series of DatashiftJourney::Collector::DataNode
objects.
The generator will geneate a series of forms that inherit from DatashiftJourney::Collector::BaseCollectorForm
and your state form becomes very simple, for example
class ResourcesForm < ::BaseForm
journey_plan_form_field name: :namespace, category: :string
journey_plan_form_field name: :number_of_cpu, category: :number
journey_plan_form_field name: :memory, category: :number
end
This will create field definitions, which can then be rendered automatically using the default views, and on submit, the data entered will be saved to the DB as a DataNode, one per field name
DataNode: {
"id":8,
"form_field_id": 1,
"field_value": "My New Namespace"
}
Routes
The generator will add the engines routes to your app's config/routes.rb
file.
You can manually change the mount point to whatever suits your application.
Rails.application.routes.draw do
mount DatashiftJourney::Engine => "/"
end
If youd like to set your apps root to be the initial state, you can manually add the following :
Rails.application.routes.draw do
root to: "datashift_journey/journey_plans#new"
end
State Jumper Toolbar
There is breadcrumb style toolbar available for creating and jumping straight to any State
This must be activated by setting
config.add_state_jumper_toolbar = true
In development, so that any data required for previous states can be created, it supports passing in a Factory that creates that data for you.
The factory should return an instance of your DatashiftJourney.journey_plan_class
Configure your list of required 'jump to' states and factories - where no factory required simply pass nil -
by setting state_jumper_states
, for example
config.state_jumper_states = {contact: my_contact_factory, ship_address: nil, :bill_address: nil}
The view is added to a content_for block called :datashift_journey_state_jumper so you can add this somewhere in your layout.
To pull in some default styling add following to your application.css.scss
@import 'datashift_journey/partials/state_jumper_toolbar';
License
Author :: Tom Statter
Date :: April 2016
The MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.