Developing View Widgets
This tutorial will guide you through:
- Creating a widget
- Widget configuration
- Accessing data (server to widget)
- Widget layout handling
- Integrating 3rd party libraries
Step 1: Creating a Widget
First we present how a simple "Hello World!" widget is constructed.
Create a JavaScript file for your widget, and place it in a folder in the SourceRoot (/view/widgets). Name it hello_world.js. Enter the following contents.
{
const { Components } = mia_require("componentbase/common");
const { cWidgetBase } = mia_require("widgetbase/widgetbase");
class HelloWorld extends cWidgetBase
{
constructor(parentelement, config)
{
super(parentelement);
this.Properties.Read(config);
parentelement.textContent = "Hello World!";
}
}
Components.Add(HelloWorld, "Demo.JS.HelloWorld")
}import { Components } from "componentbase/common";
import { cWidgetBase, IWidgetBaseConfig } from "widgetbase/widgetbase";
class HelloWorld extends cWidgetBase
{
constructor(parentelement : HTMLElement, config : IWidgetBaseConfig)
{
super(parentelement);
this.Properties.Read(config);
parentelement.textContent = "Hello World!";
}
}
Components.Add(HelloWorld, "Demo.TS.HelloWorld");Let's go through what we have here:
mia_require- Imports the exports of a moduleComponents- The widget registry
cWidgetBase- The base class for all widgets- In order for the widget to work on a Dashboard it has to extend
cWidgetBase
- In order for the widget to work on a Dashboard it has to extend
constructor- Only these two parameters in this particular order are supported when widgets are used in Dashboards
parentelement- the element given to the widget to place it's contentconfig- the initial property values for the widget- cWidgetBase constructor is called with
super, withparentelementas the required parameter - The initial property values are read with calling
this.Properties.Readin the widget. - We create content in the container element – in this case, setting the content to "Hello World!".
Components.Addwe register the component class with unique name"Demo.HelloWorld".
Widget Modules
In the previous section, we created the JavaScript code for the widget. Next, we need to create a module definition so that it can be found in the widget library. This is done by creating a .mdw.json file for the widget (Module Definition for Widget). Place this file next to your JavaScript file.
{
"Name": "Hello World",
"Category": "Demo Examples",
"Class": "Demo.HelloWorld",
"Icon": "hello_world.png",
"Sources": ["$/hello_world.js"]
}This file defines the widget:
Name: The name of the widget.Category: The category of the widget in the widget library.Class: Defines which component class to instantiate. This should be the same as the second parameter ofComponents.Addin the JavaScript code.Icon: The icon shown for the widget in the widget library.Sources: The source files required by the widget. These can be JavaScript (.js) files or cascading style sheets (.css).$/signifies the location of the.mdw.jsonfile.
Widget Configuration
Next we want to be able to configure our widget. Simply printing "Hello World!" is not quite enough. Next, we add some properties that can be used to configure the widget.
You can add new properties to your component with this.Properties.Add of cComponentBase. It takes the following parameters:
name- Name for the property (should be unique in the component)type- The type of the property, affects how to value of property is stored, and how it is presented in the Dashboard Editor.options- Additional attributes for the property, including:DefaultValue- The initial value for the property.IsBrowsable- If true, the property is visible in Dashboard Editor.IsSerializable- If true, the property's value is stored in the dashboard configuration.- Only value differing from
DefaultValueis stored (with some exceptions).
- Only value differing from
const { cPropertyType } = mia_require("componentbase/propertytype");
…
class HelloWorld extends cWidgetBase
{
Message = this.Properties.Add("Message", cPropertyType.Text, {
DefaultValue: "Hello World!",
IsBrowsable: true,
IsSerializable: true
});import { cPropertyType, IProperty } from "componentbase/common";
…
class HelloWorld extends cWidgetBase
{
readonly Message : IProperty<string> = this.Properties.Add("Message", cPropertyType.Text, {
DefaultValue: "Hello World!",
IsBrowsable: true,
IsSerializable: true
});
…In the widget or component, you can use the Get and Set functions to read and write property values.
this.Message.Set("A new value for the property!");this.Message.Set("A new value for the property!");Changes in a component's properties can be monitored with the PropertyChanged event. You can add a listener to changes on any property in the component with this.Properties.AddListener, or you can add a listener for individual property with OnChange.
const update = () => {
const message = this.Message.Get();
content.textContent = message;
};
this.Message.OnChange(update);
this.Properties.Read(config);const update = () => {
const message : string = this.Message.Get();
content.textContent = message;
};
this.Message.OnChange(update);
this.Properties.Read(config);The method this.Properties.Read must be used in the widget constructor to read initial property values from the standard config parameter. When the widget is created or loaded in the dashboard, few of the properties of cWidgetBase need to be initialized with this command. At the same time (when loading a dashboard) any of the properties of the widget itself will also be initialized with the values stored in the dashboard configuration.
Technically speaking, the config parameter contains key-value pairs, where the key is the name of the property and the value is the value assigned to the property. For example, setting the value "Good afternoon!" to the Message property is done like this:
this.Properties.Read({ Message: "Good afternoon!" })this.Properties.Read({ Message: "Good afternoon!" })Accessing Data
How do we fetch data from the server into our widget? To achieve this, we use the View Data API, which is available for widgets.
The function GetConnection, when called without parameters, returns the server connection to the default data source. This connection provides the standard server API functions.
The function FetchClassData fetches class instance data using the specified mask and mask arguments. This operation is asynchronous; the widget needs to wait for the server to respond before rendering. The response contains the Data array, which contains property values in the same order as in the request.
By default, the data returned by FetchClassData returns formatted strings. By using the RAW: or LOW: prefix for the property name in the request, it returns raw or low-level values respectively.
In the following example, we fetch the values of four properties from the server and render them.
const { GetConnection } = mia_require("client/connection");
const dataProperties=["DisplayName", "ServiceStatus", "CPU", "MEM_Private"];
…
fetch = async () =>
{
const connection = GetConnection();
const result=await connection.FetchClassData(
"Path_Builtin.SysDiag.Process", dataProperties, -1,
"DisplayName LIKE ?", ["RTDB*"]
).catch(this.renderfail);
if(result) this.render(result);
};
render(result)
{
this.content.innerHTML = "";
for(let i = 0;i < result.Data.length;i += dataProperties.length)
{
const row = this.content.insertRow();
for(let j = 0;j < dataproperties.length;j++)
{
const cell = row.insertCell();
cell.innerHTML = result.Data[i + j];
}
}
}import { GetConnection, IConnection } from "client/connection";
import { IFetchClassDataResult } from "client/servertypes";
const dataProperties : string[] = ["DisplayName", "ServiceStatus", "CPU", "MEM_Private"];
…
private fetch = async () =>
{
const connection : IConnection = GetConnection();
const result = await connection.FetchClassData(
"Path_Builtin.SysDiag.Process", dataProperties, -1,
"DisplayName LIKE ?", ["RTDB*"]
).catch(this.renderfail);
if(result) this.render(result as IFetchClassDataResult);
};
private render(result : IFetchClassDataResult)
{
this.content.innerHTML = "";
for(let i : number = 0; i < result.Data.length; i += dataProperties.length)
{
const row : HTMLTableRowElement = this.content.insertRow();
for(let j : number = 0; j < dataProperties.length; j++)
{
const cell : HTMLTableCellElement = row.insertCell();
cell.innerHTML = result.Data[i + j] as string;
}
}
}Data Access Components
Data access components are components that allow the user to configure data access in the UI, including the dashboard editor. These can be stored in a widget's properties.
In the following example, we create a data access component of the class cCurrentValueAccessor as a property of the widget.
const { cCurrentValueAccessor } = mia_require("valueaccessors/currentvalueaccessor");
…
Data = this.Properties.Add("Data", cPropertyType.Component, {
ComponentType: "ABB.Mia.cCurrentValueAccessor",
DefaultValue: new cCurrentValueAccessor(),
IsSerializable: true, IsBrowsable: true, IsReadOnly: true //<-- constant
});
…
this.Properties.Read(config);
const accessor = this.Data.Get();
accessor.Changed.AddListener(() =>
{
const vr = accessor.ValueRecord.Get();
content.innerHTML = vr.GetValueString() + " " + (accessor.Unit.Get() || "");
});import { cCurrentValueAccessor } from "valueaccessors/currentvalueaccessor";
…
readonly Data : IProperty<cCurrentValueAccessor> = this.Properties.Add(
"Data", cPropertyType.Component, {
ComponentType: "ABB.Mia.cCurrentValueAccessor",
DefaultValue: new cCurrentValueAccessor(),
IsSerializable: true, IsBrowsable: true, IsReadOnly: true //<-- constant
});
…
this.Properties.Read(config);
const accessor : cCurrentValueAccessor = this.Data.Get();
accessor.Changed.AddListener(() =>
{
const vr : cValueRecord = accessor.ValueRecord.Get();
content.innerHTML = vr.GetValueString() + " " + (accessor.Unit.Get() || "");
});The ComponentType attribute contains the component registry name of the component. This is necessary in case we allow the user to create an new instance from a chosen component class. The component class itself and any of its inheritors can be chosen by the user. However, in this example this attribute is only informational as IsReadOnly prevents the user from changing the component class.
cCurrentValueAccessor triggers the Changed event each time a new value is available. The accessor's ValueRecord property contains a value record (cValueRecord) consisting of time, value and status. The value record's GetValueString returns the value as formatted string.
In addition, the value accessor's properties contain item metadata such as minimum and maximum limits, unit, display name etc.
Committing data
You can also use the current value accessor to commit data into the database. You can commit a current value by calling the cCurrentValueAccessor's CommitValue function. The following example updates the current value:
const commit = (value) =>
const accessor = this.Data.Get();
accessor.CommitValue(value, // Change the item value
() => { console.log("Commit OK"); },
(err) => { console.log("ERROR:", err); }
);
};const commit = (value : number) =>
const accessor : cCurrentValueAccessor = this.Data.Get();
accessor.CommitValue(value, // Change the item value
() => { console.log("Commit OK"); },
(err : string) => { console.log("ERROR:", err); }
);
};You can provide the CommitValue function callbacks for successful and failed commits.
Context handling
The interface IContextHanding provides the Data Context API of the widget. AddHandler registers a callback for handling data context events. Many of the default Data Access components also implement the context API. In the following example, we relay the context to be handled by the value accessor by calling Context.Fire.
…
this.Context.AddHandler((ctx) =>
{
accessor.Context.Fire(ctx);
});…
this.Context.AddHandler((ctx : IContext) =>
{
accessor.Context.Fire(ctx);
});Widget Layout Handling
After the widget is based from the widget library onto the dashboard in the dashboard editor, the widget's parent element's size and position are controlled by the dashboard's layout. It is expected that the widget does not overflow its reserved size.
It is up to the widget's developer to decide how the widget handles its scaling. As the developer, you might want to scale the widget's entire contents to fit any possible size, or use scrollbars to let your user browse overflowing content. This can be done with CSS styles, but sometimes it is useful to know that the widget's size is changed in the JavaScript code too. All widgets contain a Refresh event, which is triggered each time the widget's size or position is changed.
…
this.RefreshEvent.AddListener(() => {
canvas.width = parentelement.clientWidth;
canvas.height = parentelement.clientHeight;
//OR even:
this.SetCorrectCanvasSize(canvas, ctx, parentelement);
draw();
});
……
this.RefreshEvent.AddListener(() => {
canvas.width = parentelement.clientWidth;
canvas.height = parentelement.clientHeight;
//OR even:
this.SetCorrectCanvasSize(canvas, ctx, parentelement);
draw();
});
…In the above example, we listen the Refresh event and resize a HTML canvas element contained within the widget, as its width and height are not automatically refreshed when its parent element is resized. The widget base also contains some helper functions for this use case; for example, the function SetCorrectCanvasSize automatically updates a canvas' size to match another element, taking into account the DPI settings of the client device and web browser.
Integrating 3rd Party Libraries
3rd party code can be loaded the same way as any widget sources through the module loader, by defining them in the Sources of the widget. If the 3rd party code involves compilation, transpiling or packaging, the result has to be something that can be integrated on any web page and can be accessed and controlled through the widget's constructor function.
In most cases, the data returned by the Data APIs have to be transformed into a format that is understood by the 3rd party component. A simple example utilizing heatmap.js:
widget.mdw.json:
{
"Name": "Heatmap demo",
"Category": "Demo Examples",
"Class": "Demo.Heatmap",
"Icon": "icon.png",
"Sources": ["$/lib/heatmap.js", "$/js/heatmap_widget.js"]
}heatmap_widget.js:
…
const heatmap = h337.create({
container: parentelement
});
…
heatmap.setData({
max: 5,
data: [{ x: 10, y: 15, value: 5}, ...]
});
…Integrating React components as View Widgets
This documentation contains a separate page showing proof of concept on how to integrate React components as View widgets.
Updated 5 months ago
