This API provides a mechanism to incorporate custom widgets into the HCL Leap product.
Additional resources can be loaded into the Leap UI's by adding ibm.nitro.NitroConfig.runtimeResources properties to Leap_config.properties. These additional resources are expected to include definitions of your custom widgets and any auxiliary styles or libraries that are required to support them.
For example:
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
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.0', // the widget's version
apiVersion: '1.0.0', // 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', time, timestamp
},
// for placement in the palette
category: {
id: 'example.choice.widgets',
label: 'Choice Components'
},
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 {
...
};
}
};
}
};
Some widgets are for collecting data (ie. "data widgets") and others are presentational in nature (ie. "display widgets").
A data widget is required to:
datatype property (described below)setValue() and getValue() functionsonChange event when its value is changed by the user. This will trigger a call to the widget's getValue() function, and validateValue() function if suppliedSome 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 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'length - the max number of characters allowed, including multi-byte charaters. Note, if this length is greater than 255 the submitted value will be stored as CLOB in the database and will not be sortable. Default value: 50subType
format - limit user's input to specific format, use # for numbers 0-9, @ for letters A-Z and ? for number or letter
simplePatterninvalidMessagemultiValue - true or false, if true the value returned by the widget is an array of strings, otherwise it is a single string. The default value is false
Example:
const myWidgetDefinition = {
...
datatype: {
type: 'string',
length: 50,
format: {
simplePattern: '#####,#####-####' // US zip code
invalidMessage: 'Please enter a valid US zip code'
}
}
...
};
'boolean'true or false value. A null value is not supported. The default value is false'number'numberType: one of 'decimal' or 'integer'. Default: 'decimal'decimalPlaces: if number is a 'decimal', will round to the given number of decimal places. Default: 2minValue : minimum value of number expected. Can be omitted or set to null if no minimummaxValue : maximum value of number expected. Can be omitted or set to null if no maximumExample:
const myWidgetDefinition = {
...
datatype: {
type: 'number',
numberType: 'decimal',
decimalPlaces: 2
}
...
};
'date''YYYY-MM-DD' string format'YYYY-MM-DD' string format, or as a Date object.minValue : minimum value of date expected. Can be omitted or set to null if no minimummaxValue : maximum value of date expected. Can be omitted or set to null if no maximum'time''hh:mm' string format (24-hour clock)minValue : minimum value of time expected. Can be omitted or set to null if no minimummaxValue : maximum value of time expected. Can be omitted or set to null if no maximum'timestamp''YYYY-MM-DDThh:mmZ' string format, normalized to the UTC timezone (denoted by Z)'YYYY-MM-DDThh:mmZ' format, or as a Date object.minValue : minimum value of time stamp expected. Can be omitted or set to null if no minimummaxValue : maximum value of time stamp expected. Can be omitted or set to null if no maximumApp authors will be able to incorporate custom widgets in rules, as follows:
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:
'required': For data widgets, allows the app author to ensure that a value is collected. Requiredness will be enforced beyond the UI; the integrity of the data will be enforced when it is submitted to the server'title': This is used in various contexts to allow for editing and display of the name of a widget instance'seenInOverview': Allows the app author to decide if the widget's data should be displayed in the View Data page.'range': This will add a Range with Min and Max properties to the property panel and allow an app author to specify min and max value. This property can be used with widgets with datatype of number, time, date and timestamp..'format': This will add a Format property to the property panel and allow an app author to specify a format for the string. This property can be used by widgets with datatype of string only.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 last substring of the widget definition's id. For example, a widget with an id of 'example_YesNo' 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.
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:
id: (required) uniquely identifies this property for this widgetlabel: (required) the property's labelpropType: (required) one of:
'string': rendered as a textbox'string-multiline': rendered as a textarea'enum': rendered as a dropdown. must be accompanied by a values attribute (see example below)'boolean': rendered as a checkbox'number' : rendered as a number input, for any number'integer' : rendered as a number input, for integers only'customOptions' : see belowvalues: required if propType is 'enum' (see example below)defaultValue : (optional) the property's default valueconstraints : (optional)
min: (optional) minimum allowed property value for numbersmax: (optional) maximum allowed property value for numbersExample:
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 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.
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.
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'}];
},
...
};
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.
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:
context: an object containing useful meta-data, including:
locale: the locale of current page. This is useful for displaying messages in the correct language or for dealing with locale preferences (ex. number formatting)mode: one of 'design' (authoring), 'preview' (previewing), or 'run' (running app). The widget's behaviour may need to be tailored based on the context, for example disabling some behaviours in 'design' mode.domNode: The parent DOM node into which the widget's DOM must be placed. The custom widget code must not manipulate the parent node or anything outside of it.initialProps: These will be the initial set of property values as chosen by the app author.eventManager: for triggering events. For example, eventManager.fireEvent('onChange')The returned object is expected to supply the following functions:
getValue(): required for data widgetssetValue(value): required for data widgetssetProperty(propName, propValue): required for all widgetsgetDisplayTitle(): (optional) - for display widget's title in various parts of the UIsetDisabled(isDisabled): (optional) - to tailor the widget's behaviour when disabled and enabledsetErrorMessage(errorMessage): (optional) - for the widget to report validation errors. errorMessage will be null if the data is valid.setRequired(isRequired): (optional) - to tailor the widget's behaviour when the data is required, or not required.getOptions(): (optional) - see "Widgets with Options"validateValue(value): (optional) - see "Validation" belowgetJSAPIFacade(): optional. Returns an object that supplies additional custom functions that will be available to app authors to use in their custom JavaScript. Special care must be taken to ensure that app authors have a limited range of possibilities and cannot take over the whole page with their custom JavaScript. When Leap's secure sandbox mode is enabled (secureJS=true), an author's custom JavaScript cannot access any variables prefixed with a double-underscore (see full example below).The widget creator is free to decide how they want to code and manage the widget instance internally.
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:
null - indicates the value is validIt 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 contraints 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.
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 widget attributes are globalizable:
labeldescriptioncategory > labelproperties > (property) > labelproperties > (property) > defaultValueproperties > (property) > options > (option) > titleCustom 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 { ... };
}
}
The widget's version must follow "Semantic Versioning" (semver.org) practices of MAJOR.MINOR.PATCH. The following behaviours are expected:
PATCH increment (ex. 1.0.0 > 1.0.1)
MINOR increment (ex. 1.0.1 > 1.1.0)
MAJOR increment (ex. 1.1.0 > 2.0.0)
Note: This Custom Widget API will also follow "semver" practices.
The exact techniques for upgrading widgets from one major version to the next has not yet been established. The intention is to solicit feedback from the community on how best to achieve this.
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.
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 jeporadize the security of the product and break your custom widgets in future product releases.
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".
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).
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>
string value (ex. JSON). There is no mechanism to handle customized rendering of this value in some parts of the product (ex. Print View), or to customize searching/filtering based on the intricacies of the complex value.See Acme_PageNavHeader_Widget.js
Other examples will be coming soon to https://github.com/HCL-TECH-SOFTWARE
See
Acme_YesNo_Widget.js
Other examples will be coming soon to https://github.com/HCL-TECH-SOFTWARE
Coming soon to https://github.com/HCL-TECH-SOFTWARE