This API provides a mechanism to incorporate custom widgets into HCL Leap and HCL Domino Leap.
Table of Contents:
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.
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
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 {
...
};
}
};
}
};
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 characters. 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: 50
format
- TBDExample:
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: 2
minValue
: 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 formatdefaultValue
of 'today'
is supported (note: not enforced during submission)'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.'customCssClasses'
: Allows the app author to provide custom class names for additional styling of individual widgets.'customAttribute'
: Allows the app author to assign a meaningful programmatic value to a particular widget.'dataLabel'
: Allows the app author to specify short title for a data widget instance, used typically as a column header.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.
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 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.
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:
label
description
category > label
properties > (property) > label
properties > (property) > defaultValue
properties > (property) > options > (option) > title
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 { ... };
}
}
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.
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.
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 jeopardize the security of the product.
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
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>
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>
Download leap-custom-widgets-react-mui-master.zip
Consult the README.md
file within.
More examples coming soon to https://github.com/HCL-TECH-SOFTWARE