gravitonian

Activiti Enterprise Developer Series - Custom Form Fields

Blog Post created by gravitonian Employee on Nov 23, 2016

Table of Contents                   

Introduction

Something that comes up quite often in Activiti Forums is how to add a new form field type and render it in the Activiti Application UI. Doing this is actually quite easy. We will look at a couple of examples. The first one is a simple Month field presented as a dropdown where the end-user can select a month. This is all static and involves no dynamic data. However, in many solutions we need to load a dropdown or a list with data from an external service. The second example shows one way of doing that via a REST call. And finally we will have a look at an example that also involves custom parameters specified by the form designer.

 

If you are working with an earlier version of Activiti that has the Explorer UI, then see this blog for how to do this.

 

This article covers the new Angular based UI.

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

There is no need to set up an extension project when working with custom form fields, it is all done via the Activiti Application UI.

Create a Custom Form Stencil

All the custom form fields that we create will be part of a custom form stencil. You can think of a custom form stencil as a custom Form Editor configuration.

 

Stencils are created via the Kickstart App:

 

 

Clicking on this application shows the following page:

 

 

Click on the Stencils menu item at the top and then on the Create a new Stencil now! Button:

 

 

Here I have to select Form editor from the Editor type drop down. We should now have a custom stencil as follows:

 

Implementing a Month Form Field

Let’s start with a simple example where we want to add a form field type that allows the user to select a month. Visually, we want to use a dropdown list for that on the UI side of things.

 

When finished we will have something that looks like this:

 

 

And whatever month is selected will be available in a process variable when the form is completed.

 

When designing a form the month field type will look like this:

 

Creating the Form Field in the Custom Stencil

Open up the “My Company Form Editor” stencil in the Stencil Editor:

 

 

Here we have the default form editor palette of fields that we can use when creating forms. Now, to add a new field type, click on the +Add new item in the upper right corner, this displays the following screen:

 

 

The properties in this screen have the following meaning:

 

  • Name: the name of this field in the Form Editor palette
  • Description: the description of this field in the Form Editor palette
  • Internal identifier: id that can be used to access the field control in JavaScript
  • Icon: the icon of this field in the Form Editor palette
  • Field width: how many columns should this field take up in the finished form shown to user
  • Form runtime template: the markup that makes up the UI representation of the field, as shown to the end-user
  • Form editor template: what you the form designer see when using this field in a form

 

Fill in the properties like this (to the right we can see where the properties are used in the form designer or in the finished form as shown to the end user):

 

 

In this case we have given the new field the name Month, and it will show up as that in the Form Editor palette just after the icon. The icon is a custom one that is based on the text field one but with some extra month list text in it.

 

The Form runtime template has been filled in with HTML markup that will be used to display the field in the form as seen by the end-user. The UI is written in Angular version 1, so there is some Angular directives in the code, such as ng-controller. It will bind this UI code to a Angular controller called monthController, which we have to define later on.

 

There is also the ng-model Angular directive that can be used to do a data binding so when you select something in the dropdown, the value is stored in the data.selectedMonth JavaScript variable. The data binding is bidirectional so we can initialize the dropdown (i.e. pre-select) in the controller if we want to.

 

We also need to define how the new field should be displayed in the form editor when dragged onto the form canvas. This is done with the Form editor template, keep this display static and simple as it does not support parameters.

 

Next step is to implement the Angular controller for the field, this is done by clicking on the Edit button in the field creator screen:

 

This brings up the following dialog where we can implement the controller:

 

 

The following code attaches this Controller to the HTML markup we just created (i.e. the View):

 

angular.module('activitiApp').controller('monthController'

 

The following function declaration starts the controller implementation:

 

function ($rootScope, $scope) {

 

Inside the controller we first need to set up the JavaScript object that is used for the data binding to the dropdown control:

 

$scope.data = {
    selectedMonth: null,
};

 

This is also where you could initialize more variables that should be used in the dropdown control, such as initially selected option. For more information about scopes, see Angular scope guide.

 

After this we hook into one of the extension points in the UI called formBeforeComplete, which will be called just before the end-user completes the user-task. There are a number of empty extension point functions that we can implement to get called back at certain points/events in the UI lifecycle. See these docs for a full list of extension points.

 

These extension functions are all defined in the ALFRESCO.formExtensions object. Our implementation looks like this:

 

ALFRESCO.formExtensions.formBeforeComplete =
    function(form, outcome, scope) {
        console.log('Before form complete');
        $scope.field.value = $scope.data.selectedMonth;
    };  

 

What this code does is making sure that what was selected in the month drop down box, such as the value feb, for month February, is set in the field value as specified by the form designer. Basically, when the form designer uses our control, they will give the field an ID, such as this:

 

The following code make sure that the selectamonth field will have the selected month value set:

 

$scope.field.value = $scope.data.selectedMonth;

 

This means also that there will be a process variable with the name selectamonth available with the value that the end-user selected.

Trying out the Month Field

Create a simple process model with a single User Task as follows:

Then create a form for the user task by clicking on the Referenced form property for it and then click New form button:

 

Call the new form Select Month and make sure to select the My Company Form Editor stencil that we have just customized with the Month field:

 

Drag-and-drop the new Month field into the form canvas and add also a standard text field so we can see how they are presented together. Fill in the Month field properties as follows:

 

Now, create an Application and add this process model to it so we can publish it and execute it. For more information about this see this blog section.

 

Start a process instance and you should see a User Task form as follows:

 

Select a month and fill in some text:

 

Then click the COMPLETE button.

 

If we were to install an execution listener on the sequence flow after the user task, and it printed process variables, then we would see something like this:

 

04:29:37,387 [http-nio-8080-exec-6] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - --- Process variables:

04:29:37,387 [http-nio-8080-exec-6] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [specifysometext = Activiti is cool!]

04:29:37,387 [http-nio-8080-exec-6] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [initiator = 1]

04:29:37,387 [http-nio-8080-exec-6] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [selectamonth = Aug]

Debugging an Angular Controller

Implementing Angular controllers can sometimes be a bit frustrating if you don’t know how to debug them. To debug JavaScript code we can for example use the FireBug plugin.

 

What we want to do first is to kick off a new process instance so we see the user task as follows:

 

 

Now, start FireBug in detached mode, or whatever other JS debugger you are using:

 

 

If you are not in the Script tab switch to it. To set a breakpoint in the controller code select the  controllers file from the drop down in upper left corner:

 

 

Set breakpoints by clicking in the area to the left of the line numbers. In this case I have set one breakpoint just at the entry of the controller and one inside the callback function, which is useful as I can then be sure the extension point is actually called.

 

Select a month and fill in some text and then click COMPLETE, the debugger should stop at the breakpoint inside the extension point callback code:

 

 

You can inspect variables to the right in the Watch area. This should give you an idea of how to debug your Angular Controllers.

Implementing a Dynamic Form Field

In this example we will see how we can populate a dropdown with data from a REST call.

What the end result look like

In this example we will use an external Web Service that can return a list of the US States.

 

We call the web service like this:

 

http://services.groupkt.com/state/get/usa/all

 

{
 "RestResponse" : {
   "messages" : [ "More webservices are available at

http://www.groupkt.com/post/f2129b88/services.htm",

"Total [56] records found." ],
   "result" : [ {
     "country" : "USA",
     "name" : "Alabama",
     "abbr" : "AL",
     "area" : "135767SKM",
     "largest_city" : "Birmingham",
     "capital" : "Montgomery"
   }, {
     "country" : "USA",
     "name" : "Alaska",
     "abbr" : "AK",
     "area" : "1723337SKM",
     "largest_city" : "Anchorage",
     "capital" : "Juneau"
   }, {
     "country" : "USA",
     "name" : "Arizona",
     "abbr" : "AZ",
     "area" : "294207SKM",
     "capital" : "Phoenix"
   },...

 

It responds with JSON and we can get to the list of states via the RestRespone.result property.

 

The end-user will see the following dropdown in a form that uses this field:

And whatever state is selected will be available in a process variable when the form is completed.

 

When designing a form the state field type will look like this:

 

Prerequisites

As this involves making a call to another domain it will not work straight away because of the same origin policy applied by the web browsers. If we just went on with this implementation and called the other domain (i.e. not the local domain where we loaded the Activiti Application), then we would see the following type of error messages during debugging:

 

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://services.groupkt.com/state/get/usa/all. (Reason: missing token 'cache-control' in CORS header 'Access-Control-Allow-Headers' from CORS preflight channel).

 

We can easily get around this by installing a Proxy in front of the Activiti Server and the External Web Service. We would access everything through the proxy so to the browser it looks like everything is coming from the same origin.

 

Note. if the server supports JSONP, such as the Alfresco ONE server, you could get around installing an extra proxy. Angular $http supports JSONP calls.

Installing NGINX proxy

The NGINX server is a useful piece of software that makes it easy to set up a Reverse Proxy locally. It has a similar function as Apache HTTP Server. The installation is different for different operating systems, here is how I installed it on Ubuntu (see the NGINX site for more info on how to install on other platforms):

 

$ sudo netstat -nlp |grep :80

tcp6       0      0 :::80                   :::*                    LISTEN      2922/apache2

$ /etc/init.d/apache2 stop

[ ok ] Stopping apache2 (via systemctl): apache2.service.

 

$ sudo apt-get install nginx

Setting up nginx-core (1.9.3-1ubuntu1.2) ...

Setting up nginx (1.9.3-1ubuntu1.2) ...

Configure NGINX proxy

So we want to configure the proxy with location of both the Activiti server and the external web service. Open up the default site configuration:

 

martin@gravitonian:/etc/nginx/sites-available$ sudo gedit default

 

And change it so it looks like this:

 

server {

    listen                          8888;

    server_name                     dev-proxy;

 

    access_log  /var/log/nginx/dev-proxy.log;

    error_log  /var/log/nginx/dev-proxy.error.log;

 

    location /state {

          proxy_pass  http://services.groupkt.com;

    }

 

    location /activiti-app/ {

   proxy_pass  http://localhost:8080;

   proxy_pass_header  Set-Cookie;

   proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;

 

   proxy_redirect          off;

   proxy_set_header        Host            $host;

   proxy_set_header        X-Real-IP       $remote_addr;

   proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

   client_max_body_size    10m;

   client_body_buffer_size 128k;

   proxy_connect_timeout   90;

   proxy_send_timeout      90;

   proxy_read_timeout      90;

   proxy_buffers           32 4k;

    }

}

 

Then restart the proxy:

 

$ sudo service nginx restart

 

You should be able to access the proxy via browser now at http://localhost:8888/ and see something like this:

 

Now, try and access the external web service at http://services.groupkt.com/state/get/usa/all via the proxy on http://localhost:8888/state/get/usa/all:

 

 

And Activiti at http://localhost:8080/activiti-app via the proxy at http://localhost:8888/activiti-app:

 

 

Ok, that’s it, we are all set now to start implementing our US state dropdown.

Creating the Form Field in the Custom Stencil

Open up the “My Company Form Editor” stencil in the Stencil Editor:

 

 

To add a new field type, click on the +Add new item in the upper right corner. Fill in the form as follows (to the right we can see where the properties are used in the form designer or in the finished form as shown to the end user):

 

In this case we have given the new field the name US State, and it will show up as that in the Form Editor palette just after the icon. Upload an icon that fits the field.

 

The Form runtime template has been filled in with HTML markup that will be used to display the field in the form as seen by the end-user. It will bind this UI code to an Angular controller called usSateController, which we have to define later on.

 

The ng-model Angular directive is used to do a data binding so when you select something in the dropdown, the value is stored in the data.selectedState JavaScript variable. The data binding is bidirectional so we also use it to fill the dropdown with options, via the ng-options Angular directive, from the data.states property, which we will set up in the controller implementation.

 

We then define how the new field should be displayed in the form editor when dragged onto the form canvas. This is done with the Form editor template, keep this display static and simple as it does not support parameters.

 

Next step is to implement the Angular controller for the field, this is done by clicking on the Edit button in the field creator screen:

 

 

This brings up the following dialog where we can implement the controller:

 

The full JavaScript code looks like this:

 

angular
.module('activitiApp')
.controller('usSateController',
['$rootScope', '$scope', '$http',
  function ($rootScope, $scope, $http) {
       $scope.data = {
           selectedState: null,
           states: null
       };
    
       // Fetch all the states from an external REST service
       // that responds with JSON
       $http.get('http://localhost:8888/state/get/usa/all').
           success(function(data, status, headers, config) {
               var tempResponseArray = data.RestResponse.result;
               $scope.data.states = [];   
               for (var i = 0; i < tempResponseArray.length; i++) {
                   var state = { name: tempResponseArray[i].name,
                                 code : tempResponseArray[i].abbr };
                   $scope.data.states.push(state);   
               }   
           }).
           error(function(data, status, headers, config) {
                   alert('Error: '+ status);
                   tempResponseArray = [];
           });         

       // Setting the value before completing the task so it's properly stored
       ALFRESCO.formExtensions.formBeforeComplete =
           function(form, outcome, scope) {
               $scope.field.value = $scope.data.selectedState;
           };    
  }]
);

 

The following code attaches this Controller to the HTML markup we just created (i.e. the View):

 

angular.module('activitiApp').controller('usSateController' 

 

The following function declaration starts the controller implementation:

 

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

 

Note here that we also bring in the $http Angular module so we can execute our REST call to the Web Service.

 

Inside the controller we first need to set up the JavaScript object that is used for the data binding to the dropdown control:

 

$scope.data = {
    selectedState: null,
    states: null
};

 

For more information about scopes, see Angular scope guide.

 

We then make the Web Service call via the $http service:

 

$http.get('http://localhost:8888/state/get/usa/all').
    success(function(data, status, headers, config) {
        var tempResponseArray = data.RestResponse.result;
        $scope.data.states = [];   
        for (var i = 0; i < tempResponseArray.length; i++) {
            var state = { name: tempResponseArray[i].name,
                          code : tempResponseArray[i].abbr };
            $scope.data.states.push(state);   
        }   
    }).
    error(function(data, status, headers, config) {
        alert('Error: '+ status);
        tempResponseArray = [];
    });  

 

Note here that we make the Web Service call via the proxy at localhost:8888. The get() call is asynchronous and will return promises, so we need to make sure we do all processing inside the success() method. Otherwise we will try and do the processing before the REST call returns. The $http service will automatically handle JSON responses and we can grab the JSON Array straight from the data.RestResponse.result property. We then get the State Name and Code from the name and abbr properties respectively.

 

After this we hook into one of the extension points in the UI called formBeforeComplete, which will be called just before the end-user completes the user-task. Our implementation looks like this:

 

ALFRESCO.formExtensions.formBeforeComplete =
     function(form, outcome, scope) {
         $scope.field.value = $scope.data.selectedState;
     };           

 

What this code does is making sure that what was selected in the state drop down box, such as the value FL, for state Florida, is set in the field value as specified by the form designer. Basically, when the form designer uses our control, they will give the field an ID, such as this:

 

 

The following code make sure that the chooseusstate field will have the selected state value set:

 

$scope.field.value = $scope.data.selectedState;

 

This means also that there will be a process variable with the name chooseusstate available with the value that the end-user selected.

Trying out the US State Field

Continuing with the process and user-task form we created earlier on for the Month field. Important, be sure to access Activiti via the proxy at http://localhost:8888/activiti-app. Open up the form and add a US State field to it:

 

Drag-and-drop the new US State field into the form canvas and fill in the field properties as follows:

 

 

Now, re-publish the application to get the changes to take effect.

 

Start a new process instance and you should see a User Task form as follows:

 

 

Select a month, fill in some text, and choose a US State:

 

 

Then click the COMPLETE button.

 

If we were to install an execution listener on the sequence flow after the user task, and it printed process variables, then we would see something like this:

 

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  - --- Process variables:

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting1Proc = Hello World!]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [chooseusstate = HI]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [specifysometext = Activiti is cool!]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [greeting1ProcLocal = Hello World Local!]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [initiator = 1]

10:44:44,216 [http-nio-8080-exec-10] INFO  com.activiti.extension.bean.HelloWorldExecutionListener  -    [selectamonth = null]

Implementing a Hyperlinked Image Form Field

So far all our example custom fields have not needed any form designer input. Meaning they have needed no configuration at the time they were included in the user-task form. However, there might be certain custom form fields that need extra parameters for configuring the field when it is included in the form.

 

The next example will show how to use custom parameters for a form field. We will implement a hyperlink image field, which will need the form designer to supply both the image link and the hyperlink at the time when the form is designed.

Creating the Form Field in the Custom Stencil

Open up the “My Company Form Editor” stencil in the Stencil Editor:

 

 

To add a new field type, click on the +Add new item in the upper right corner. Fill in the form as follows (to the right we can see where the properties are used in the form designer or in the finished form as shown to the end user):

 

In this case we have given the new field the name Image Hyperlink, and it will show up as that in the Form Editor palette just after the icon. Upload an icon that fits the field.

 

The Form runtime template has been filled in with HTML markup that will be used to display the field in the form as seen by the end-user. In the above example the configuration parameters (i.e. hyperlinkURL and imageURL) have been set to point to the Activiti Org site.

 

We then define how the new field should be displayed in the form editor when dragged onto the form canvas. This is done with the Form editor template, keep this display static and simple as it does not support parameters.

 

Next step is to define the two configuration parameters. Click on the Edit link at the bottom of the field creator screen:

 

 

This brings up the following dialog where we can define custom tabs and properties:

 

 

Start by adding a new Tab called Image and position it between the two existing tabs, then add the first property with id hyperlinkURL:

 

 

Click Save property. Then add the other property with id imageURL:

 

 

Click Save property. And then save the stencil.

 

When you specify the IDs for the properties it is important that they match what has been coded in the Form runtime template.

Continuing with the process and user-task form we created earlier on for the other custom fields. Important, be sure to access Activiti via the proxy at http://localhost:8888/activiti-app as we still have the dynamic form field. Open up the form and add an Hyperlink Image field to it:

 

 

Then hover over the field and click the pen icon to the right to edit properties:

 

Specify a hyperlink URL and an image URL.

 

Now, re-publish the application to get the changes to take effect.

 

Start a new process instance and you should see a User Task form as follows:

 

 

If you click on the Activiti Logo it should take you to the http://www.activiti.org site.

Outcomes