A widget allows combining multiple information into one graphical element placed within the page, facilitating a specific type of user-application interaction, it appears as a visible part of the application’s user interface and is rendered by the front-end application engine.
Examples of widgets are:
-
Line/Bar chart diagrams.
-
Product-specific schema representations.
-
Data visualization into legacy diagrams.
In the case the built-it widgets are not enough, Servitly allows defining custom widgets that can be used within pages to cover particular needs in displaying data.
A widget is made with JavaScript, HTML, and CSS loaded into the Servitly front-end and executed directly inside the user browser. This allows system integrators to create a custom user experience and so provide a single solution covering all the manufacturer’s needs.
Components are executed on the client-side, which means that the underlying device hardware may affect the way a component is executed and rendered.
The following section describes how to develop a custom widget, by using the predefined services and classes provided by Servitly.
Class declaration
Custom widgets can be defined by declaring a service class (JavaScript) with a predefined set of methods that are invoked by the front-end application engine during the page computation.
For instance, this is the sample HTML template fragment relative to a custom widget named MyCustomWidget.
<my-custom-widget [title]="'My Values'" [config]="{'par1':'fooBar'}" [inputs]="{'startDate': 'periodStart', 'endDate': 'periodEnd'}> <metric name="Temperature"></metric> <metric name="Humidity"></metric>
<property name="properties.sensorType"></property> </my-custom-widget>
And this is the underlying service class of the widget.
class MyCustomWidget extends AbstractWidget { constructor(config) { super(config); } onInit(context) { let widget = this;
return super.onInit(context).then(function() {
widget.setLoaderVisible(true);
// do initialization stuff
widget.setLoaderVisible(false);
}).catch(error => {
console.error(error);
widget.showError({ "message": "Error initializing diagram" });
widget.setLoaderVisible(false);
}); } onDataUpdated(context) { } onInputUpdated(context) { } onDestroy(context) { } } // registers the widget within the application. window.componentRegistry.registerWidget({'tagName': 'my-custom-widget',
'widgetClass': MyCustomWidget
});
The AbstractWidget class provides some helpful methods for simplifying widget definition:
-
loadMetricDefinitions(context, resolve, reject): enriches the list of metrics with all the model information.
-
setLoaderVisible(visible): shows or hides the overlay spinner during data loading.
-
showMessage(text, severity): displays a message with a colored badge with a certain severity (info, warning, danger).
-
showNoDataAvailable(): displays the No data available message.
-
showError(error): displays an error message within a colored badge.
-
hideMessage(): hides the displayed informational message.
When the page template is processed (on the client-side), for each widget tag, the associated class is instantiated, and the base methods are called during page computation and user interaction:
-
constructor(config): called when the widget instance is going to be created, it receives the config JSON object providing the same information defined within the config input attribute on the widget element.
-
onInit(context): called when the widget is going to be displayed. This method must return a Promise, in this way you can also handle asynchronous code (e.g. library loading, API requests)
-
onDataUpdated(context): called when a new value is available for the subscribed metrics or an update has been performed on the underlying thing.
-
onInputUpdated(context): called when one of the inputs has been changed (e.g. a new filtering period is selected within the page).
-
onDestroy(context): called when the widget is going to be destroyed. This method allows disposing of allocated graphical resources (e.g. amChart4 diagrams).
On the widget instance, you can access the config JSON object providing all the information you have configured into the template.
let widget = this; // required to access widget instance into closures.
widget.config.par1 // reads the value of "par1" configured into the template.
The context JSON object provided access to the following information:
-
metrics: the array of metrics defined by using the tag metric.
-
properties: the array of properties defined by using the tag property.
-
thing: the context thing object (if present)
-
user: the currently logged-in user.
-
location: the parent context location (if present).
-
customer: the parent context customer (if present).
-
partner: the context partner (if present).
-
thingDefinition: the thing-definition associated with the context thing (if thing present).
-
data: the map containing the last values of the metric and properties.
-
inputValues: the map of input name and value pairs as defined within the inputs attribute of the widget.
-
htmlElement: the HTML element which can be used to append the widget graphical interface.
Third-party library loading
If the widget needs third-party libraries to render its content, for instance, a specific graphic library (e.g. SVG.js, amCharts4.js), the onInit method must return a promise, in this way, the application engine, wait for the library to be loaded before continuing.
It is strongly recommended to use promises to perform asynchronous tasks to be done during the widget initialization.
onInit(context) {
let widget = this;
return super.onInit(context).then(function() {
return new Promise((resolve, reject) => {
loadResources(["https://cdn.amcharts.com/lib/4/core.js", "https://cdn.amcharts.com/lib/4/charts.js"], resolve);
}).then(function() {
// create diagram
}).then(function() {
// load data
}).catch(error => {
console.error(error);
widget.showError({ "message": "Error initializing diagram" });
widget.setLoaderVisible(false);
});
});
}
The complete code of a custom widget based on the amCharts4 library can be downloaded here.
Dealing with page inputs
In case the widget must be refreshed on a page input update (e.g. a period field is changed), the widget must:
-
declare the inputs attribute to define which are the page variables the widget is listening to
-
implement the onInputUpdated(context) method to refresh data according to the new input values
This is the sample HTML fragment:
<period-field name="Period" startVariable="periodStart" endVariable="periodEnd" defaultPeriodValue="LAST_7_DAYS"> </period-field> <widget name="MyCustomWidget" [title]="'My Values'" [inputs]="{'start': 'periodStart', 'end': 'periodEnd'}> <metric name="Temperature"></metric> </widget>
This is the sample Javascript fragment:
onInputUpdated(context) { let startTime = context.inputValues.start; let endEnd = context.inputValues.end; // do data refresh }
Loading metric data
By default, the widget loads the last value for each referenced metric, and you can access metric values from the context object.
context.data["temperature"]
In case you need to load more values or use aggregation, you need to make explicit API requests, that can be made by using the ServitlyClient class.
Template
<my-custom-widget>
<metric name="temperature"></metric>
</my-custom-widget>
Widget Class
class MyCustomWidget extends AbstractWidget { constructor(config) { super(config); } onInit(context) { let widget = this;
let promises = [];
promises.push(super.onInit(context));
return Promise.all(promises).then(function() {
// loads the metric definitions referenced by the widget
return widget.loadMetricDefinitions(context);
}).then(function() {
widget.loadData(context);
}).catch(error => {
console.error(error);
}); }
loadData(context) {
let metric = context.metrics[0];
let params = {
"startDate" : 1234,
"endDate" : 456,
"limit": 1, // optional (use 1 if you need just the last value)
"aggregation": "AVG_DAYS_1" // optional
};
servitlyClient.getMetricValues(context.thing.id, metric.name, params, function(data) {
// handle metric data
}, function(error){
console.error(error)
});
} onDataUpdated(context) { } onInputUpdated(context) { } onDestroy(context) { } }
The AbstractWidget.loadMetricDefinitions(context) function can be used to enrich the template metrics, accessible in the context.metrics object, with the referenced metric definitions. This can be helpful, in case you need to display data by using metric information not present in the template (e.g. dictionary, ranges).
If you have multiple metrics to load, you can create multiple promises to be solved in parallel.
let promises = [];
context.metrics.forEach(m => {
promises.push(new Promise(function(resolve, reject) {
servitlyClient.getMetricValues(context.thing.id, m.name, params, function(data) {
resolve(data);
}, reject);
}));
});
Promise.all(promises).then(function(data) {
// handle metrics data
});
Embedded Period Picker
In case you need a period picker directly embedded into your widget, you can leverage a predefined method of the AbstractWidget class.
let options = { "defaultPeriodValue": "TODAY", "period": ["TODAY", "LAST_7_DAYS", "CUSTOM"] }; let callback = function(period) { widget.period = period; widget.loadData(context); } let periodPicker = widget.addPeriodPicker(element, options, callback, context); let period = periodPicker.getCurrentPeriod();
The "element" parameter is the HTML element in which you want to insert the period picker.
The period object contains these properties.
{
"start": 1719405079227,
"end": 1719491479227,
"name": "LAST_7_DAYS"
}
Optionally you can force the CUSTOM selected period by calling this method.
let period = {
"start": 1719405079227,
"end": 1719491479227
}
periodPicker.setCurrentPeriod(period);
When invoked, to avoid infinite loops, the callback function is ignored.
Comments
0 comments
Please sign in to leave a comment.