Aggregation Widgets
A custom widget with a datatype of aggregation is a special type of data widget that can achieve the use-case of a repeating section.
The section is a sub-form which the author can design as they wish, with required fields and others.
The end-user can then create and fill multiple instances of that section, shown sequentially on the page. For example, the widget could be used by the app author to ask for a list of vehicles owned by the end-user, with maker, model, and year for each vehicle.-
placeDesignForm(formNode)
: When the Leap app author places a repeating section widget on the canvas, Leap will create a sub-form (ie.formNode
) for the app author to add fields to. Leap will call this function for the sub-form to be placed into the custom widget's DOM. -
addEntry(entryNode, entryId)
: When a new item is added to the custom widget's BOL, Leap will instantiate the corresponding sub-form (ie.entryNode
) and pass it to this function to be placed into the custom widget's DOM.entryId
is a transient UUID that can be used by the custom widget to keep track of entries, if needed, for the current user session. -
removeEntry(entryNode, entryId)
: When an item is removed from the custom widget's BOL, Leap will call this function to ensure the corresponding sub-form (ie.entryNode
) is removed from the custom widget's DOM.
formNode
and entryNode
(and their ancestors), should not be manipulated
or inspected in any way; treat each as a black-box.Example
This specialized widget is best described with an example:
acme.myRepeatingSection = {
id: "acme.RepeatingSection",
version: "1.0.0",
apiVersion: "1.0.0",
label: {
"default": "ACME Repeating Section",
},
description: {
"default": "ACME Repeating Section",
},
datatype: {
type: "aggregation"
},
category: {
id: "acme.sampleWidgets",
label: {
"default": "ACME Samples",
}
},
iconClassName: "acmeRepeatingSectionIcon",
builtInProperties: [{ id: "title" }],
properties: [
{ id: "explanationText", propType: "string", label: { "default": "Explanation Text" }, defaultValue: { "default": "Some default explanation text" } },
{ id: "minEntries", propType: "number", label: { "default": "Min Entries" }, defaultValue: 0 },
{ id: "maxEntries", propType: "number", label: { "default": "Max Entries" }, defaultValue: 10 },
],
// initialize widget in the DOM, with initial properties and event callbacks
instantiate: function (context, domNode, initialProps, eventManager) {
const widgetInstance = {
_disabled: false,
_mode: null, // 'design', 'preview', or 'run'
_rootNode: null,
_titleNode: null,
_entriesNode: null,
_addBtn: null,
_eventManager: null,
_formDesignNode: null,
_minEntries: null,
_maxEntries: null,
// internal custom mechanics for changing widget props
_setProp: function ({ propName, propValue }) {
switch (propName) {
case "title":
this._titleNode.innerHTML = acme.makeHTMLSafe(propValue);
break;
case "minEntries":
this._minEntries = propValue;
break;
case "maxEntries":
this._maxEntries = propValue;
break;
default:
// ignore
break;
}
this._updateUI();
},
// internal method for creating and initializing the widget
_init: function (context, domNode, initialProps, eventManager) {
this._mode = context.mode;
this._businessObjectList = context.BOA;
this._eventManager = eventManager;
const widgetHTML = `
***replace the parenthesis "(" ")" in this part with less than and greater than symbol respectively***
(div class="acme-rs")
(div class="acme-rs-title") (/div)
(div class="acme-rs-form-design")**sub-form design will go here**(/div)
(div class="acme-rs-entries") **entries will go here** (/div)
(div class="acme-rs-prompt")(/div)
(div (button class="acme-rs-add-btn")Add Entry(/button) (/div)
(/div)
`;
domNode.innerHTML = widgetHTML;
this._rootNode = domNode.firstChild;
this._titleNode = domNode.querySelector(':scope .acme-rs-title');
this._formDesignNode = domNode.querySelector(':scope .acme-rs-form-design');
this._entriesNode = domNode.querySelector(':scope .acme-rs-entries');
this._formDesignNode.style.display = this._mode === 'design' ? '' : 'none';
this._entriesNode.style.display = this._mode === 'design' ? 'none' : '';
this._promptNode = domNode.querySelector(':scope .acme-rs-prompt')
this._addBtn = domNode.querySelector(':scope button');
this._addBtn.style.display = this._mode === 'design' ? 'none' : '';
this._addBtn.addEventListener('click', () => {
// use documented JavaScript API to add a new data entry
const bo = this._businessObjectList.createNew();
this._businessObjectList.add(bo);
});
Object.keys(initialProps).forEach((propName) => {
this._setProp({ propName: propName, propValue: initialProps[propName]
});
});
},
_updateUI: function () {
if (this._mode === 'design') {
this._promptNode.innerHTML = 'Drag and drop some widgets above';
} else {
this._addBtn.disabled = this._disabled || this._businessObjectList.getLength() >= this._maxEntries;
this._promptNode.innerHTML = 'No entries';
this._promptNode.style.display = this._businessObjectList.getLength() > 0 ? 'none' : '';
}
this._entriesNode.querySelectorAll('.acme-rs-delete-btn').forEach((btn) => {
btn.disabled = this._disabled;
});
},
placeDesignForm: function (formNode) {
return this._formDesignNode.appendChild(formNode);
},
addEntry: function (entryNode, entryId) {
const entryContainer = document.createElement('div');
entryContainer.className = 'acme-rs-entry';
entryContainer.id = `${entryId}_container`;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'acme-rs-delete-btn';
deleteBtn.innerHTML = 'Delete';
deleteBtn.setAttribute('data-entry-id', entryId);
entryContainer.appendChild(deleteBtn);
deleteBtn.addEventListener('click', () => {
// use documented JavaScript API to remove data entry
const bo = this._businessObjectList.getById(entryId);
this._businessObjectList.remove(bo);
});
entryContainer.appendChild(entryNode);
this._entriesNode.appendChild(entryContainer);
this._updateUI();
},
removeEntry: function (entryNode, entryId) {
const entryContainer = document.getElementById(`${entryId}_container`);
entryContainer.removeChild(entryNode);
entryContainer.remove();
this._updateUI();
},
getValue: function () {
// must be present, but is not needed for 'aggregation' widgets
},
setValue: function (val) {
// must be present, but is not needed for 'aggregation' widgets
},
setDisabled: function (disabled) {
this._disabled = disabled;
this._updateUI();
},
// called when properties change in the authoring environment, or via JavaScript API
setProperty: function (propName, propValue) {
this._setProp({ propName, propValue });
}
};
widgetInstance._init(context, domNode, initialProps, eventManager);
return widgetInstance;
}
}