Editing a Widget

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. A widget is included into a frame with header, where the title (optional) can be displayed, and the body including the widget main content.

Examples of widgets are:

  • Line/Bar chart diagrams.

  • XY chart diagrams.

  • Product schemes or other diagrams.

A widget is made with JavaScript, HTML, and CSS loaded into the Servitly front-end and executed directly inside the user browser.
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) which must have a predefined set of methods that are invoked by the front-end application engine during the page computation.

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 execution (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 object.
    See the Context.data object below to read new metrics data.

  • onInputUpdated(context): called when one of the inputs has been changed (e.g. a new filtering period is selected within the page).
    See the Context.inputValues object below to read inputs data.

  • onDestroy(context): called when the widget is going to be destroyed.
    This method allows disposing of allocated graphical resources (e.g. chart diagrams).

When writing code for your widget, you can use the following objects already available in the context:

  • appUtils: an object providing a set of methods to access and interact with the page.

  • servitlyClient: an object providing methods to access the Servitly backend API.

You can extend the AbstractWidget class which provides some utility methods facilitating the implementation:

  • loadMetricDefinitions(context, resolve, reject): enriches the list of metrics with all the model information.
    This function enriches 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).

  • setLoaderVisible(visible): shows or hides the overlay spinner during asynchronous 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.

  • hideMessage(): hides the displayed message.

  • addPeriodPicker(container, options, callback, context): create a period picker embedded in the widget and placed under the given container element.

Here is an example of a widget class extending the AbstractWidget.

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) {
       // update widget with fresh metric data
    }

    onInputUpdated(context) {
       // recompute widget with new inputs
    }

    onDestroy(context) {
       // dispose allocated resources
    }
}

Config

The config JSON object provides access to what has been configured into the template.

<my-custom-widget [config]="{property1: true, property2:123, property3: 'abc', property4: ['foo', 'bar']}">
   <metric name="temperature"></metric>
   <metric name="humidity"></metric> 
</my-custom-widget>
{
	"property1": true,
	"property2": 123,
	"property3": "abc",
	"property4": [
		"foo",
		"bar"
	]
}
this.config.property1  // reads the property1 as boolean
this.config.property2  // reads the property1 as integer
this.config.property3  // reads the property1 as string
this.config.property4  // reads the property1 as array

Context

The context JSON object provided access to the following information:

  • metrics: the array of metrics defined under the widget by using the tag metric.

  • properties: the array of properties defined under the widget by using the tag property.

  • thing: the thing object in the navigation context (if present)

  • user: the currently logged-in user.

  • location: the location object in the navigation context (if present).

  • customer: the customer object in the navigation context (if present).

  • partner: the partner object in the navigation context (if present).

  • thingDefinition: the thing-definition object 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.

Updating the HTML

Within the context you can access the htmlElement the widget is based on.
By using jQuery you can modify the HTML  

Third-party library loading

In case the widget needs third-party libraries to render its content, for instance, a specific graphic library (e.g. SVG.js, amCharts4.js), you can do it in the onInit method which returns a promise, the application engine waits for the promise to be completed before continuing.

onInit(context) {
 let widget = this; 
 return super.onInit(context).then(function() {
    return new Promise((resolve, reject) => {
       loadResources(["https://www.acme.com/lib_ABC.js", "https://www.acme.com/lib_ABC.css"], resolve);
    }).then(function() {
       // create diagram 
    }).then(function() {
       // load data 
    }).catch(error => {
      console.error(error);
      widget.showError({ "message": "Error initializing diagram" });
      widget.setLoaderVisible(false);
    });
 });
}

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:

  1. declare the inputs attribute to define which are the page variables the widget is listening to;

  2. implement the onInputUpdated(context) method to refresh data according to the new input values.

For instance considering the following template fragment:

<period-field-v2 id="period-1" defaultPeriodValue="LAST_7_DAYS"></period-field-v2>
<search-field id="query-1"></search-field>

<widget [title]="'My Values'" [inputs]="{'period': 'period-1', 'query': 'query-1'}">
   <metric name="Temperature"></metric>
</widget>

This is the sample JavaScript fragment:

onInputUpdated(context) {
   let query = context.inputValues.query;
   let period = context.inputValues.period;
   let startTime = period.start;
   let endEnd = period.end;

   // refresh data 
}

Loading metric data

By default, the widget loads the last value for each referenced metric, and the each time a new metric value is available the onDataUpdated(context) method is invoked.
You can always access metric values from the Context.data object.

// access a metric vale by metric name
   let value = context.data["temperature"]

// iterate on all metrics
context.metrics.forEach((m) => {
   let value = context.data[m.name]
});

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 always available in the JavaScript context.

Here below you can find an example of template and widget class.

<my-custom-widget>
 <metric name="temperature"></metric>
</my-custom-widget>
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) {}
}

If you need to load data from multiple metrics, 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 widget = this;
let periodPicker = widget.addPeriodPicker(
     containerElement,
     {
        defaultPeriod: "TODAY",
        enabledPeriods:  ["TODAY", "LAST_7_DAYS", "CUSTOM"],
        scrollable: true,
     },
     function (period) {
        widget.period = period;
        widget.loadData(context);
     },
     context);

The "containerElement"  is the HTML element in which you want to insert the period picker, which is accessible starting from the widget main element Context.htmlElement.
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 period2 = {
 "start": 1719405079227,
 "end": 1719491479227
}
periodPicker.setCurrentPeriod(period2);