Flow Controllers
Contents |
The goal of flow controllers is to contain the flow logic and keep it separated from the rest of the application so that it can be managed and customized independently and more easily. The flow controller is in charge of managing the flow, which means the following:
- maintaining in the data model the current state of the flow
- intercepting calls to the module controller to update that state
- receiving navigation requests from templates
- and raising events so that templates can be refreshed to reflect the current state of the flow
For each module controller instance, there is an associated flow controller instance (if a flow controller is present). The following schema describes the relation between a module controller, its flow controller and templates:
Similarities with module controllers
Flow controllers are very similar to module controllers:
- a flow controller is a class which extends
aria.templates.FlowCtrl, which itself extendsaria.templates.PublicWrapper - a flow controller has a public interface (the interface is created and attached to the flow controller in the same way as for module controllers with the difference that flow controller interfaces must inherit
aria.templates.IFlowCtrl(currently empty) and notaria.templates.IModuleCtrl) - a flow controller is created and destroyed at the same time as its associated module controller
- templates can access the public interface wrapper of the associated flow controller through the
flowCtrlvariable (similar to themoduleCtrlvariable) - template scripts can receive events from the flow controller by implementing the
onFlowEventmethod (similar to theonModuleEvent)
Associating a flow controller with a module controller
Currently, there is a naming convention to find the flow controller associated to a given module controller: the flow controller classpath is the classpath of the module controller with the "Flow" suffix added. The framework knows that there is an associated flow controller if the $hasFlowCtrl variable is set to true in the prototype of the module controller. Then, when the module controller is created (either through Aria.loadTemplate or this.loadSubModules in a parent module, or directly through the module controller factory aria.templates.ModuleCtrlFactory), the flow controller is automatically created as well.
Remember that flow controllers can be customized (this feature will be provided in a future release of Aria Templates), so that the flow controller associated to a module controller can actually be different from the one with the "Flow" suffix added. There should not be any assumption made on which flow controller is associated to a given module controller (anyway, the module controller does not have any reference to the flow controller).
If a special flow controller has to be used, the $hasFlowCtrl property can be set directly to the flow controller classpath:
File /modules/flow_controllers/MyModuleController.js
$classpath: 'modules.mymodule.MyModuleController',
$extends: 'aria.templates.ModuleCtrl',
$implements: ['modules.mymodule.IMyModuleController'],
$constructor: function () {
if ( mySpecialCondition ) {
this.$hasFlowCtrl = 'modules.mymodule.MySuperSpecialClasspath';
}
this.$ModuleCtrl.constructor.call(this);
},
$prototype: {
$publicInterfaceName: 'modules.mymodule.IMyModuleController',
/**
* Flow controller Classpath, or true if default is to be used.
* @type Boolean|String
*/
$hasFlowCtrl : 'modules.mymodule.MySpecialClasspath'
}
Intercepting module controller calls
A flow controller can intercept calls to its associated module controller, so that each time a template (or the parent module controller) is calling the module controller, the flow controller is notified and can update its internal state, do extra action before, after or on the callback of the call to the module controller, and/or even cancel the call to the module controller.
To intercept calls to the module controller, the following naming convention is used for intercepting methods in flow controllers (MethodName should be replaced by the corresponding module controller method declared in its public interface)
-
on<<MethodName>>CallBegin- called before the corresponding method is called on the module controller. -
on<<MethodName>>CallEnd- called after the corresponding method of the module controller returns. -
on<<MethodName>>Callback- called if the method is asynchronous (as declared in the interface) when the callback is called, before the call of the normal callback. Note that "Callback" can sometimes be called before "CallEnd" if the module controller method calls its callback synchronously.
These methods are automatically called, if present, when the corresponding method is called in the module controller. The parameter passed to these functions is described in the article about interceptors, which also gives more details about the concept of interceptors in Aria Templates.
Example Code
Module controller interface
Let's consider the following public interface of the module controller:
File /modules/flow_controllers/IMyModule.js
$classpath : 'modules.mymodule.IMyModule',
$extends : 'aria.templates.IModuleCtrl',
$interface: {
/**
* Get the flight availability. This function is asynchronous. Its first parameter is
* the callback (called when results are available).
* Arguments will be retrieved from the searchQuery section of the data model.
* Response will be set in one of the availability sections of the datamodel.
* Note: this method will automatically call validateSearchQuery() first.
**/
getFlightAvailability: {
$type: "Function",
$callbackParam: 0
},
/**
* Get the flight information for the flights passed as argument.
* The result will be set in the flightInfo[flightNbr] section of the datamodel
* @param flightNbr {number} the flight number corresponding to the information to retrieve
*/
getFlightInfo: function (flightNbr) {},
/**
* Validates the search query part of the data model and updates errors meta-data that can
* be directly bound to the fields
**/
validateSearchQuery: function() {}
}
});
Flow controller interface
Here is the public interface of the flow controller:
File /modules/flow_controllers/IMyModuleFlow.js
$classpath : 'modules.mymodule.IMyModuleFlow',
$extends : 'aria.templates.IFlowCtrl',
$events : {
"stateChange" : "raised when the current flow state changed",
"emptyFlightAvailReceived" : "raised when an empty flight avail is received"
},
$interface: {
navigate:function(state) {},
back:function() {},
clearHistory:function() {}
}
});
Flow controller implementation
Here is a flow controller implementation. Note: so that the flow controller is actually used, the associated module controller (modules.mymodule.MyModule) must contain $hasFlowCtrl: true on its prototype - like here - or as a property defined in the constructor.
File /modules/flow_controllers/MyModuleFlow.js
$classpath : 'modules.mymodule.MyModuleFlow',
$extends : 'aria.templates.FlowCtrl',
$implements: ['modules.mymodule.IMyModuleFlow'],
$constructor:function() {
this.$FlowCtrl.constructor.call(this);
},
$prototype: {
$publicInterfaceName: 'modules.mymodule.IMyModuleFlow',
// Intercepting method called at the end of the module controller initialization:
oninitCallback : function (param) {
this.$FlowCtrl.oninitCallback.call(this, param); // call the method of the parent which sets this.data
this.flowData={
currentState:"search", // indicates the current page id - enum: 'search', 'avail' or 'info'
transitionStates:{
// tell which transitions/navigations are authorized
search:false, // true when we authorize the navigation to the search screen
// (could be bound to the 'back to search' link enabled property)
avail:false, // true when an avail result is available
info:false, // true when one info is available
back:false // true if we can navigate back
}
};
// the flow data is stored in the data model, to be accessible by templates:
this.data['flow:data'] = this.flowData;
},
// navigate is published in the Flow interface
navigate:function(transition) {
if (transition=='back') {
this.back()
return;
}
var fd=this.flowData;
if (transition==fd.currentState || !fd.transitionStates[transition]) {
// invalid transition
return this.$logError("FLOW_INVALID_TRANSITION",[transition]);
}
// let's assusme we can come back to the current (and soon ex-current) state
fd.transitionStates[fd.currentState]=true;
// change current state to new state
fd.currentState=transition;
// prevent navigation to the current state (nav buttons must be grayed)
fd.transitionStates[fd.currentState]=false;
// raise event to notify templates that current state changed
// TODO: update history stack
this.$raiseEvent("stateChange");
},
back:function() {
if (!fd.transitionStates.back) {
// invalid transition
return this.$logError("FLOW_BACK_NOT_ALLOWED",[state]);
}
// TODO manage history stack
},
clearHistory:function(state) {
// state is optional - if passed, only this state is removed from the history
// TODO implement method
},
// sample of connection of the flow on an 'Callback' event
ongetFlightAvailabilityCallback:function() {
var mcd=this.data; // this.data contains the data model
// we assume here that the flight avail is in 'flights' array
// in an 'avails' map in the module ctrl data model
if (mcd.avails.flights.length==0) {
// this flow assumes that in the case of no result we display
// a warning in a msg box and stay on the current page
// as such we raise an event instead of changing the flow state through navigate()...
this.$raiseEvent("emptyFlightAvailReceived");
} else {
this.navigate("avail");
}
},
ongetFlightInfoCallEnd:function() {
// same as onGetFlightAvailabilityComplete - but the case of no info should be treated as an error
// as the getFlightInfo() method is not a search()
this.navigate("info");
}
}
});

