Top

Flow Controllers

Draft

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:

At doc overview flow controller.png

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 extends aria.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 not aria.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 flowCtrl variable (similar to the moduleCtrl variable)
  • template scripts can receive events from the flow controller by implementing the onFlowEvent method (similar to the onModuleEvent)

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
Aria.classDefinition({

    $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
Aria.interfaceDefinition({

  $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
 Aria.interfaceDefinition({

  $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
 Aria.classDefinition({

  $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");
    }
  }
 });

 
This page was last modified on 19 April 2012, at 15:45 and has been viewed 960 times.