Extending an AngularJS Workflow

A workflow extends the extensibleService. This means that all workflows inherit properties and methods provided by the extensibleService. Extending a workflow allows you to add your own steps, remove existing steps, and inject custom data handling logic. Refer to inline documentation on what those properties and methods are.

We highly recommend that you complete the Tutorial: Creating an Horizon Plugin if you have not done so already. If you do not know how to package and install a plugin, the rest of this tutorial will not make sense! In this tutorial, we will examine an existing workflow and how we can extend it as a plugin.

Note

Although this tutorial focuses on extending a workflow, the steps here can easily be adapted to extend any service that inherited the extensibleService. Examples of other extensible points include table columns and table actions.

File Structure

Remember that the goal of this tutorial is to inject our custom step into an existing workflow. All of the files we are interested in reside in the static folder.

myplugin
│
├── enabled
│   └── _31000_myplugin.py
│
└── static
     └── horizon
         └── app
             └── core
                 └── images
                     ├── plugins
                     │   └── myplugin.module.js
                     │
                     └── steps
                         └── mystep
                             ├── mystep.controller.js
                             ├── mystep.help.html
                             └── mystep.html

myplugin.module.js

This is the entry point into our plugin. We hook into an existing module via the run block which is executed after the module has been initialized. All we need to do is inject it as a dependency and then use the methods provided in the extensible service to override or modify steps. In this example, we are going to prepend our custom step so that it will show up as the first step in the wizard.

(function () {
  'use strict';

  angular
    .module('horizon.app.core.images')
    .run(myPlugin);

  myPlugin.$inject = [
    'horizon.app.core.images.basePath',
    'horizon.app.core.images.workflows.create-volume.service'
  ];

  function myPlugin(basePath, workflow) {
    var customStep = {
      id: 'mypluginstep',
      title: gettext('My Step'),
      templateUrl: basePath + 'steps/mystep/mystep.html',
      helpUrl: basePath + 'steps/mystep/mystep.help.html',
      formName: 'myStepForm'
    };
    workflow.prepend(customStep);
  }

})();

Note

Replace horizon.app.core.images.workflows.create-volume.service with the workflow you intend to augment.

mystep.controller.js

It is important to note that the scope is the glue between our controllers, this is how we are propagating events from one controller to another. We can propagate events upward using the $emit method and propagate events downward using the $broadcast method.

Using the $on method, we can listen to events generated within the scope. In this manner, actions we completed in the wizard are visually reflected in the table even though they are two completely different widgets. Similarly, you can share data between steps in your workflow as long as they share the same parent scope.

In this example, we are listening for events generated by the wizard and the user panel. We also emit a custom event that other controllers can register to when favorite color changes.

(function() {
  'use strict';

  angular
    .module('horizon.app.core.images')
    .controller('horizon.app.core.images.steps.myStepController',
      myStepController);

  myStepController.$inject = [
    '$scope',
    'horizon.framework.widgets.wizard.events',
    'horizon.app.core.images.events'
  ];

  function myStepController($scope, wizardEvents, imageEvents) {

    var ctrl = this;
    ctrl.favoriteColor = 'red';

    ///////////////////////////

    $scope.$on(wizardEvents.ON_SWITCH, function(e, args) {
      console.info('Wizard is switching step!');
      console.info(args);
    });

    $scope.$on(wizardEvents.BEFORE_SUBMIT, function() {
      console.info('About to submit!');
    });

    $scope.$on(imageEvents.VOLUME_CHANGED, function(event, newVolume) {
      console.info(newVolume);
    });

    ///////////////////////////

    $scope.$watchCollection(getFavoriteColor, watchFavoriteColor);

    function getFavoriteColor() {
      return ctrl.favoriteColor;
    }

    function watchFavoriteColor(newColor, oldColor) {
      if (newColor != oldColor) {
        $scope.$emit('mystep.favoriteColor', newColor);
      }
    }
  }

})();

mystep.help.html

In this tutorial, we will leave this file blank. Include additional information here if your step requires it. Otherwise, remove the file and the helpUrl property from your step.

mystep.html

This file contains contents you want to display to the user. We will provide a simple example of a step that asks for your favorite color. The most important thing to note here is the reference to our controller via the ng-controller directive. This is essentially the link to our controller.

<div ng-controller="horizon.app.core.images.steps.myStepController as ctrl">
  <h1 translate>Blue Plugin</h1>
  <div class="content">
    <div class="subtitle" translate>My custom step</div>
    <div translate style="margin-bottom:1em;">
      Place your custom content here!
    </div>
    <div class="selected-source clearfix">
      <div class="row">
        <div class="col-xs-12 col-sm-8">
          <div class="form-group required">
            <label class="control-label" translate>Favorite color</label>
            <input type="text" class="form-control"
              ng-model="ctrl.favoriteColor"
              placeholder="{$ 'Enter your favorite color'|translate $}">
          </div>
        </div>
      </div><!-- row -->
    </div><!-- clearfix -->
  </div><!-- content -->
</div><!-- controller -->

Testing

Now that we have completed our plugin, lets package it and test that it works. If you need a refresher, take a look at the installation section in Tutorial: Creating an Horizon Plugin.