Form Factory architecture overview

March 6, 2023

Overview

In this document we will introduce to the architecture and design of Form Factory 2.0, we will highlight all the changes from version1. First we will describe what has changed and what is new in the structure of the application itself. Form Factory is an application dedicated to ease the creation of all your forms on your platform. To achieve this, FF provide a set of functionalities that can be group in 4 big sections:

  • Building custom form
  • Manage your forms
  • Render/Submit forms
  • Analyze results

1 From FF1 to FF2

In FF2 we have switch from using “Spring Webflow” on the server side and BackboneJS on the client side to using a RESTful API and AngularJS for the client. On the DX side we did not touch the node types so that it is easier to move from FF1 to FF2. Your forms and results from version 1 are not lost and will appear right away in version 2. The UI has switch from Bootstrap 2 to Bootstrap 3. The captcha is not using Jahia DX captcha but it is using reCaptcha from Google.

Changes has been made on how you declare new input/action/validation/logic/callback/renderers/progress bar. From now on once you have defined the node type in the definition file of your module, all the files needed are views of this node type. No more modification of the repository.xml file.

From the previous list of extension points you figured that there is quite a lot of new things in this version. FF 2 will allow to have logics on your field to toggle their visibility. This will allow to define forms where fields appear only if the user match certain conditions, you are able to provide your own rules as for validations. Validations validate one field upon static criteria mostly whereas logics allows to show/hide a field based on value of fields on the same step or on previous steps. It is also possible to change the layout of your form, so once you have assembled all the fields you are going to need on each steps of your form you can now dispatch those inputs on a grid and not just under one another as previously.

You can now define a progress bar along your form to show to the user where they stand in the form completion. It is possible to associate a form to a callback to react after submission to the result of the actions.

The only unchanged part is the results; those have just been upgraded to Bootstrap 3.

FF 2 retain the concept of building language, meaning that a form structure (inputs/steps/actions) can only be altered in its building language, in other languages you can only translate some properties.

2 Server SIde Architecture

The JCR structure of the form has not changed; the new functionalities are working the same way as previous ones. Meaning that logics for example are stored and define like validations.

2.1 Structure

The JSON serialization of a FF2 form is quite similar to the FF 1 serialization.

Here an example of FF2 serialization:

{
  "jcrId": "0b7f45fb-081a-4be5-b72f-7bb9d6165d46",
  "formName": "architecture",
  "buildingLang": "en",
  "cssClassName": null,
  "formDescription": null,
  "afterSubmissionText": null,
  "modified": "2016-04-04T14:02:50.320-04:00",
  "steps": [
    {
      "jcrId": "72898fce-effb-4dcf-b6d0-567673bd48b9",
      "label": "Step 1",
      "inputs": [
        {
          "name": "email-input_0_1",
          "label": "Email input",
          "tab": "input",
          "template": "<ff-email></ff-email>",
          "wizard": "<ff-email view-type ='designView'></ff-email>",
          "nodeType": "fcnt:emailDefinition",
          "jcrId": "7714bafe-7226-4bcd-ba15-4780a5eb7e3e",
          "choiceField": null,
          "value": null,
          "layoutUUID": "510917c2-0ea0-47c6-a717-2aa1d545c303",
          "inputsize": "input-md",
          "helptext": "Please enter a valid email here",
          "placeholder": "aaa@aaa.com",
          "definitionOptions": [
            "inputsize"
          ],
          "definitionOptionsTranslatable": [
            "helptext",
            "placeholder"
          ],
          "validations": {
            "required": {
              "nodeType": "fcnt :requiredValidation",
              "jcrId": "ef343f97-a59b-4382-9424-adf9a06118ab",
              "message": "This field is required",
              "definitionOptions": [
              ],
              "definitionOptionsTranslatable": [
                "message"
              ]
            },
            "email": {
              "nodeType": "fcnt:emailValidation",
              "jcrId": "7c9f48b7-8ffb-4b96-a46c-6935cbc21997",
              "message": "Please enter a valid email address",
              "definitionOptions": [
              ],
              "definitionOptionsTranslatable": [
                "message"
              ]
            }
          }
        },
        {
          "name": "checkboxes-matrix_0_2",
          "label": "Checkboxes Matrix",
          "tab": "checkbox",
          "template": "<ff-checkboxes-matrix></ff-checkboxes-matrix>",
          "wizard": "<ff-checkboxes-matrix view-type='designView'></ff-checkboxesmatrix>",
          "nodeType": "fcnt:matrixCheckBoxesDefinition",
          "jcrId": "0ef30bc8-ba51-460e-8241-0ee7f3c72ca0",
          "choiceField": "rows,columns",
          "value": null,
          "layoutUUID": "d2a15b06-0187-486d-a9f2-8869d0bcc32d",
          "helptext": "Help",
          "rows": [
            {
              "key": "q1",
              "value": "Question One"
            },
            {
              "key": "q2",
              "value": "Question Two"
            },
            {
              "key": "q3",
              "value": "Question Three"
            }
          ],
          "columns": [
            {
              "key": "a1",
              "value": "Answer One"
            },
            {
              "key": "a2",
              "value": "Answer Two"
            },
            {
              "key": "a3",
              "value": "Answer Three"
            }
          ],
          "definitionOptions": [
          ],
          "definitionOptionsTranslatable": [
            "helptext",
            "rows",
            "columns"
          ],
          "validations": {
            "matrix-required": {
              "nodeType": "fcnt:matrixRequiredValidation",
              "jcrId": "3f38c426-add4-438c-8b21-78cf2ab9f8f3",
              "message": "Enter your error message here",
              "definitionOptions": [
              ],
              "definitionOptionsTranslatable": [
                "message"
              ]
            }
          },
          "logics": [
            {
              "name": "if-field-is-valid_0",
              "nodeType": "fcnt:ifFieldIsValidLogic",
              "jcrId": "16b30c64-9e00-45ec-ac66-6179700b3ed4",
              "fieldName": "email-input_0_1",
              "noview": "true",
              "definitionOptions": [
                "fieldName",
                "noview"
              ],
              "definitionOptionsTranslatable": [
              ]
            }
          ]
        }
      ]
    }
  ],
  "actions": [
    {
      "jcrId": "1b8226f7-8898-4099-ae61-b61fcab6960b",
      "nodeType": "fcnt:saveToJcrAction",
      "actionname": "savetojcr",
      "apiEntryPoint": "/modules/formfactory/results",
      "definitionOptions": [
        "actionname",
        "apiEntryPoint"
      ],
      "definitionOptionsTranslatable": [
      ]
    }
  ],
  "missingLanguages": null,
  "existingLanguages": {
    "en": "English"
  },
  "progressBar": null,
  "callbacks": null,
  "displayableName": "architecture",
  "layoutJson": "{\"active\":false,\"layoutEmpty
  \
  ":true,\"steps\":[{\"rows\":[],\"inputPositionManagement\":{\"colIndex\":null,\"
  rowIndex
  \
  ":null,\"activeInputFieldName
  \
  ":null,\"activeInputExists\":false},\"inputMovementHistory\":[]}]}",
  "token": null,
  "trackUser": true,
  "callbackName": null,
  "displayCaptcha": true,
  "theme": null
} 

The structure is the same as in V1 a form with its metadata and an array of steps and another array of actions. Each steps contains an array of inputs with their validations and logics.

2.2 Rest API

The API is defined with “JAX-RS”, each entry point is managing certain parts of the application. The 4 main entry points are

  • /formfactory/builder: all the sub resources for form creation/management
  • /formfactory/settings: all the sub resources for form factory settings
  • /formfactory/live: all the sub resources for form rendering/submission
  • /formfactory/results: all the sub resources to load/analyze the submissions

2.3 OSGI/Extendability

Once Form Factory is deployed on your platform it will react upon every module deployment and scan their definition file, looking for node types specific to Form Factory. With V2 there no need anymore to create complex structure in the repository.xml file of your module. To declare a new input, you just declare the type in the definition file, then you create a “WZD” view (it’s a file following a specific DSL in groovy to ease the creation of extensions for Form Factory).

Example of a WZD file, from Input Text definition:

input {
    label "Text input"
    template "<ff-input-text></ff-input-text>"
    wizard "<ff-input-text view-type='designView'></ff-input-text>"
    supportedValidationTypes "required", "equal", "length", "number", "regex"
    supportedLogicTypes "valid", "input"
    properties {
        inputsize "input-md"
    }
    propertiesI18n {
        helptext "Help"
        placeholder "Text input"
    }
} 

An input like this one, will need a few other files to be working with Form Factory 2.

3 Client Side Architecture

With the switch to AngularJS, we have decided that each element of Form Factory 2 will be an AngularJS directive. This means that for each input/action/validation/logic/etc. you might have to code a JS file and if needed some JSP’s. Form Factory 2 will aggregate all those JS files inside only one JS file so that all the application can be loaded with as less requests as possible. All the resource bundle from your module are going to be aggregated together and merge in a dictionary available in the builder so that you can use them in JS without worrying about how to load them across modules. This structure has allowed us to have custom templates for each input either for rendering or for editing. The panels are no more auto-generated but now you design them the way you want. The build system of Form Factory is doing the same, upon building we aggregate all the JS libraries we are using into one file (_ff2.js)., we do the same for our AngularJS application, we aggregate each components in there own JS file (ff-core.js, ff-builder.js, ff-rendering.js, ff-settings.js). We are using NPM, Bower and Grunt in conjunction with maven. Here is what a typical input structure will look like:

As you can see we need 4 files to define a new input. Actions and validations will need even less files (it is possible to declare a new action with just a “wzd” file).

  • The “wzd” file is the properties of your input (label, properties, i18n properties, validations, logics, etc.)
  • The directive view is the AngularJS directive defining how and which template to load and if needed some specific behavior.
  • Then the default view is used to render the input in the builder/preview/live mode, this is the view your visitor will see when they fill the form.
  • And last view is the “designView” which will be visible by your form designer when they create/modify a form and update an input.

The builder is build as a Single Application Page (SAP) every redirection is done internally using AngularJS UI-Router. The main routes are the library, the builder, and the translation mode.

angular.module('formFactory').config(function(contextualData, $stateProvider, $urlRouterProvider) {
...
   $stateProvider
      .state('library', {
         url: '/library',
         templateUrl:...
      })
      .state('builder', {
         url: '/builder',
         template: '<ff-controller>',
         params: {...}
      })
      .state('translator', {
         url: '/translator',
         template: '<ff-form-translator>',
         params: {...}
      });
}, ['contextualData', '$stateProvider', '$urlRouterProvider']);

On the preview/live side each form is bootstrapped individually so that they do not collide on the same page. There is no routing on the preview/live side, we load the whole form at once from the REST API and then navigate between all the steps, until submission. The module in preview/live is much lighter and with less dependencies than in the builder side. Live module declaration:

ff.app = angular.module('formFactory', ['i18n', 'formFactory.validation', 'formFactory.logic', 'ngFileUpload',
    'checklist-model', 'formFactory.commonUseFactory', 'formFactory.templateResolver', 'ui.bootstrap',
    'formFactory.dataFactory', 'vcRecaptcha']);

Builder module declaration:

angular.module('formFactory', ['ui.router', 'formFactory.dataFactory', 'datatables', 'datatables.bootstrap',
               'i18n', 'ui.bootstrap', 'ui.bootstrap.tooltip', 'formFactory.commonUseFactory', 'ngResource', 
               'formFactory.downloadZipFactory', 'formFactory.updaterFactory', 'checklist-model', 
               'uiSwitch', 'formFactory.templateResolver', 'toaster', 'ngAnimate'], 
               function ($uibTooltipProvider) {
                   $uibTooltipProvider.options({
                       placement: 'bottom',
                       popupDelay: 200,
                       popupCloseDelay: 0,
                       appendToBody: true
                   });
               });

4 Library

The library is the main entry point to your form creation and lifecycle management. Users need to be part of the role “Form Factory Manager”, “Form Factory Editor” or “Form Factory Reviewer” to access the library.

4.1 Creation

When you want to create a new form we validate that there is no form existing with the same name already. The UI is calling the entry point:

/formfactory/builder/checkformnameavailability/{language}

If name is available and valid then upon click we route you to the builder with a predefined form composed of one input text and one action (“Save To JCR”).

4.2 Management

Once you have created a form and saved it in the builder, when you are coming back to the library you will see it in the lift of forms you can manage.

You can easily modify the form, or by expanding the menu you have access to some other actions like publishing the form, deleting it. There is a shortcut to go directly to the metadata edition, this will route you the builder and open up the metadata side panel automatically.

4.2.1 Use as template

This function will create a virtual copy of the form with its new name and then you can edit/save it. If you are working with multiple languages it will copy only the current language.

4.2.2 Duplicate

On the other side duplicate is here to help you create an exact copy of an existing form, by duplicating it on the server side, meaning that as soon as you duplicate the form this one is saved on the server. The interesting behavior of duplicate is that it allows you to duplicate a form that has been created in English for say into French and then start modifying it in French. The duplicate process will switch the building language of your form to your current one. This way you can duplicate a form and modify its structure in another language.

4.3 Import/Export

FF 2 allows you to export one or multiple form(s) at the same time. If you export multiple forms, we are aggregating all the zip in a parent zip file. You can then import this/those form(s) inside another platform. The import/export is otherwise using the usual archive format of DXM import/export services.

5 Builder

The form building is with the submission analysis the main components of Form Factory. The building process can be tear down to multiple phases/functionalities. We are going to go through them one by one.

5.1 Inputs

Inputs are the heart of your forms, the builder allows you to select which inputs to add to your form. An input is comprised of multiple sub elements: § Node type definition § “WZD” file to declare the properties of this input § AngularJS directive to define the behavior of this input and how to load the different template § A default template for rendering in preview/live of this input

§ A “designView” to edit the defined properties in the builder

Definition example from country selection:

[fcnt:countryListDefinition] > jnt:content, fcmix:definition, mix:title, jmix:droppableContent, jmix:hiddenType
 - country (string, choicelist[country, sort])
//Here we are declaring a 'fake’' country property at the cnd level so that we can use DXM choicelist 
//initializer capabilities

WZD file allows to define all the properties of your input, from the same country selection example:

select {
    label "Select Country"
    template "<ff-select-country></ff-select-country>"
    wizard "<ff-select-country view-type='designView'></ff-select-country>"
    supportedValidationTypes "required"
    supportedLogicTypes "valid", "country"
    properties {
        inputsize "input-md"
        topSelections {
            values "US", "FR"
        }
    }
    propertiesI18n {
        helptext "Help"
        placeholder "Select country"
    }
}

In this file you are defining the template you want to use for the default view and for the wizard. See the appendix for a more detailed description of this WZD format. Then you list the validation and logic rules that this input support. After that you need to list all your properties, there is non I18N properties and I18N properties. The I18N properties are the only one that can be edited in translation mode.    
Form Factory is using the label property of you “WZD” file to create the node, the node name is the label in lowercase and with the whitespaces hyphenated. So that “Select Country” become “select-country”. See how the node name is referenced in the next file (the directive to resolve the different views from DXM).

The AngularJS directive is the one defining the behavior of your input when the user interacts with it:

<jcr:propertyInitializers var="countries" nodeType="fcnt:countryListDefinition" name="country"/>
angular
    .module('formFactory')
    .directive('ffSelectCountry', ['$log', 'ffTemplateResolver', function ($log, ffTemplateResolver) {
        var directive = {
            restrict: 'E',
            require: ['^ffController'],
            templateUrl: function(el, attrs) {
                return ffTemplateResolver.resolveTemplatePath('${formfactory:addFormFactoryModulePath('/form-factory-definitions/select-country', renderContext)}', attrs.viewType);
            },
            link: linkFunc
        };

        return directive;

        function linkFunc(scope, el, attr, ctrl) {
            var countryList = [
                <c:forEach items="${countries}" var="country" varStatus="s">
                {
                    country:
                    {
                        key: '${country.value.string}',
                        name: '${functions:escapeJavaScript(country.displayName)}'
                    },
                    rendererName: 'country'
                }<c:if test="${not s.last}">, </c:if>
                </c:forEach>
            ];
        ...
        }
    }]);

In this directive we are initializing the list of country from DXM country initializer, this way we do not need to get the country list from another service than DXM, but it will be possible to do some asynchronous request to an external service to get the list of countries. We then build a JavaScript structure to use as a source for our AngularJS dropdown list. The directive template is calculated upon runtime depending on your context ffTemplateResolver.resolveTemplatePath, but the view itself is rendered once and only, for the builder and for the live mode, per site and per language the results are stored in a JS file in digital-factory-data/generated-resources, if you are in development mode then this file is generated every time:

This means that calling the initializer is done only once for everybody, whereas if we were using an external services it will called for every user, so depending on the type of data each solution as its benefits. Now the UI part, the default view, this is the one your users will see when they submit the form, or when they build it:

Here is the code for this input:

<div class="form-group"
     ng-class="{'has-error': form[input.name].$invalid&&form.$dirty}"
     ng-show="resolveLogic()">
    <label class="col-sm-2 control-label">
        {{input.label}}
    </label>
    <div class="col-sm-10">
        <select name="{{input.name}}"
                class="form-control"
                ng-model="input.value"
                ng-required="isRequired()"
                ng-options="country as country.country.name for country in countries"
                ng-disabled="readOnly"
                ff-validations
                ff-logic>
            <option value="">{{input.placeholder}}</option>
        </select>
        <span class="help-block"
              ng-show="input.helptext != undefined">
            {{input.helptext}}
        </span>
        <span class="help-block"
              ng-repeat="(validationName, validation) in input.validations"
              ng-show="form[input.name].$error[(validationName | normalize)]&&form.$dirty">
            {{validation.message}}
        </span>
    </div>
</div>

When AngularJS is compiling this template, you will have access to the form controller “form” and to the current input itself “input”.

The  ng-show="resolveLogic() instruction mean that we want to display this div only if the logic rules are true (works only in live/preview mode, will do nothing in builder mode).

The ng-class="{'has-error': form[input.name].$invalid&&form.$dirty}" will apply Bootstrap 3 error classes on this div if the field is invalid and if the form has been started by the user.

On your input itself you need to map the model to “input.value”, and add the directives “ff-validations” and “ff-logic” if you want to have access and apply all the logic/validations rules defined on your input by the form designer.

The wizard view:

is defined by a JSP view like this one:

<div class="row">
    <div class="col-md-12">
        <div class="row" ng-if="!inTranslateMode">
            <div class="col-md-12">
                <label>
                    <span message-key="ff.label.changeInputFieldName"></span>
                </label>
                <input class="form-control" ng-model="input.name"/>
            </div>
        </div>
        <br/>
        <label>
            <span message-key="ff.label.changeLabel"></span>
        </label>
        <input class="form-control" ng-model="input.label"/>
    <br/>
    <label>
        <span message-key="ff.label.changePlaceholder"></span>
    </label>
        <input type="text" class="form-control" ng-model="input.placeholder"/>
    <br/>
    <label>
        <span message-key="ff.label.changeHelpText"></span>
    </label>
        <input type="text" class="form-control" ng-model="input.helptext"/>
    <br/>
        <ff-country-top-selection countries="countries"
                                  top-selections="input.topSelections"
                                  ng-if="!inTranslateMode">
        </ff-country-top-selection>
    </div>
</div>

Here again we inject some object in the scope of your directive, so you have access to your current input object “input”, there is also a flag that is very useful:

  • inTranslateMode ” will be true during translation, should be used on every non I18N property to avoid changing it.

In the wizard it is possible to use other directives to have complex input and very helpful wizard. In this example we are using a dedicated directive to allow to select a list of countries that will appear on top of the list of countries instead of having them all sorted alphabetically.

<ff-country-top-selection countries="countries" top-selections="input.topSelections" 
    ng-if="!inTranslateMode">
</ff-country-top-selection>

5.2 ActionS

Actions are a much simpler element for Form Factory. The actions are standard DXM actions, to use a DXM action in Form Factory you need to declare a specific node type, a “WZD” file and an AngularJS directive. You can also but it is not mandatory have a wizard view for your action (if you need to setup some properties for the action). List of files for declaring a working action: § Node type definition § “WZD” file to declare the properties of this input § AngularJS directive to define the behavior of this action and how to load the different template § An optional wizard view to edit the defined properties in the builder § And of course a Java class (the action is found by its action name, so it is possible to map easily any available action to be used inside Form Factory)

Here is the example from the core action “Redirect To URL”, first the definition:

[fcnt:redirectToUrlAction] > jnt:content, fcmix:action, mix:title, jmix:droppableContent, jmix:hiddenType

now the “WZD” file:

action {
    label "Redirect To Url"
    wizard "<ff-redirect-to-url view-type='designView'></ff-redirect-to-url>"
    properties {
        actionname "redirecttourl"
    }
    propertiesI18n {
        redirectto ""
    } 
}

then the directive:

(function () {
    'use strict';

    var redirectToUrl = function($log, $compile, contextualData, ffDataFactory, ffTemplateResolver) {
        var directive = {
            restrict: 'E',
            templateUrl: function(el, attrs) {
                return ffTemplateResolver.resolveTemplatePath('${formfactory:addFormFactoryModulePath('/form-factory-actions/redirect-to-url', renderContext)}', attrs.viewType);
            },
            link: linkFunc
        };
        return directive;

        function linkFunc(scope, el, attr) {}
    };

    angular
            .module('formFactory')
            .directive('ffRedirectToUrl', ['$log', '$compile', 'contextualData', 'ffDataFactory', 'ffTemplateResolver', redirectToUrl]);
})();

And finally the wizard view:

<div class="side-panel-body-content">
    <label message-key="fcnt_redirectToUrlAction.designView.label.redirectTo"></label>
    <input type="text" class="form-control" ng-model="action.redirectto">
</div>

5.3 Steps

Steps are really only managed by the designer of the form. There is no specific code associated to them. The live controller enforce that all steps are valid before allowing to go to the next one or to submit the form.

5.4 Validations

Validations are simple JavaScript function receiving the new and the previous value of the field they are validating; each field can have multiple validation active at the same time. This means that contrary to inputs or actions, validations do not always need a specific directive, in fact if they need a directive it will be for their wizard view, to achieve more complex scenarios. So at minima a validation need:

  • A node type in the CND file
  • A WZD file to define its properties
  • A JavaScript file for the rule itself (loaded only in live/preview mode)
  • A view for the wizard (validation have no views in live/preview mode) use in the builder
  • Optionally they can have an associated AngularJS directive to allow more complex input in the builder

Example code from the email validation rule, this rule ensure that the input text is matching a specified regex email:

ff.validationRules.email = function(rule, scope, el, attr, ctrls) {
    'use strict';
    var model = ctrls[1];
    //Regexp taken from http://www.regular-expressions.info/email.html
    var regexp = /^[A-Z0-9._%+-]{1,64}@(?:[A-Z0-9-]{1,63}\.){1,125}[A-Z]{2,63}$/i;

    return function(modelValue, viewValue) {
        return model.$isEmpty(viewValue) || regexp.test(viewValue);
    };
};

When Form Factory render an input it is calling every validation defined on it and register the returned function in the ngModel.$validators array of the AngularJS model, this will automatically ensure that the whole AngularJS form controller behave as expected from AngularJS point of view.

Also this validation is a standard view of DXM it is easy to override it by redefining the exact same view in a module with a higher priority for rendering. This way you can easily override all your email validation for a specific site by deploying another module. No need to tell your users to use the regex validation with a specific rule, just override the email one.

5.5 Logics

Logics rule are defined exactly the same way as validations, what changes is how they are called in the system. Every time an input value change, Form Factory check if this input is used as a source for logics rule on other inputs in the current active step (as it is the only step where values can change). If if it is then we call every logic rule on the found input, until a rule is failing. If all the rule pass the field is flagged as visible.

To declare a logic this is all the files you will need:

  • A node type in the CND file
  • A WZD file to define its properties
  • A JavaScript file for the rule itself (loaded only in live/preview mode)
  • A view for the wizard (validation have no views in live/preview mode) use in the builder
  • Optionally they can have an associated AngularJS directive to allow more complex input in the builder (after the beta)

Example code of “if field is valid logic”:

ff.logicRules.ifFieldIsValid = function(rule, scope, el, attr, ctrls) {
    'use strict';
    var model = ctrls[1];
    var ffFormController = ctrls[0];
// Try to find the input in a previous step if not found it must be in the current step
    var searchedInputValue = ffFormController.getPreviousStepInputValue(rule.fieldName);
    return function(scope,fieldValue) {
        if (_.isUndefined(searchedInputValue) && !_.isUndefined(scope.form[rule.fieldName])) {
                        return scope.form[rule.fieldName].$valid
                                && scope.form[rule.fieldName].ffVisible
                                && scope.form[rule.fieldName].$dirty;
        } else if (!_.isUndefined(searchedInputValue)) {
            //we have a valid result from a previous step.
            return (searchedInputValue != null);
        }
    }
};

Logic and Validation rules are not loaded by the builder. The only way to test those rules is to load the form in Preview or in Live.

5.6 Layout

The layout of the form once activated is defined at the form level, and serialized as a JSON property:

layoutJson: {
  "active": true,
  "layoutEmpty": true,
  "steps": [
    {
      "rows": [
        {
          "row": [
            {
              "col": "col-sm-6",
              "layoutUUID": "510917c2-0ea0-47c6-a717-2aa1d545c303",
              "offset": "col-sm-offset-0",
              "moveActive": true,
              "activeClass": "activeColumn"
            },
            {
              "col": "col-sm-6",
              "layoutUUID": "d2a15b06-0187-486d-a9f2-8869d0bcc32d",
              "offset": "col-sm-offset-0",
              "moveActive": false,
              "activeClass": "inActiveColumn"
            }
          ]
        }
      ],
      "inputPositionManagement": {
        "colIndex": 0,
        "rowIndex": 0,
        "activeInputFieldName": "510917c2-0ea0-47c6-a717-2aa1d545c303",
        "activeInputExists": true
      },
      "inputMovementHistory": [
        {
          "rowIndex": 0,
          "colIndex": 0,
          "createdColumn": false
        }
      ]
    }
  ]
}

The layout is a structure listing the rows per steps and for each rows the number of cells and if there is an input in the cell.

Input that are not dispatched in a cell won’t be visible by the user. Use preview mode to test your layout

5.7 Metadata

As layout, metadata are stored at the form level they contain:

  • the list of languages for this form
  • its title
  • its description
  • CSS class
  • submission message
  • progress bar
  • tracking
  • captcha
  • themes

5.7.1 Themes

Themes allows user to override the default views of Form Factory for every input, allowing user to switch from our default Bootstrap 3 theme to another one like Material CSS for example. This way you will be able to have forms matching your website theme/CSS engine. Those themes are just sets of views allowing a quick overhaul of Form Factory forms in your site, not in the builder and not how they behave. Themes are an integrated part of Form Factory so you can have multiple modules defining multiple themes. To declare a theme, you just need to declare a view of fcnt:form and use the same name for all the views you want to overhaul. Once the theme is selected it will be injected into the form as an attribute called view-type, this happening only in live/preview mode, excerpt from form.angular.jsp

<div id="formFactory${identifier}">
    <ff-controller view-type="${currentNode.properties['theme'].string}" id="'${identifier}'" data-locale="'${currentNode.resolveSite.language}'" data-show-form-title="${currentResource.moduleParams.showFormTitle}"></ff-controller>
</div>

Then we are calling your main entry point form.theme-name.jsp

<c:if test="${jcr:isNodeType(currentNode, 'fcmix:formTheme')}">
    <template:include view="${currentNode.properties['theme'].string}" templateType="html"/>
</c:if>

This way you can initialize any framework you are using if needed. The theme property is also used in the ffInputFormDirective during form rendering.

var formController = ffControllerCtrl[0];
            scope.form = formController.getFormController();
            scope.readOnly = !formController.isLiveMode();
            el.append($compile(angular.element(scope.input.template).attr('view-type', !_.isUndefined(formController.currentForm.theme) && formController.currentForm.theme !== null ? formController.currentForm.theme : ''))(scope));

5.7.2 Progress Bar

Progress Bar are showing information like steps name or percentage of completion along your form. They are composed of a definition, a WZD file, a directive and a template.

5.7.2.1 Definition

//-----------------------------------------------
// Form Center Progress Bar Definition
//-----------------------------------------------

[fcmix:hasProgressBar] mixin
 + progressBar (fcmix:progressBar) = fcmix:progressBar

[fcmix:progressBar] mixin
 - template (string) mandatory
 - wizard (string) mandatory
 - position (string, choicelist) = "top" mandatory < "top", "bottom", "both"
 + * (fcnt:definitionOptions) = fcnt:definitionOptions

[fcnt:percentProgressBar] > jnt:content, fcmix:progressBar, mix:title, jmix:droppableContent, jmix:hiddenType

The mixin fcmix:hasProgressBar will be applied on the form if there is a progress bar for this form. The mixin fcmix:progressBar is the mixin your progress bar definition should extend to be found by the system.

5.7.2.2 WZD

progressBar {
    label "Percent Progress Bar"
    template "<ff-percent-progress-bar></ff-percent-progress-bar>"
    wizard "<ff-percent-progress-bar view-type='designView'></ff-percent-progress-bar>"
    position "top"
}

5.7.2.3 Directive

angular
    .module('formFactory')
        .directive('ffPercentProgressBar', ['$log', 'ffTemplateResolver', '$filter', function ($log, ffTemplateResolver, $filter) {
        var directive = {
            restrict: 'E',
            templateUrl: function (el, attrs) {
                return ffTemplateResolver.resolveTemplatePath('${formfactory:addFormFactoryModulePath('/form-factory-progress-bars/percent-progress-bar', renderContext)}', attrs.viewType);
            },

            link: linkFunc
        };

        return directive;

        function linkFunc(scope, el, attr, ctrl) {
            scope.roundValue = function(value) {
               return $filter('roundValue')(value);
            };
        }
    }]);

5.7.2.4 View/Template

<div class="row">
    <div class="col-md-12">
        <div class="progress">
            <div class="progress-bar progress-bar-success" role="progressbar"
                 aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{roundValue((currentStep+1)/form.steps.length*100)}}"
                 style="min-width: 2em; width: {{roundValue((currentStep+1)/form.steps.length*100)}}%;">
                {{roundValue((currentStep+1)/form.steps.length*100)}}%
            </div>
        </div>
    </div>
</div>

5.7.3 Captcha

Form Factory 2 is using reCaptcha from Google instead of the DX captcha by default. This need to be configured by setting up your API key. The integration is done on the client side, there is no server call to google. This integration use Angular ReCaptcha to validate the form or not, as long as the captcha is not validated the submit buttons are not activated. The JS files are loaded if needed in form.angular.jsp and the captcha is displayed on the last step with this code from formDefinition.formView.jsp, here is the code.

<div class="row" ng-if="vm.getCaptchaKey() !== null && vm.currentForm.steps.length-1 === vm.currentStep && vm.currentForm.displayCaptcha">
    <div class="col-sm-12 text-right">
        <div vc-recaptcha theme="'light'" key="vm.getCaptchaKey()" on-create="vm.notifyOfCaptchaLoad()"></div>
    </div>
</div>

5.7.4 Tracking

The tracking option will save the user IP upon submission or not.

6 Preview/Live

The preview/live mode allows you to test/submit your form. In preview you will able to validate your validations, logics and layout. Actions can be only tested in live mode.

6.1 Rendering

During the rendering first we bootstrap every form separately. This way you could have different forms on the same page. Once every app has been bootstrapped, we load the form data. We are using Jahia Assets management capabilities so that the JavaScript files are loaded only once, even the one aggregating every element at your disposal.

6.1.1 Inputs

We render every inputs of the current step. For each type of input we request compile the template, by default it means rendering the default view of this input. The HTTP request for the view should happen only once per type as AngularJS should cache the result of the template locally. Then for each input AngularJS will apply the model of the input into its template.

6.1.2 Validations

Validations are executed on every field upon every changes in the model of the current step. So every time the user enters a new data or select a new entry all the fields are validated. When moving between steps, we restore the status of every input/validation upon re-entering, this will flag the form as dirty and touched once again.

6.1.3 Logics

Every change in the values of the input will trigger an evaluation of the logic rules on the current step, to show or hide fields depending on the current state of the model.

All invisible inputs are considered valid.

A field can be required and invisible, as long as it is invisible it will not block the validation of the form, if it become visible then it will block the user to move forward until it fills this input.

6.1.3.1 Logic directive and Logic service

The logic directive is in charge of listening to the changes of values on the current step and evaluate all the logic rules define on those fields. The directive is in charge of finding the fields values in the previous steps or in the current one, apply all the rules on a field. The Logic service is the repository of rules available, same as the Validation service.

In preview/Live we load only the rule for Validation and Logic, we do not load any template

6.2 Submission

Reaching the last step, as long as the form is valid, it is possible to submit it. Upon submission the user will submit only the ids of the inputs along with there value, we do not submit the whole model to the server.

6.3 Actions

Those are regular DX Actions, by default FF provide 4 actions: § Save to JCR: Will save the data submitted in the JCR backend § Send Email: Will send an email to the configured emails address, the email will contain the data submitted by the user § Redirect to Page: Will redirect the user to a specific page after submission § Redirect to URL: Will go to another website upon submission All actions are called in the backend whatever their result is. So you will always receive in the response a message from each action.

6.4 Callbacks

6.4.1 Per form callback

One of the new functionality of this version 2, it is now possible to define at the form level which callback function you want to apply once the actions have been called, this will allow to customize the way you handle errors, redirection etc. on a per form basis. Here is an example of a callback that will automatically redirect to the page selected in “Redirect to a page” action but also display a message to the user and let it choose between waiting or clicking, the callback also detects if there is a “Redirect to URL” and in that case offer a link to the URL instead of an automatic redirect.

angular
        .module('formFactory')
        .directive('callbackTwo', ['$log', 'ffTemplateResolver', '$interval', function ($log, ffTemplateResolver, $interval) {
            var directive = {
                restrict: 'E',
                require: ['^ffController'],
                scope: {
                    actionData: '=',
                    callback: '&'
                },
                templateUrl: function(el, attrs) {
                    return ffTemplateResolver.resolveTemplatePath('${formfactory:addFormFactoryModulePath('/form-factory-callbacks/callback-two', renderContext)}', attrs.viewType);
                },
                link: linkFunction
            };

            return directive;

            function linkFunction(scope, el, attr, ctrl) {
                scope.url = '#';
                scope.page = '#';
                scope.secondsToRedirect = 5;

                for (var i in scope.actionData) {
                    if ('actionName' in scope.actionData[i] && scope.actionData[i].actionName[0] === 'redirectToUrl') {
                        scope.url = scope.actionData[i].redirectUrl[0];
                    }
                    if ('actionName' in scope.actionData[i] && scope.actionData[i].actionName[0] === 'redirectToAPage') {
                        scope.page = scope.actionData[i].redirectUrl[0];
                    }
                }

                scope.countDown = function() {
                    if (scope.secondsToRedirect === 0) {
                        window.location.assign(scope.page);
                        return;
                    }
                    scope.secondsToRedirect -= 1;
                };

                $interval(scope.countDown, 1000);
            }
        }]);

6.4.2 Form Factory API JS callback

This type of callback is the one used to integrate Form Factory forms with other systems, they are used in our own products like Marketing Factory or Marketo module. Those integrations work in two steps, first those tools allow a tighter integration with Form Factory for their form mapping functionalities, allowing to map multiple steps form easily to their own sets of fields. Once the mappings are defined those products register themselves as callback for Form Factory, doing so they will receive all the data submitted.

<script type="text/javascript">
   document.addEventListener("ffFormReady", function(e) {
   var formInfo = e.formInfo;
   window.ffCallbacks.registerCallback(formInfo, testObject.testFunction, testObject);
});

var testObject = {
   testFunction : function(data, formInfo) {
       var self = this;
       setTimeout(function() {
           formInfo.notifyFormFactoryOfCompletion("testFunction");
       }, 3000);
   }
}
</script>

7 Results

The overall architecture of the results analysis as not changed from V1 to V2, this is still the same code, the views have been switched to Bootstrap 3 and all the JavaScript framework have been updated.

The form submits are saved in the live Workspace, under the folder named “formFactory/results “. In all the cases, whatever the destination of the form submitted data, the form metadata are saved in this folder. The actual data are saved in JCR only when the form contain the action “Save to JCR”.

We can see that the Results are fetched from JCR directly by the API which return JSON objects to pages. The structure of the returned JSON Object is :

{
  "status" : <"success" || "error">,
  "code" : <HTML CODE>,
  "message" : <String Message>,
  "data" : <Data Object>
}

The results are wrapped in an object under the “data” key.

7.1 Actions/Results Bound

In the Form Center, the actions have to be mapped to the results display methods to be able to fetch view data from the good source (JCR, Database ...). Form center actions bounding is based on three entities:

  • Backend type: A constant to define where the results are saved.
  • API: An API name (this represent how results can be fetched)
  • Mapping view: A file defined for each API/Backend couple that returns the API entry points for each request of the view.
  • Views: View to display results fetched from a given API to the user.

To know which API to map on each action, the form center watch for the ResultsProviderService implementations which map a list of API on a backend type. As each actions declare a Backend type implementing the APIBackendType interface, the org.jahia.modules.formfactory.api.subresources.FormResults get the backend type for each action and watch for ResultsProviderService implementation with this backend type, then it gets the corresponding list of API and from there can access all the views of the APIs.

7.1.1 APIBackendType Interface

The interface org.jahia.modules.formfactory.api.APIBackendType is implemented by the action class to declare the Action Backend type.

This action Backend type is a String constant that has to be reused in the class that will implement org.jahia.modules.formfactory.api.ResultsProviderService to map the backend to the right API.

7.1.2 ResultsProviderService Interface

The Interface org.jahia.modules.formfactory.api.ResultsProviderService is implemented for each couple Action / API to map an API to a given Backend type. For the action SaveToJCRAction, the class org.jahia.modules.formfactory.impl.SaveToJcrRawResultsImpl is doing this mapping :

public class SaveToJcrRawResultsImpl implements ResultsProviderService {
    @Override
    public String getAPINames() {
        List<String> APINames = new ArrayList<String>();
          APINames.add("rawResults")
          return APINames;
    }

    @Override
    public String getBackendType() {
   return "JCR";
    }
}

We can see here that the Backend type is the same than the one declared in the SaveToJCRAction class.

7.2 Implementations Fetching

The function getViewsByAction of SaveToJCRResultAPI.java return all the view and the APIs by action. To do so, it gets the list of actions in the formed saved in JCR and from this list it gets all the API by action:

//Putting in the map all the views for each [backend,API] couple
for (ResultsProviderService providerService : resultsProviderServices) {
    String providerBackEndType = providerService.getBackendType();
    if (providerBackEndType.equals(((APIBackendType) action).getBackendType())) {
        List<Map<String, String>> prefixedViews = new ArrayList<Map<String, String>>();
        for(String APIName : providerService.getAPINames()){
            prefixedViews.addAll(prefixViews(providerBackEndType, viewMap.get(APIName)));
        }
        actionViewMap.put("views", prefixedViews);
    }
}

Then it gets the list of views for each API. Finally, it returns the list of views for each action.

7.2.1 Views

A view is linked to an API because it has to submit to the format of the results returned. To declare this bound, each view name has to contain the API name. A results view is a view of the node submissions (of type fcnt:submissions) and its name has to respect a rule:

submissions.<APIName>.<viewName>

A resource bundle property is used to properly display this view name to a user, this property key has to be defined precisely:

fcnt_submissions.viewName.<APIName>.<viewName>

for example the the view datatable of the rawResults API will be named:

submissions.rawResults.dataTable

and its property key will be:

fcnt_submissions.viewName.rawResults.dashboard

7.2.2 API Mapping JSP file

The last link between the actions and the API is the JSP file that map the view fetch keys to the API entry points. The different views that displays results call getFetchURL function :

//Generate an URL from parameters
function getFetchURL(urlParameters) {
    return APIMethods[urlParameters.APIMethodName](urlParameters);
}

This function takes the following object as parameter:

{
    formId:this.model.formId,
    APIMethodName:"results_submissiontime",
    fromDate: this.model.fromDate,
    toDate: this.model.toDate
}

The parameter APIMethodName is a constant used to map API entry points thanks to the object APIMethods :

{
    "results":function(parameters){
        if(parameters.fromDate!=null && parameters.toDate!=null){
            return APIURLBase + parameters.formId + "/from/" + parameters.fromDate + "/to/" + parameters.toDate + "/results";
        }
        else{
            return APIURLBase + parameters.formId + "/results";
        }
    },
        "results_submissiontime":function(parameters){
        return APIURLBase + parameters.formId + "/results/submissiontime";
    },
        "results_submissionpage":function(parameters){
        return APIURLBase + parameters.formId + "/results/submissionpage";
    },
        "results_submissionempty":function(parameters){
        return APIURLBase + parameters.formId + "/results/submissionempty";
    },
        "results_choicelabel":function(parameters){
        return APIURLBase + parameters.formId + "/results/choicelabel";
    },
        "results_label":function(parameters){
        return APIURLBase + parameters.formId+"/results/labels";
    },
        "results_choice":function(parameters){
        return APIURLBase + parameters.formId+"/results/choice/"+parameters.choiceId;
    },
        "total":function(parameters){
        return "${url.context}/modules/formcenter/results/total/"+parameters.formId;
    },
        "lastDays":function(parameters){
        return "${url.context}/modules/formcenter/results/${renderContext.UILocale.language}/totallastdays/"+parameters.formId;
    },
        "groupTotal":function(parameters){
        return "${url.context}/modules/formcenter/results/${renderContext.UILocale.language}/groupTotal/"+parameters.formId;
    }
}


This object map the different String keys to API get URLs.

This system make possible:

  • The creation of new view for each API in separated module (by naming the views the right way).
  • The mapping of a Backend type to new API in any separated module (implementing org.jahia.modules.formfactory.ResultsProviderService).
  • The creation of new APIs for a given backend type in any separated module (Implementing the API and org.jahia.modules.formfactory.ResultsProviderService).

7.3 Results API

The results API is developed using JAX-RS, the source of API different function (All HTTP GET methods ) is localized in the org.jahia.modules.formfactory.api.subresources.FormResults class.

This class uses different Spring injected variables:

  • renderService: Jahia RenderService used to get views of node submissions
  • repository: jcrSessionFactory used to get current session
  • resultsProviderServices: List of implementations of the interface ResultsProviderService.
  • templateManagerService: Jahia TemplateManagerService used to get the actions available on the server.

The API class contains a certain number of methods (all HTTP Get) that return JSON object.

Some of these methods will always be used even if the form results are not saved in JCR :

getViewsByActions : Returns an object containing the views for each action basing on the existing Form center actions. 

Example:

{<actionname>:{<view1Name>:<view1Label>, <view2Name>:<view2Label>}}
[{"views":[{"viewName":<viewName>,"viewLabel":<viewLabel>}],"actionName":<actionName>}]

The view name is composed the following way:

<APIName>.<viewName>, so the name of dashboard view of rawResults API will be rawResults.dashboard.

The view Label is a resource bundle with a key composed the following way : fcnt_submissions.viewName.<APIName>.<viewName>, so the resource bundle key for the dashboard view of the RawResult API is : fcnt_submissions.viewName.rawResults.dashboard § getActionsProviders: Returns the list of actions id in an array. § getFormsDetails: Returns in an object the details (metadata) from a given form

7.3.1 Views

The results section contains multiple pages. The first pages (That we can call results home page) displays the list of forms that have at least one result already saved :

The JCR Results API (org.jahia.modules.formfactory.api.subresources.FormResults) is called to fill this table. The function getViewsByActions is called with the API entry point “/views” to fill the view selection of the Form Display column. The columns Name and Date are filled with the getFormsDetails function. This table is displayed using a Backbone view (FormsResultsView) based on a collection (FormsCollection). All the Backbone Views definitions are localized in the main-results.js file. Each table line contains a form that is submitted to the same page. On the page load, if a form and a view have been submitted the form results are displayed in the selected view. The form center comes with the RawResult API and its JCR implementation. The rawResult API contains 3 views :

7.3.2 Metadata view

The metadata view is made to display only metadata of the submitted results (Source, user and date) and its code is in the submissions.rawResults.metadata.jsp file.

It uses the Backbone view MetadataResultsView which initializes a Datatable fetching backbone model. In each line the submission date value is clickable and display the submitted result in a popover which is defined by the function displayData of the JSP file.

7.3.3 Datatable view

The Datatable view render results in a table managed by Datatables .

The Backbone model used is a pageable collection called PageableResults from the framework Backbone.Paginator defined in the results.js file under the collections folder. The view implemented to display this collection is PageableResultsView (from main-results.js ). The size of the page is 500 results per page.

The view defines a Datatable with unlimited scroll using Scroller datatable extension.

The columns labels are requested from server by the JavaScript function getLabelsFromResults located in the fcResultsUtils.js file. This function then calls the RawResultAPI entry point.

7.3.4 Dashboard view

The dashboard view is using Charts.JS (1.0.2), each chart is doing some specific queries to get the data and aggregate them for visualization.

8 Project Structure

As Form Factory 2 is using even more JavaScript than version 1, we have totally restructured the code, with a more dedicated build cycle for the JavaScript source code. The JavaScript source code of the main AngularJS components are stored at the same level as the Java source code. The build system is then using GruntJS to aggregate all the components files in one file per component.

Form Factory is also using NodeJS , NPM , Bower and Grunt . To avoid building issues on developer environment, we are using a maven plugin, this will download all the necessary JavaScript tools/frameworks for the project.

<plugin>
    <groupId>com.github.eirslett</groupId>
    <artifactId>frontend-maven-plugin</artifactId>
    <version>0.0.29</version>
    <!-- executions go here -->
    <executions>
        <execution>
            <id>npm install node and npm</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>install-node-and-npm</goal>
            </goals>
        </execution>
        <execution>
            <id>NPM install</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>npm</goal>
            </goals>
        </execution>
        <execution>
            <id>bower update</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>bower</goal>
            </goals>
            <configuration>
                <arguments>update -F</arguments>
            </configuration>
        </execution>
        <execution>
            <id>bower install</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>bower</goal>
            </goals>
            <configuration>
                <arguments>install -F</arguments>
            </configuration>
        </execution>
        <execution>
            <id>grunt build</id>
            <goals>
                <goal>grunt</goal>
            </goals>
            <phase>generate-resources</phase>
            <configuration>
                <arguments>default</arguments>
            </configuration>
        </execution>
    </executions>
</plugin>


This way just doing the usual mvn install will download all required resources, both Java backend and JavaScript frontend ones. The default GruntJS task will concatenate files and uglify them.

8.1.1 GruntJS tasks

First we concatenate all our developments in 4 components: ff-formbuilder.js, ff-formrendering.js, ff-settings.js and ff-core.js. Then all the bower dependencies are concatenated in _ff2.js and _ff2.css.

The uglification process is creating 4 compressed files for the JS:

files: {
    'src/main/resources/javascript/lib/_ff2.min.js' :
        ['src/main/resources/javascript/lib/_ff2.js'], 
    'src/main/resources/javascript/lib/_ff-rendering.min.js' :
        ['src/main/resources/javascript/angular/components/ff-formrendering.js', 
            'src/main/resources/javascript/angular/components/ff-core.js'],
    'src/main/resources/javascript/lib/_ff-builder.min.js' :
        ['src/main/resources/javascript/angular/components/ff-formbuilder.js', 
            'src/main/resources/javascript/angular/components/ff-core.js'],
    'src/main/resources/javascript/lib/_ff-settings.min.js' :
        ['src/main/resources/javascript/angular/components/ff-settings.js', 
            'src/main/resources/javascript/angular/components/ff-core.js']
}

Before release we change the options of the uglification tasks to compress the JavaScript, instead of just concatenating them. The JS frameworks are compressed in two different files for builder and for live.

9 AppendixeS

9.1 WZD files definitions

Here you will find the syntax and explanation for all the WZD files we have introduced. A WZD file always starts with a keyword that is defining which object we are declaring to use. On the server side each Form Factory node type is associated with a specific parser. The WZD file are following a DSL (Domain Specific Language) defined in Groovy, more detail about it following. All the parsers are declared in Spring like this:

<bean id="formFactoryDefinition" class="org.jahia.modules.formfactory.bundle.DefinitionService">
    <property name="jcrTemplate" ref="jcrTemplate"/>
    <property name="publicationService" ref="jcrPublicationService"/>
    <property name="renderService" ref="RenderService"/>
    <property name="templateManagerService" ref="JahiaTemplateManagerService"/>
    <property name="jahiaUserManagerService" ref="JahiaUserManagerService"/>
    <property name="settingsBean" ref="settingsBean"/>
    <property name="dslExecutor" ref="dslExecutor"/>
    <property name="dslHandlerMap">
        <map key-type="java.lang.String" value-type="org.jahia.modules.formfactory.dsl.DSLHandler">
            <entry key="input" value-ref="ffInputHandler"/>
            <entry key="validation" value-ref="ffValidationHandler"/>
            <entry key="logic" value-ref="ffLogicHandler"/>
            <entry key="action" value-ref="ffActionHandler"/>
            <entry key="progressBar" value-ref="ffProgressBarHandler"/>
            <entry key="renderer" value-ref="ffRendererHandler"/>
            <entry key="callback" value-ref="ffCallbackHandler"/>
        </map>
    </property>
</bean>
<lang:groovy id="dslExecutor" script-source="classpath:org/jahia/modules/formfactory/dsl/DSLExecutorImpl.groovy">
</lang:groovy>
<lang:groovy id="ffInputHandler" script-source="classpath:org/jahia/modules/formfactory/dsl/InputHandler.groovy">
<lang:property name="jcrTemplate" ref="jcrTemplate"/>
<lang:property name="packageRegistry" ref="org.jahia.services.templates.TemplatePackageRegistry"/>
</lang:groovy>

<lang:groovy id="ffValidationHandler" script-source="classpath:org/jahia/modules/formfactory/dsl/ValidationHandler.groovy">
<lang:property name="jcrTemplate" ref="jcrTemplate"/>
<lang:property name="packageRegistry" ref="org.jahia.services.templates.TemplatePackageRegistry"/>
</lang:groovy>

<lang:groovy id="ffLogicHandler" script-source="classpath:org/jahia/modules/formfactory/dsl/LogicHandler.groovy">
<lang:property name="jcrTemplate" ref="jcrTemplate"/>
<lang:property name="packageRegistry" ref="org.jahia.services.templates.TemplatePackageRegistry"/>
</lang:groovy>

A parser is a groovy script creating content in the JCR, something like this (simple one from the renderer):

class RendererHandler implements DSLHandler {
    private static final Logger logger = LoggerFactory.getLogger(RendererHandler.class);
    def JCRTemplate jcrTemplate;
    def TemplatePackageRegistry packageRegistry

    def renderer(@DelegatesTo(FormFactoryWizardParser) Closure cl) {
        parseDeclaration(cl)
    }

    def parseDeclaration(Closure cl) {
        def rendererType = new FormFactoryWizardParser()
        def code = cl.rehydrate(rendererType, this, this)
        code.resolveStrategy = Closure.DELEGATE_FIRST
        code()
        logger.info("Declared renderer structure: " + rendererType.toString());
        def result = rendererType.contentMap
        createWizard result, cl.owner.currentPackage, cl.owner.currentNodeType
    }

    def createWizard(Map<String, Object> map, JahiaTemplatesPackage currentPackage, ExtendedNodeType currentNodeType) {
        assert map.containsKey("label")
        assert map.containsKey("name")
        jcrTemplate.doExecuteWithSystemSessionAsUser(JahiaUserManagerService.instance.lookupRootUser().jahiaUser, Constants.EDIT_WORKSPACE, Locale.ENGLISH, new JCRCallback() {
            @Override
            Object doInJCR(JCRSessionWrapper jcrSessionWrapper) throws RepositoryException {
                JCRNodeWrapper bundleRootNode = jcrSessionWrapper.getNode(currentPackage.rootFolderPath + "/" + currentPackage.getVersion().toString())
                def rNodeWrapper = bundleRootNode.getNode("templates").getNode("contents")
                if (!rNodeWrapper.hasNode("form-factory-renderers")) {
                    rNodeWrapper.addNode("form-factory-renderers", "jnt:contentFolder")
                }
                def rendererRootNode = rNodeWrapper.getNode("form-factory-renderers")
                def nodeName = JCRContentUtils.generateNodeName(map["label"], 32)
                if (rendererRootNode.hasNode(nodeName)) {
                    rendererRootNode.getNode(nodeName).remove();
                    jcrSessionWrapper.save()
                }

                def node = rendererRootNode.addNode(nodeName, currentNodeType.name)
                node.setProperty(Constants.JCR_TITLE, map["label"])
                map.remove("label")
                node.setProperty("rendererName", map["name"])
                map.remove("name")

                jcrSessionWrapper.save()
                return null
            }
        })
    }
}


Let’s describe the DSL:

handler-name {
   label
   template/wizard/etc. DSL properties 
}

9.1.1 Inputs

To declare an input, you need to write a WZD file looking like this:

checkbox {
    label "Checkboxes"
    template "<ff-checkboxes></ff-checkboxes>"
    wizard "<ff-checkboxes view-type='designView'></ff-checkboxes>"
    supportedValidationTypes "required", "checkbox", "number"
    supportedLogicTypes "valid", "checkbox"
    choiceField "checkboxes"
    propertiesI18n {
        helptext "Help"
        checkboxes {
            values "checkboxone":"Checkbox One","checkboxtwo":"Checkbox Two"
        }
    }
}

or from a text input:

input {
    label "Text input"
    template "<ff-input-text></ff-input-text>"
    wizard "<ff-input-text view-type='designView'></ff-input-text>"
    supportedValidationTypes "required", "equal", "length", "number", "regex"
    supportedLogicTypes "valid", "input"
    properties {
        inputsize "input-md"
    }
    propertiesI18n {
        helptext "Help"
        placeholder "Text input"
    }
}

9.1.1.1 Inputs DSL

Property Name Value Mandatory
handler/type input/checkbox/select/radio yes
label String yes
template String yes
wizard String yes
supportedValidationTypes List<String> no
supportedLogicTypes List<String> no
properties Map no
propertiesI18n Map no
choiceField String no

9.1.1.2 Properties DSL

To declare the properties of your element you can use simple key, value:

properties {
    inputsize "input-md"
}
propertiesI18n {
    helptext "Help"
    placeholder "Text input"
}

You can also use a list of string:

properties {
    inputsize "input-md"
    topSelections {
        values "CA", "FR"
    }
}

Or a more complex map:

propertiesI18n {
    helptext "Help"
    checkboxes {
        values "checkboxone":"Checkbox One","checkboxtwo":"Checkbox Two"
    }
}

Contrary to V1 there is no need for you to define the list of options in the WZD, just list the properties you want the user to fill and their default value. After that it is in the wizard view that you will define the possible values or validations

For example, the property inputsize of a regular text input can be designed as this in the wizard view:

<div class="col-md-12">
    <label>
        <span message-key="ff.label.changeInputSize"></span>
    </label>
    <select class="form-control" ng-model="input.inputsize">
        <option value="input-lg" message-key="ff.label.large"></option>
        <option value="" message-key="ff.label.medium"></option>
        <option value="input-sm" message-key="ff.label.small"></option>
    </select>
</div>

9.1.1.3 choicefield property

This property is an inheritance from V1, it is use to declare the list of your properties that are using a key/value system, so that for the results we know how to map the key (saved upon submission) to the label in your current language. Form Factory does not save the label themselves but just the key, this way whatever the language your user submit you will be able to see the label in your language of choice (as long as the form is translated in this language). Example from simple checkboxes:

propertiesI18n {
    checkboxes {
        values "checkboxone":"Checkbox One","checkboxtwo":"Checkbox Two"
    }
}
choiceField "checkboxes"

Example from matrix fields:

propertiesI18n {
    rows {
        values "q1":"Question One","q2":"Question Two","q3":"Question Three"
    }
    columns {
        values "a1":"Answer One","a2":"Answer Two","a3":"Answer Three"
    }
}
choiceField "rows,columns"

9.1.2 Actions

Property Name Value Mandatory
handler/type action yes
label String yes
wizard String yes
properties Map no
propertiesI18n Map no

In the properties of an action you have to declare the actionname property at least as it is the bound between your Form Factory declaration and the action class in DX. Remember that you can refer/bound to any action in the system without module dependencies.

Example from “Redirect to a page” action:

action {
    label "Redirect To A Page"
    wizard "<ff-redirect-to-a-page view-type='designView'></ff-redirect-to-a-page>"
    properties {
        actionname "redirecttoapage"
    }
    propertiesI18n {
        redirectto ""
        pagename ""
    }
}

or more simply from “Save to JCR” action (this one has no wizard associated):

action {
    label "Save To Jcr"
    properties {
        actionname "savetojcr"
        apiEntryPoint "/modules/formfactory/results"
    }
}

9.1.3 Validations

Property Name Value Mandatory
handler/type validation yes
label String yes
types String or List<String> yes
wizard String no
properties Map no
propertiesI18n Map no

    
The label is use to retrieve the rule in the JavaScript environment, by being transformed to Camel Case and also use for the resource bundle key mapping, in this case the label is transformed with the following rule as always:
    
Label -> lower case -> whitespace converted to hyphen

So that label “Equal to” become:

ff.label.validation.equal-to=Equal to 
ff.validationRules.equalTo = function (rule, scope, el, attr, ctrls)

Minimal example from “Required” validation:

validation {
    label "Required"
    types "required"
    propertiesI18n {
        message "This field is required"
    }
}

And a more complex validation declaration for the “File” validation rule:

validation {
    label "File"
    types "files"
    wizard "<ff-file-wizard  view-type='designView'></ff-file-wizard>"
    properties {
        filetype {
            values "all": ".*/.*", "image":"image/.*", "audio":"audio/.*", "video":"video/.*", "pdf":"application/pdf", "doc":"application/(pdf|.*word.*|.*openxmlformats-officedocument.*|.*opendocument.*|.*excel.*|.*powerpoint.*)"
        }
        filesize ""
        unittype {
            values "KB", "MB", "GB"
        }
    }
    propertiesI18n {
        message "Enter your error message here"
    }
}

This validation declares a wizard template, so that we can use some specific behavior for this validation rules.

9.1.3.1 types property

types property allows you to declare a list of type for your validation rule, which will be used to display this rule for every input supporting this type of validation. This means that you can extend the validation of every existing input by declaring a new validation being part of the existing types. Or you can declare a very private validation for your new input by binding them through a new validation type.

9.1.3.2 List of predefined types

equal files checkbox
matrix selections length
number date email
required regex  

9.1.3.3 Example

ff.validationRules.required = function(rule, scope, el, attr, ctrls) {
    'use strict';
    var model = ctrls[1];
    var formController = ctrls[0].getFormController();
    scope.$watch(function () {
        return scope.form[rule.fieldName].$valid;
    }, function () {
        model.$validate();
    });
    attr.required = true; // force true in case we are on non input element

    return function (modelValue, viewValue) {
        var input = formController[rule.fieldName];
        if (input.$valid && input.$dirty) {
            if (angular.isUndefined(viewValue)) {
                return false;
            }
            return !model.$isEmpty(viewValue);
        } else {
            return model.$isEmpty(viewValue);
        }
    };
};

9.1.4 Logics

Property Name Value Mandatory
handler/type logic yes
label String yes
types String or List<String> yes
properties Map no

The label is use to retrieve the rule in the JavaScript environment, by being transformed to Camel Case and also use for the resource bundle key mapping, in this case the label is transformed with the following rule as always:

Label -> lower case -> whitespace converted to hyphen
    

 So that label “If field is valid” become:

ff.label.logic.if-field-is-valid =Equal to (Resource Bundle)
ff.logicRules.ifFieldIsValid = function (...) (JavaScript)

Example from “if Field is Valid” logic rule

logic {
    label "If field is valid"
    types "valid"
    properties {
        fieldName "Enter the Name of the field you want to be valid."
        noview "true"
    }
}

The noview property indicates that this logic rules do not have any specific wizard (it has no configuration).

9.1.4.1 types property

types property allows you to declare a list of type for your logic rule, which will be used to display this rule for every input supporting this type of validation. This means that you can extend the validation of every existing input by declaring a new validation being part of the existing types. Or you can declare a very private validation for your new input by binding them through a new validation type.

9.1.4.2 List of predefined types

input files checkbox
country selections valid
radio date select
selectmultiple switch  

9.1.4.3 Example

ff.logicRules.ifFieldIsValid = function(rule, scope, el, attr, ctrls) {
    'use strict';
    var model = ctrls[1];
    var ffFormController = ctrls[0];
    var searchedInputValue = ffFormController.getPreviousStepInputValue(rule.fieldName);
    return function(scope,fieldValue) {
        if (_.isUndefined(searchedInputValue) && !_.isUndefined(scope.form[rule.fieldName])) {
            return scope.form[rule.fieldName].$valid
                && scope.form[rule.fieldName].ffVisible
                && scope.form[rule.fieldName].$dirty;
        } else if (!_.isUndefined(searchedInputValue)) {
            //we have a valid result from a previous step.
            return (searchedInputValue != null);
        }
    }
};

9.1.5 Renderers

Property Name Value Mandatory
handler/type renderer yes
label String yes
name String yes

9.1.6 Progress Bars

Property Name Value Mandatory
handler/type progressBar yes
label String yes
template String yes
wizard String yes
position String yes
properties Map no
propertiesI18n Map no

9.2 Roles/Permissions

Form Factory 2 defines new roles and permissions to allow access to its different functionalities.

9.2.1 Roles

Manager
  • Can create form
  • Can publish form
  • Can manage settings
  • Can view results
Editor
  • Can create form
  • Can start workflow on form
Reviewer
  • Can review form before publication
Results
  • Can see form results

9.2.2 Permissions

All the permissions of form factory are stored under the global scope of ‘Site Administration’ permissions. Those permissions limit the access to the menu entries, in site settings.

Form Factory Global access to form factory (mandatory for seeing the menu)
Form Factory Editor Access to the builder panel
Form Factory Results Access to the results panel
Form Factory Settings Access to the settings panel

9.3 JavaScript Frameworks

Dependency Homepage
bootstrap-datepicker@1.5.1 https://github.com/eternicode/bootstrap-datepicker
angular-animate@1.5.3 https://github.com/angular/angular.js
angular-bootstrap@1.1.2 https://github.com/angular-ui/bootstrap
angular-recaptcha@2.4.1 https://github.com/vividcortex/angular-recaptcha
angular-resource@1.5.3 https://github.com/angular/angular.js
angular-route@1.5.3 https://github.com/angular/angular.js
angular-sanitize@1.5.3 https://github.com/angular/angular.js
angular-ui-switch@0.1.1 https://github.com/xpepermint/angular-ui-switch
angular@1.5.3 https://github.com/angular/angular.js
AngularJS-Toaster@0.4.18 https://github.com/jirikavi/AngularJS-Toaster
bootstrap@3.3.6 http://getbootstrap.com
datatables-plugins@1.10.11 https://github.com/DataTables/Plugins
datatables.net-buttons-bs@1.1.2 https://datatables.net
datatables.net-buttons@1.1.2 https://datatables.net
datatables.net-scroller-bs@1.4.1 https://datatables.net
datatables.net-scroller@1.4.1 https://datatables.net
file-saver.js@1.20150507.2 https://github.com/Teleborder/FileSaver.js
flag-icon-css@1.4.0 https://github.com/lipis/flag-icon-css
moment@2.11.2 https://github.com/moment/moment
ng-file-upload@10.0.4 https://github.com/danialfarid/ng-file-upload
pdfmake@0.1.20 https://bpampuch.github.io/pdfmake
underscore@1.8.3 http://underscorejs.org
angular-datatables@0.5.3 http://l-lin.github.io/angular-datatables
angular-ui-router@0.2.18 http://angular-ui.github.io/
Chart.js@1.1.0 http://www.chartjs.org
datatables.net-bs@1.10.11 https://datatables.net
datatables.net@1.10.11 https://datatables.net
underscore.string@3.2.3 http://epeli.github.com/underscore.string/
jszip@2.6.0 http://stuartk.com/jszip
moment-range@2.1.0 https://github.com/gf3/moment-range