ladenberger

Efficient Stencil Development for Alfresco Process Services

Blog Post created by ladenberger on Feb 4, 2019

One of the most important elements in BPMN and thus also in Alfresco Process Services (APS) is the user task. To facilitate the interaction between business processes and human actors, APS allows process designers to create forms, i.e. user interfaces for user tasks. APS provides a set of predefined UI controls like text input, drop down menu or content of a process variable.

Custom form field types can be created by using custom form stencils. APS includes an easy-to-use form editor for the creation of custom form stencils. While the editor provides a convenient and fast way for creating new form field types for simple and small business cases, developing complex form stencils can be a pain.

 

This blog post presents an approach for the efficient development of custom form stencils. The approach describes faster and easier iterations when changing code for stencils and testing it. At the end, there is no need to publish the app definition, start a new process, start a user tasks and reconstruct the form data in order to see the result of the changes.

 

The overall idea of the approach is to outsource the code for the design (i.e. HTML) and the controller (i.e. AngularJS) of a form field to external files.

 

The Approach

In the following steps we will create a new custom form field for creating notes.

 

Step 1: Create a new set of custom stencils called “DynamicStencils”.

 

Step 2: Create a new form field called “Notes”.

 

Step 3: Add the following HTML snippet to the “Form runtime template” in the form field configuration:


<dynamic-stencil stencil="notes"></dynamic-stencil>
 

The snippet acts like a place holder that is later replaced dynamically by the HTML content defined in an external file.

 

 

Step 4: Save the custom stencils.

 

Step 5: Create a new form (e.g. call it “DynamicStencilForm”) based on the “DynamicStencils” package and add the previously created “Notes” form field to the form:

 

 

Step 6: Create a new process (e.g. call it “DynamicStencilProcess”) with a user task and reference the previously created “DynamicStencilForm”:

 

 

 

Step 7: Create an app that includes the process “DynamicStencilProcess”, save and publish the app.

 

Step 8: Start a process and open the created user task. You see the following form:

  

 

 

What do we see...? Nothing! Don´t worry, this is expected since our form runtime template only defines a placeholder (see step 3). In the next steps, we replace the placeholder with HTML content at runtime and add logic (i.e. an AngularJS controller) to the form field where both the HTML and the AngularJS controller code is located in external files.

 

Step 9: Prepare a script to render form fields based on HTML content located in an external file:

 

Create a new file called dynamicStencil.js with the following content in the folder webapps/activiti-app/scripts located in your activiti-app web application:

 

angular.module('activitiApp')
   .directive('dynamicStencil', function($rootScope, $compile) {

      return {
         template : '<div id="{{stencilName}}DynamicStencil" ng-include="getContentUrl()"></div>',
         link : function(scope, element, attrs) {
            scope.stencilName = attrs.stencil;
            scope.getContentUrl = function() {
               return scope.stencilName;
            }
         }
      };
});

 

This file needs to be created only once.

 

Step 10: Now add the dynamicStencil.js file to the list of additional web resources that are loaded by the browser for an APS application (see also https://docs.alfresco.com/process-services1.9/topics/custom_web_resources.html). Open the file webapps/activiti-app/scripts/app-cfg.js and add the following snippet at the end of the file contents:

 

ACTIVITI.CONFIG.resources = {
'workflow' : [
   {
      'tag' : 'script',
      'type' : 'text/javascript',
      'src' : ACTIVITI.CONFIG.webContextRoot + '/scripts/dynamicStencil.js?v=1.0'
   }
]};

 

Step 11: Define the actual form runtime template: open the file webapps/activiti-app/workflow/index.html located in the activiti-app web application and add the following AngularJS ng-template to the body tag:

 

<!-- Dynamic Stencil Templates -->
<script type="text/ng-template" id="notes">
   <div ng-controller="notesController">
      <div class="input-group" style="width: 100%" ng-show="!(field.type === 'readonly')">
         <textarea class="form-control" rows="3" style="resize: none;" ng-model="currentNote" placeholder="Write some note ..."></textarea>
         <span class="input-group-addon btn btn-primary" ng-click="addNote()">
           <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
         </span>

      </div>
      <div class="list-group">
         <div ng-repeat="note in field.value | reverse track by $index" class="list-group-item list-group-item-action flex-column align-items-start active">
            <div class="d-flex w-100 justify-content-between">
               <small>{{note.fullName}} - {{note.date}}</small>
            </div>
            <p class="mb-1">{{note.content}}</p>
         </div>
      </div>
   </div>
</script>

 

Please note that the id of the ng-template must be the same as defined in the “stencil” attribute in step 3.

 

Go back to your browser and refresh the page. You should now see the rendered HTML defined in the ng-template:

 

  

Great! However, the logic for the form field is still missing.

 

Step 12: Add an AngularJS controller to the form field by creating a new file webapps/activiti-app/workflow/dynamic-stencils/notes-ctrl.js with the following content:

 

function notesController($rootScope, $scope, $http, $filter) {

   var getDatetime = function() {
      return (new Date).toLocaleFormat("%A, %B %e, %Y");
   };

   // Register this controller to listen to the form extensions methods
   $scope.registerCustomFieldListener(this);

   // Deregister on form destroy

   $scope.$on("$destroy", function handleDestroyEvent() {
      $scope.removeCustomFieldListener(this);
   });

   // Parse JSON string content
   if (!$scope.field.value) {
      $scope.field.value = [];
   } else if (typeof $scope.field.value === "string") {
      try {
         $scope.field.value = JSON.parse($scope.field.value);
      } catch (error) {
         $scope.field.value = [];
         console.error(error);
      }
   }

   // Will be triggered before the task is saved
   this.taskBeforeSaved = function(taskId, form, data, scope) {
     // If a note has been entered however the user did not click on the "+" button, save the note anyway
      $scope.addNote();
      // Save the content of the form field as JSON
      data.values.notesfield = angular.toJson($scope.field.value);
   };

   // Will be triggered before the form is completed
   this.formBeforeComplete = function(form, outcome, scope) {
      // If a note has been entered however the user did not click on the "+"
      // button, save the note anyway
      $scope.addNote();
      // Save the content of the form field as JSON
      $scope.field.value = angular.toJson($scope.field.value);
   };

   // Scope function for adding a new note to the scope
   $scope.addNote = function() {
      // Add the note only if it is not undefined and not empty
      if ($scope.currentNote) {
         // Create a new note object
         var newNote = {
            "userID" : $scope.$root.account.id,
            "fullName" : $scope.$root.account.fullname,
            "date" : $filter("date")(new Date(), "dd.MM.yyyy HH:mm"),
            "content" : $scope.currentNote
         };
         // Add the new note to the list of notes
         $scope.field.value.push(newNote);
         // Clear the text field
         $scope.currentNote = "";
      }
   }

};

// Filter that is used to reverse the order of the notes (force that the newest note is at the top of the list)
function ReverseFilter() {
   return function(items) {
      if (items) {
         return items.slice().reverse();
      } else {
         return items;
      }
   };
}

angular.module('activitiApp').filter('reverse', ReverseFilter).controller('notesController', notesController);

 

The file defines the logic for our notes form field.

 

Step 13: Similar to step 8, we need to add the controller file to the list of additional web resources. For this, add a new entry in the file webapps/activiti-app/scripts/app-cfg.js:

 

ACTIVITI.CONFIG.resources = {
   'workflow' : [
      {
         'tag' : 'script',
         'type' : 'text/javascript',
         'src' : ACTIVITI.CONFIG.webContextRoot + '/scripts/dynamicStencil.js?v=1.0'
      },
      {
         'tag' : 'script',
         'type' : 'text/javascript',
         'src' : ACTIVITI.CONFIG.webContextRoot + '/workflow/dynamic-stencils/notes-ctrl.js?v=1.0'
      }
   
]
};

 

Step 14: Congratulations, that's it! Now you can edit/adapt both files (the HTML design and the AngularJS controller). Changes are directly shown after refreshing the browser.

 

From Development to Production

 

 

The outsourced code of this approach can also be used in production. However, please note that the approach also levers out the actual form field design process of Alfresco Process Service. Normally, defining the code for the design and controller of a form field in the built-in stencil editor is bundled in the final app definition zip. This connects the current version of the code to the current version of the process definition. In case of a new version of the process definition and maybe of the form field code, old versions of the process definition are still using the “old” form field code. With the approach presented, the defined code in the external files is always used for all versions of a process definition. This means that you need to ensure backward compatibility of your code (similar to the concept of service tasks defined in Java).

 

Of course, you can also use this approach for development only: after you have reached a state of your form field that you define as “final”, you can just copy and paste the design and controller code into the built-in stencil editor and process it as usual.

Outcomes