HCL Leap and HCL Domino Leap
Custom Widget API (v0.9)

This API provides a mechanism to incorporate custom widgets into HCL Leap and HCL Domino Leap.

Table of Contents:

Tech Preview

This is a "tech preview" of the Custom Widget API. Custom widgets and the applications created with them are not guaranteed to work in subsequent releases of HCL Leap and HCL Domino Leap.  This is unsupported/unwarranted code.  HCL recommends using it in a non-production environment.

Getting Started

Product Configuration

Additional resources can be loaded into the Leap UI's by adding runtimeResources properties to the product configuration. These additional resources are expected to include definitions of your custom widgets and any auxiliary styles or libraries that are required to support them.

Leap example (Leap_config.properties):

ibm.nitro.NitroConfig.runtimeResources.1 = \
  <link rel='stylesheet' type='text/css' media='screen' href='https://mywidgets.example.com/common.css'>; \n\
  <link rel='stylesheet' type='text/css' media='screen' href='https://mywidgets.example.com/MyYesNoWidget.css'>; \n\
  <script src='https://myWidgets.example.com/common.js'></script> \n\
  <script src='https://myWidgets.example.com/MyYesNoWidget.js'></script> \n

Domino Leap example:
Open VoltBuilder.nsf and add a new configuration setting doc:

Enable Setting
Setting Name: runtimeResources.1
(value): 
  <link rel='stylesheet' type='text/css' media='screen' href='https://mywidgets.example.com/common.css'>; \n\
  <link rel='stylesheet' type='text/css' media='screen' href='https://mywidgets.example.com/MyYesNoWidget.css'>; \n\
  <script src='https://myWidgets.example.com/common.js'></script> \n\
  <script src='https://myWidgets.example.com/MyYesNoWidget.js'></script> \n

Registering a Widget

As your custom .js is loaded into the page, it is expected to register one or more widget definitions:

const myWidgetDefinition = {...};
nitro.registerWidget(myWidgetDefintion);

Full descriptions and examples are provided below in this document; here is the basic skeleton of a custom widget:

const myWidgetDefinition = {
    id: 'example.YesNo', // uniquely identifies this widget
    version: '2.0', // the widget's version
    apiVersion: '0.9', // the version of this API
    label: 'Yes/No',
    description: 'Allows user to choose "Yes" or "No"',
    datatype: {
        type: 'string' // must be one of 'string', 'date', 'number', 'boolean' 
    },
    // for placement in the palette
    category: {
        id: 'example.choice.widgets',
        label: 'Choice Components'
    },
    formPalette: true, // show/hide on the palette for Forms
    appPagePalette: true, // show/hide on the palette for App Pages
    iconClassName: 'myYesNoIcon', // styling of this class expected in custom .css
    builtInProperties: [...], // use existing properties: 'title', 'required', etc
    properties: [...], // custom properties, of prescribed types

    // called by Leap to initialize widget in the DOM with initial properties, and set-up event handling
    instantiate: function (context, domNode, initialProps, eventManager) {
        return {
             // (optional) for display in various parts of the UI
            getDisplayTitle: function () {
                return ...
            },

            // (required) for Leap to get widget's data value
            getValue: function () {
                return ...
            },

            // (required) for Leap to set widget's data value
            setValue: function (val) {
                ...
            },

            // (optional) for additional validation of value
            validateValue: function (val) {
                // return true, false, or custom error message
            },

            // (required) called when properties change in the authoring environment, or via JavaScript API
            setProperty: function (propName, propValue) {
                ...
            },

            // (optional) method to enable/disable widget
	        setDisabled: function (isDisabled) {
                ...
            },

            // (optional) determines what the author can do with the widget via custom JavaScript
            getJSAPIFacade: function () {
                return {
                    ...
                 };
            }
        };
    }
};

Data Widgets vs Display Widgets

Some widgets are for collecting data (ie. "data widgets") and others are presentational in nature (ie. "display widgets").

A data widget is required to:

Some display widgets are still expected to trigger events (ex onClick), which can be used by the app author to invoke an action, by custom JavaScript or other techniques.

Data Types

Data widgets can declare one of the following data types, each with additional optional constraints. Constraints on the data type goes beyond the UI. These constraints will be enforced when data is submitted to the server.

'string'

Example:

const myWidgetDefinition = {
   ...
   datatype: {
     type: 'string',
     length: 50,
     format: {
        simplePattern: '#####,#####-####' // US zip code
        invalidMessage: 'Please enter a valid US zip code'
     }
   }
   ...
};

'boolean'

'number'

Example:

const myWidgetDefinition = {
   ...
   datatype: {
     type: 'number',
     numberType: 'decimal',
     decimalPlaces: 2
   }
   ...
};

'date'

'time'

'timestamp'

Rules

App authors will be able to incorporate custom widgets in rules, as follows:

Display Widgets

Data Widgets

Built-In Properties

Some properties that already exist in the product are general purpose, or, are integral to the proper functioning of a widget. The following built-in properties are supported for custom widgets:

Example:

const myWidgetDefinition = {
    ...
    builtInProperties : [{ id: 'required'}, {id: 'title'}, {id: 'seenInOverview', defaultValue: true}],
    ...
}

Note: All widgets will be implicitly given an ID property. The default value of this property will be auto-incrementing unique value based on the widget definition's label. For example, a widget with a label of 'Yes/No' will result in a default ID of 'F_YesNo1'. Similar to Leap's built-in widgets, the app author is free to alter the ID to suit their needs.

Custom Properties

The custom widget can define an array of custom properties for the app author to modify. Each property is an object with the following attributes:

Example:

const myWidgetDefintion = {
  ...
  properties: [
    ...
    {
      id: 'messageType',
      label:  'Message Type',
      propType: 'enum',
      values: [{title: 'Information', value: 'info'}, {title: 'Warning', value: 'warn'}, {title: 'Error', value: 'error'}],
      defaultValue: 'info'
    },
    ... 
  ],
  ...
};

Widgets with Options

Widgets that allow the end-user to select from a set of options require specific treatment. This includes widgets such as dropdowns, radio groups, or checkbox groups. These options could be hardcoded in the custom widget, or defined by the app author.

Author-Defined Options

If a widget requires app authors to define their own options, define a property with both id and propType set to a value of "customOptions". For example,

const myWidgetDefintion = {
    ...
    properties: [
        {
            id: "customOptions",
            propType: "customOptions",
            label: "Options"
        },
        ...
    ],
    ...

Note: An id of 'customOptions' is meaningful to Leap. All other custom property id's are arbitrary.

Hardcoded Options

If the widget's options are hardcoded, add a getOptions() function to your widget. For example:

const myWidgetDefintion = {
    ...
    getOptions : function () {
	    return [{title: 'Yes', value: 'yes'}, {title: 'No', value: 'no'}];
    },
    ...
};

Multiple Selections

If your widget allows end-users to select multiple options (ie. a checkbox group), set the isMultiSelect attribute of the widget to true

const myWidgetDefintion = {
    ...
    isMultiSelect: true
    ...
}

When isMultiSelect is true, it is expected that the widget's getValue() function will return an Array of values. Similarly, the widget's setValue() function will be passed an Array of values.
Note: The isMultiSelect flag is only supported for string data type and widgets with options.

Widget Instantiation

The widget's instantiate() function is called when an instance of the custom widget needs to be created. The function is expected to return an object that allows Leap to interact with the instantiated widget.

instantiate() is called with the following arguments:

The returned object is expected to supply the following functions:

The widget creator is free to decide how they want to code and manage the widget instance internally.

Validation

Some intrinsic validation will be done according to the type and constraints declared in the widget's datatype property; however, it might be necessary for a widget to supply its own custom validation logic. This can be done by supplying a validateValue() function, which returns one of the following values:

It is responsibility of the custom widget to render itself appropriately based on its state of validity. Note, the widget's setErrorMessage function will be triggered whenever the validity changes, due to constraints on the datatype or custom validation from the validateValue() function.
Note: Any additional validation provided by the custom widget via a validateValue() function will not be enforced on the server; however, it will prevent the form from being submitted by the user in the browser.

Internationalization

Certain attributes of the widget definition can be displayed to app authors working in different locales. To support multiple languages during authoring, some properties can be specified as "multi-string" objects rather than a plain string values.
For example:

  label: 'Yes/No',

can be written as

  label: {
    "default": 'Yes/No',
    "fr": 'Oui/No',
    "de": 'Ja/Nein'
  },

The property names are expected to match the lang attribute of the current HTML page. For example, "fr": 'Oui/No' matches <html lang="fr">.
If there is no match, then the "default" property will be used as a fallback.

The following items are globalizable:

Usage of Leap JavaScript API

Custom widgets can use Leap's JavaScript API to help achieve their objectives. The API is can be accessed via the global NitroApplication object or by the passed-in context object. For example, the following is a widget that renders itself appropriately based on the form's currently selected page:

const myPageNavigator = {
    ...
    instantiate: function (context, domNode) {
        if (context.mode === 'run' || context.mode === 'preview') {
            const currentPage = context.page;
            context.form.getPageIds().forEach((pageId) => {
                const page = context.form.getPage(pageId);
                const btn = document.createElement('button');
                btn.innerHTML = makeHTMLSafe(page.getTitle());
                if (page === currentPage) {
                    btn.setAttribute('disabled', 'true');
                } else {
                    btn.addEventListener('click', () => {
                        context.form.selectPage(pageId);
                    });
                }
                domNode.appendChild(btn);                    
            });
        } else {
            ...
        }
        return { ... };
    }
}

Versioning

The widget's version must follow "Semantic Versioning" (semver.org) practices of MAJOR.MINOR.PATCH. The following behaviours are expected:

Note: This Custom Widget API will also follow "semver" practices.

Upgrading Custom Widgets

As custom widgets evolve, there may be major changes to custom widget definitions that are not backwards compatible with existing applications. The exact technique for upgrading custom widgets is yet to be determined.

Security Considerations

  1. It is the responsibility of the widget creator to avoid script injection attacks by ensuring that values are sanitized or escaped properly before placing them into the DOM. In general, the widget creator is responsible for following secure engineering practices.

  2. The custom widget code has full access to the page, but it should not call product functions, manipulate the product's JavaScript values, or interact with the product's DOM nodes in any way that is not prescribed by this API. Doing so could jeopardize the security of the product.

  3. As stated above, special care must be taken when supplying a getJSAPIFacade() function to expose additional widget capabilities for app authors to leverage in their custom JavaScript. These functions should provide tightly constrained interactions with the custom widget, with no possibility for script injection or access to the widget's internal objects or its DOM, or those of the product. The "facade" naming is a reminder that the app author's code should only get references to values and objects that are necessary and "safe".

  4. It is the responsibility of widget creators and Leap administrators to ensure that only trusted stable resources are loaded into Leap's pages. The specified additional resources will be loaded directly into the user's browser (by injecting them as-written into the <head> of the page). There will be no additional vetting or sanitizing of resources by Leap. It is not recommended for a customer to rely on resources that they do not tightly control (ie. avoid usage of libraries from a 3rd-party CDN).

  5. Strict CSP support requires a special nonce='#!#cspNonce!#!' attribute on <script> tags. For example:

ibm.nitro.NitroConfig.runtimeResources.4 = <script nonce='#!#cspNonce!#!' src='https://myWidgets.example.com/MyYesNoWidget.js'></script>

Incorporating 3rd-party libraries

Known Limitations

Full Example - Display Widget

See Acme_PageNavHeader_Widget.js
This "Page Navigation Header" is meant to be placed at the top of each page in a multi-page form. It demonstrates the ability for a custom widget to use Leap's documented JavaScript API, for awareness of the form in which it has been placed.

ibm.nitro.NitroConfig.runtimeResources.1 = \
  <link rel='stylesheet' type='text/css' media='screen' href='https://leapsandbox.hclpnp.com/custom_widgets/samples/acme/Acme_Widgets.css'>; \n\
  <script src='https://leapsandbox.hclpnp.com/custom_widgets/samples/acme/Acme_common.js'></script>
ibm.nitro.NitroConfig.runtimeResources.2 = \
  <script src='https://leapsandbox.hclpnp.com/custom_widgets/samples/acme/Acme_PageNavHeader_Widget.js'></script>

Full Example - Data Widget

See Acme_YesNo_Widget.js
This widget demonstrates how to create a widget with selectable options. In this example, the options are hardcoded to "Yes" and "No".

ibm.nitro.NitroConfig.runtimeResources.1 = \
  <link rel='stylesheet' type='text/css' media='screen' href='https://leapsandbox.hclpnp.com/custom_widgets/samples/acme/Acme_Widgets.css'>; \n\
  <script src='https://leapsandbox.hclpnp.com/custom_widgets/samples/acme/Acme_common.js'></script>
ibm.nitro.NitroConfig.runtimeResources.3 = \
  <script src='https://leapsandbox.hclpnp.com/custom_widgets/samples/acme/Acme_YesNo_Widget.js'></script>

Full Example - React Material UI Widget

Download leap-custom-widgets-react-mui-master.zip
Consult the README.md file within.

Coming soon

More examples coming soon to https://github.com/HCL-TECH-SOFTWARE