hmpo-form-wizard
v15.0.0
Published
Routing and request handling for a multi-step form processes
Downloads
3,178
Keywords
Readme
hmpo-form-wizard
Creates routing and request handling for a multi-step form process.
Given a set of form steps and field definitions, the wizard function will create an express router with routing bound to each step of the form and input validation applied as configured.
Additional checks are also applied to ensure a user completes the form in the correct order.
Usage
Define a set of steps:
// steps.js
module.exports = {
'/step1': {
next: 'step2'
},
'/step2': {
fields: ['name'],
next: 'step3'
},
'/step3': {
fields: ['age'],
next: [
{ field: 'age', op: '<', value: 18, next: 'not-old-enough' },
'step4'
]
},
'/step4': {},
'/not-old-enough': {}
}
Define field rules:
// fields.js
module.exports = {
'name': {
validate: 'required'
},
'age': {
validate: 'required'
}
}
Create a wizard and bind it as middleware to an app:
const wizard = require('hmpo-form-wizard');
const steps = require('./steps');
const fields = require('./fields');
app.use(wizard(steps, fields, { name: 'my-wizard' }));
Sessions
The wizard expects some kind of session to have been created in previous middleware layers.
For production use a database backed session store is recommended - such as connect-redis.
The wizard stores values and state in a model synchronised to the session. This is made available as req.sessionModel
. This provides get()
, set()
, unset()
, toJSON()
, and reset()
methods.
The wizard shares journey step history with other wizards through a journey model on the session. The is exposed as req.journeyModel
. The history is available as req.journeyModel.get('history')
.
Error handling
The app should provide error middleware that redirects to the location specified by the redirect
property of the error. This is to allow any error to be intercepted before redirection occurs.
app.use((error, req, res, next) => {
if (error.redirect) return res.redirect(error.redirect);
next(error);
});
Additional step options
The minimum amount of configuration for a wizard step is the next
property to determine where the user should be taken after completing a step. A number of additional properties can be defined.
Any of these options can also be provided as a third argument to the wizard to customise aspects of the behaviour for all steps.
name
- A namespace identifier for the wizard. This is used to store wizard data on the session. This defaults to a unique value for a wizard.journeyName
- A namespace identifier for the entire journey. This is used to store journey-wide data such as step history on the session. Defaults todefault
.entryPoint
- Allows a user to navigate to this step with no journey step history. Defaults tofalse
.checkSession
- Check if the session has expired. Defaults totrue
.checkEntryPointSession
= Check if session has expired on entry points. Defaults tofalse
checkJourney
- Check this step is allowed based on the journey step history. If this step is not allowed the user is redirected to the last allowed step, or an error is returned if no step is allowed. Defaults totrue
.reset
- Reset the wizardsessionModel
when this step is accessed. Defaults tofalse
.resetJourney
- Reset the journeyjourneyModel
when this step is accessed.skip
- A template is not rendered on a GET request. Thepost()
lifecycle is called instead. Defaults tofalse
.noPost
- Don't allow posting to this step. The post method is set to null and the step is completed if there is a next stepforwardQuery
- forward the query params when internally redirecting. Defaults tofalse
.editable
- This step is editable. This allows accessing this step with theeditSuffix
and sets the back link and next step to theeditBackStep
. Defaults tofalse
.editSuffix
- Suffix to use for editing steps. Defaults to/edit
.editBackStep
- Location to return to after editing a step. Defaults toconfirm
continueOnEdit
- While editing, if the step marked with this is evaluated to be the next step, continue to editing it instead of returning toeditBackStep
. Defaults tofalse
.fields
- specifies which of the fields from the field definition list are applied to this step. Form inputs which are not named on this list will not be processed. Default:[]
template
- Specifies the template to render for GET requests to this step. Defaults to the route (without trailing slash)templatePath
- provides the location withinapp.get('views')
that templates are stored.backLink
- Specifies the location of the step previous to this one.backLinks
- Specifies valid referrers that can be used as a back link. If this orbackLink
are not specified then an algorithm is applied which checks the previously visited steps which have the current step set asnext
.controller
- The constructor for the controller to be used for this step's request handling. The default is exported as aController
property of this module. If custom behaviour is required for a particular form step then custom extensions can be defined - see Custom ControllersdecisionFields
- Additional fields that we be recorded as being part of this step's routing decision. Default:[]
revalidate
- Show this page instead of only recalculating the routing if this page is marked invalid. Default:false
revalidateIf
- Show this page instead of only recalculating the routing if one of these values is changed. Default:[]
translate
- provide a function for translating validation error codes into usable messages. Previous implementations have used i18next to do translations.params
- Define express parameters for the route for supporting additional URL parameters.
Remaining field options documentation can be found in the hmpo-template-mixins README.
Field options
See hmpo-template-mixins or hmpo-components for additional field options.
journeyKey
- Name of the cross-wizard field storage namedefault
- Default value for this fieldmultiple
- Allow multiple incomming values for a field. The result is presented as an arrayformater
- Array of formatter names for this field in addition to the default formatter set, or formatter objectstype
- Formatter namefn
- Formatter functionarguments
- Array of formatter arguments, eg.{ type: 'truncate', arguments: [24] }
ignore-defaults
- Disabled the default set of formatters for this fieldvalidate
- An array of validator names, or validator objectstype
- Validator namefn
- Validator functionarguments
- Array of validator arguments, eg.{ type: 'minlength', arguments: [24] }
items
- Array of select box or radio button optionsvalue
- Item value
dependent
- Name of field to make this field conditional upon. This field will not be validated or stored if this condition is not met. Can also also be an object to specify a specific value instead of the default oftrue
:field
- Field namevalue
- Field value
invalidates
- an array of field names that will be 'invalidated' when this field value is set or changed. Any fields specified in theinvalidates
array will be removed from thesessionModel
. Future steps that have used this value to make a branching decision will also be invalidated, making the user go through those steps and decisions again.contentKey
- localisation key to use for this field instead of the field name
Central journey storage
To facilitate sharing form values between wizards in the same journey a field can be specified to save into the journeyModel
instead of the sessionModel
using the journeyKey
property:
// fields.js
module.exports = {
'localFieldName': {
journeyKey: 'centralfieldName',
}
}
Default field values
A default value for a field can be specified with the default
property. This is used if the value loaded from the session is missing or undefined.
// fields.js
module.exports = {
'localFieldName': {
default: 'defaultValue'
}
}
Next steps
The next step for each step can be a relative path, an external URL, or an array of conditional next steps. Each condition next step can contain a next location, a field name, operator and value, or a custom condition function:
'/step1': {
// next can be a relative string path
next: 'step2'
},
'/step2': {
// next can be an array of conditions
next: [
// field, op and value. op defaults to '==='
{ field: 'field1', op: '===', value: 'foobar', next: 'conditional-next' },
// an operator can be a function
{ field: 'field1', op: (fieldValue, req, res, con) => fieldValue === con.value, value: true, next: 'next-step' },
// next can be an array of conditions
{ field: 'field1', value: 'boobaz', next: [
{ field: 'field2', op: '=', value: 'foobar', next: 'sub-condition-next' },
'sub-condition-default-next'
] },
// a condition can be a function specified by fn
{ fn: (req, res, con) => true, next: 'functional-condition' },
// a condition can be a controller method
{ fn: Controller.prototype.conditionMethod, next: 'functional-condition' },
// a condition can be a controller method specified by name
{ fn: 'conditionMethod', next: 'functional-condition' },
// the next option can be a function to return a dynamic next step
{ field: 'field1', value: true, next: (req, res, con) => 'functional-next' },
// use a string as a default next step
'default-next'
]
}
Custom Controllers
Creating a custom controller:
// controller.js
const Controller = require('hmpo-form-wizard').Controller;
class CustomController extends Controller {
/* Custom middleware */
middlewareSetup() {
super.middlewareSetup();
this.use((req, res, next) => {
console.log(req.method, req.url);
next();
});
}
/* Overridden locals lifecycle */
locals(req, res, callback) {
let locals = super.locals(req, res (err, locals) => {
locals.newLocal = 'value';
callback(null, locals);
});
}
}
module.exports = CustomController
Examples of custom controllers can be found in the example app
Controller lifecycle
These controllers can be overridden in a custom controller to provide additional behaviour to a standard controller.
This diagram shows the interaction and sequence of these lifecycle events.
GET lifecycle
-
configure(req, res, next)
Allows changing of the
req.form.options
controller options for this request.- Middleware mixins are run.
-
get(req, res, next)
-
errors = getErrors(req, res)
Returns an
Object
ofController.Error
validation errors indexed by the field name.-
getValues(req, res, callback(err, values))
Calls
callback
with an error andObject
of field names and values. The values will include user-entered values for the current step if validation fails.-
locals(req, res, callback(err, locals))
Calls
callback
with error andObject
of locals to be used in the rendered template.-
render(req, res, next)
Renders the template to the user.
POST lifecycle
-
configure(req, res, next)
Allows changing of the
req.form.options
controller options for this request.- Middleware mixins are run.
-
post(req, res, next)
-
process(req, res, next)
Allows for processing the
req.form.values
before validation.-
validateFields(req, res, callback)
Validates each field and calls
callback
with anObject
of validation errors indexed by field name.-
validate(req, res, next)
Allows for additional validation of the
req.form.values
after the built-in field validation.-
saveValues(req, res, next)
Saves the values to the session model.
-
successHandler(req, res, next)
Saves the step into the step history and redirects to the next step.
Error handling
-
errorHandler(err, req, res, next)
Additional error handling can be performed by overriding the
errorHandler
.
Example app
An example application can be found in the ./example directory. To run this, follow the instructions in the README.
Session Injection
A helper is provided to aid with session injection:
const SessionInjection = require('hmpo-form-wizard').SessionInjection;
app.use('/debug/session', new SessionInjection().middleware());
This endpoint /debug/session
can take a POST of JSON or url encoded data in the format:
{
"journeyName": "name",
"journeyKeys": {
"key": "name"
},
"allowedStep": "/full/path",
"prereqStep": "/full/path",
"featureFlags": {
"flag": true
},
"wizards": {
"wizardName": {
"wizardKey": "value"
}
},
"rawSessionValues": {
"sessionKey": "value"
}
}
A GET to this endpoint will render a web form that can submit this JSON.
Migrating to wizard v6
- The code has been updated to es6 and requires a minimum of Node v4
- If additional middleware has been added by overriding the
constructor
orrequestHandler
method, this will need to be moved to one of the middleware setup methods (middlewareSetup
,middlewareChecks
,middlewareActions
, ormiddlewareLocals
) (see the Custom Controller example above) - Custom controllers must be specified using an es6
class
statement, and not a function. - When testing custom controllers the mimimum options that need to be supplied is
route
. backLink
andbackLinks
must now be paths relative to the wizard base URL, or full HTTP URLs.- forks are now unsupported.
Migrating to wizard v7
- Step history has been moved from a
step
array in thesessionModel
to a structuredhistory
array in thejourneyModel
. - Journey history checking has become stricter. A step will only be allowed if it is an
entryPoint
, it isnext
from an existing step, or aprereq
is in history. History checking can be disabled with thecheckJourney
option set to false. - Steps are completed when they are successfully posted to. If your step only has links, set the
noPost
option for it to be set as completed when rendered. - A
skip
option has been added that will run thepost()
lifecycle methods instead of rendering a template. - A
reset
option has been added that will reset the wizardsessionModel
. - A
resetJourney
option has been added that will reset thejourneyModel
step history. - If a step isn't allowed and the step history is empty, a
MISSING_PREREQ
error will be thrown that must be dealt with. Previously the user was sent back to a 'first' step of the current wizard. - Backlinks will automatically populate between wizards on the same journey.
next
links and error redirects are now relative to thebaseUrl
.- Branching is now supported by
next
. See the Example app for details. - The app should provide error middleware that redirects to the location specified by the
redirect
property of the error. This is to allow any error to be intercepted before redirection occurs.
Migrating to wizard v8
- Options are deep cloned to
req.form.options
on every request. These can be mutated by overriding theconfigure(req, res, next)
method. Tests may need to be updated to make surereq.form.options
is set to the same object as the controller options when not running the whole request lifecycle. - The
noPost
option will now set the step as complete if therender
method is overridden. Previously this was done byrender
.
Migrating to wizard v9
- The
hmpo-form-controller
has been merged into the wizard's controller. - Dependent fields that are hidden are not set to their formatted defaults in
_process
instead of as part of_validation
- The interface to the validation library has changed.
- The
locals()
lifecycle event is now called asynchronously if a callback is supplied:locals(req, res, callback(err, locals))
. The method can still be overwridden synchonrously by only providing a method aslocals(req, res)
.
Migrating to wizard v11
- Hogan has been removed from the wizard. Error messages are no longer localised and templated by the wizard at validation time. An updated
passports-template-mixins
module is reqiured to translate and format the error messages for both the inline errors and error summary at render time.