官方地址:https://developer.thingworx.com/en/resources/guides/create-mashup-widget-extension
Quickly Build Mashup Widget Extensions and Extend Application Functionality with the Eclipse Plugin.
Extensions enable you to quickly and easily add new functionality to an IoT solution. Mashup widget extensions can be utilized to enhance a user’s experience, your ability to develop robust applications, and make development easier as you move forward with your IoT development.
The Eclipse Plugin for ThingWorx Extension Development (Eclipse Plugin) is designed to streamline and enhance the creation of extensions for the ThingWorx Platform. The plugin makes it easier to develop and build extensions by automatically generating source files, annotations, and methods as well as updating the metadata file to ensure the extension can be imported.
These features allow you to focus on developing functionality in your extension, rather than spend unnecessary time getting the syntax and format of annotations and the metadata file correct.
Environment:
This guide may be completed in any environment that has access to the ThingWorx Foundation server. This guide is based on the Extension SDK version 8.3.
This includes both the hosted evaluation server, as well as the downloadable trial.
Before you start, ensure you have the following software installed.
Completed Example:
You are provided a complete example to use as a reference as you progress through this guide and for more insight into how steps are completed.
NOTE: If you use Gradle as the build tool, in your Eclipse application, download the Gradle STS IDE plugin from Help->Eclipse Marketplace, then restart your workspace.
Download the completed files for this tutorial: Custom Mashup Widget Example.
The MashupWidgetSamples.zip file provided to you contains a completed example of a simple Widget project and examples of more advanced widget source code. Utilize this file to see a finished example and return to it as a reference if you become stuck during this guide and need some extra help or clarification.
Keep in mind, this download uses the exact names for Entities used in this tutorial. If you would like to import this example and also create Entities on your own, change the names of the Entities you create.
To create a new extensions projects in the ThingWorx Extension Perspective, follow the steps below to get started:
The ThingWorx Extensions SDK allows for easier development and in a shorter timeframe. The SDK provides steps for creating widgets, starting with an initial setup. Follow the steps below to get started on your own widget creation.
A new folder under the /ui folder is created and contains the JavaScript and CSS files for the widget. The metadata.xml file under the configfiles directory is updated automatically. The generated JavaScript files contain a minimal implementation of the functions needed to produce a working widget.
There are scenarios in which a 3rd party JAR file might be required. None will be needed for this scenario, but take note of how to do it below.
The JAR file is added to the /lib folder and the metadata.xml file is updated automatically.
Third-party libraries, images, and other web artifacts needed for the widget should be placed in the /ui/ folder or subfolders of that location. The *.ide.js and *.runtime.js files can then reference any of those artifacts via the relative path of:
…/Common/extensions//ui//
For example, to include a third-party JavaScript library and CSS into your widget code, one would do the following:
if (!jQuery().qtip) {
$("body").append('');
$("head").append('');
}
A widget has the following lifecycle stages within the Mashup Builder. During each lifecycle stage, the specified functions on the widget are called by the Mashup Builder.
The widget is being loaded into index.html and added to the Widget toolbar/palette.
widgetProperties() - Called to get information about each widget (such as display name and
description)
widgetEvents() - Called to get information about the events each widget exposes
widgetServices() - Called to get information about the services each widget exposes
The widget is dragged onto a Mashup panel.
afterload() - Called after your object is loaded and properties have been restored from the file, but before your object has been rendered
The widget is appended to the workspace DOM element.
renderHtml() - Called to get an HTML fragment that will be inserted into the Mashup DOM element
afterRender() - Called after the HTML fragment representing the widget has been inserted into the Mashup DOM element and a usable element ID has been assigned to the DOM element holding the widget content. The DOM element is then ready to be manipulated.
The widget is resized or updated in the Widget property window.
beforeSetProperty() - Called before any property is updated
afterSetProperty() - Called after any property is updated
The widget is deleted from the mashup.
beforeDestroy() - Called right before the widget’s DOM element is removed and the widget is detached from its parent widget and deallocated. You should clean up resources (such as plugins and event handlers) acquired during the lifetime of the widget.
The [widgetname].ide.js file must implement several functions to work correctly in the Mashup Builder using its API. Widgets can declare widget properties, services, and events in functions.
Below is sample code for a widget named SimpleWidget with a bindable string property named DisplayText.
TW.IDE.Widgets.simplewidget = function () {
this.widgetIconUrl = function() {
return "../Common/extensions/MyAwesomeExtension/ui/simplewidget/SimpleWidget.ide.png";
};
this.widgetProperties = function () {
return {
name : "SimpleWidget",
description : "A simple example of Widget creation.",
category : ["Common"],
properties : {
DisplayText: {
baseType: "STRING",
defaultValue: "Hello, Awesome User!",
isBindingTarget: true
}
}
}
};
this.renderHtml = function () {
var mytext = this.getProperty('SimpleWidget Property');
var config = {
text: mytext
}
var widgetTemplate = _.template(
'
'
);
return widgetTemplate(config);
};
this.afterSetProperty = function (name, value) {
return true;
};
};
To handle the widget at runtime, you need methods to do the following:
Below is sample code of what the [widgetname].runtime.js may look like:
TW.Runtime.Widgets.simplewidget = function () {
var valueElem;
this.renderHtml = function () {
var mytext = this.getProperty('SimpleWidget Property');
var config = {
text: mytext
}
var widgetTemplate = _.template(
'
'
);
return widgetTemplate(config);
};
this.afterRender = function () {
valueElem = this.jqElement.find(".DisplayText");
valueElem.text(this.getProperty("DisplayText"));
};
this.updateProperty = function (updatePropertyInfo) {
if (updatePropertyInfo.TargetProperty === "DisplayText") {
valueElem.text(updatePropertyInfo.SinglePropertyValue);
this.setProperty("DisplayText", updatePropertyInfo.SinglePropertyValue);
}
};
};
If you have a local installation of the ThingWorx Composer, you can find examples of widgets in the Tomcat_Installation_Folder/webapps/Thingworx/Common/thingworx/widgets directory. DO NOT EDIT THESE FILES!. You will be able to mimic widgets you like to use them as a basis for new widgets. Or, just take notes on these items which will be covered more in-depth later in this guide.
You can incorporate the following features into your widgets:
You can access the full power of JavaScript and HTML in your widget code at runtime. Anything that can be accomplished using HTML and JavaScript is available in your widget.
The following APIs can be accessed by a widget in the context of the Mashup Builder:
API | Description |
---|---|
this.jqElementId | This is the DOM element ID of your object after renderHtml(). |
this.jqElement | This is the jquery element. |
this.getProperty(name) / this.setProperty(name,value) | Note that every call to this function will call afterSetProperty() if it’s defined in the widget. |
this.updatedProperties() | This function should be called anytime properties are changed in the widget so that the Mashup Builder can update the widget properties window, the connections window, and so on. |
this.getInfotableMetadataForProperty(propertyName) | If you need the infotable metadata for a property that you bound, you can get it by calling this API; it returns undefined if it is not bound. |
this.resetPropertyToDefaultValue(propertyName) | This call resets the named property to its default value. |
this.removeBindingsFromPropertyAsTarget(propertyName) | This call removes target data bindings from the propertyName. Use it only when the user has initiated an action that invalidates the property. |
this.removeBindingsFromPropertyAsSource(propertyName) | This call removes source data bindings from the propertyName. Use this only when the user has initiated an action that invalidates the property. |
this.isPropertyBoundAsTarget(propertyName) | This call returns a result that indicates if the property has been bound as a target. You can use it to determine if a property has been set or bound. |
this.isPropertyBoundAsSource(propertyName) | This call returns a result that indicates if the property has been bound as a source. You can use it to determine if a property has been bound to a target. |
Example of the Checkbox Widget’s validate() function:
this.validate = function () {
var result = [];
if (!this.isPropertyBoundAsSource('State') && !this.isPropertyBoundAsTarget('State')) {
result.push({ severity: 'warning',
message: 'State for {target-id} is not bound' });
}
return result;
}
Example of the Blog Widgets validate() function:
this.validate = function () {
var result = [];
var blogNameConfigured = this.getProperty('Blog');
if (blogNameConfigured === '' || blogNameConfigured === undefined) {
if (!this.isPropertyBoundAsTarget('Blog')) {
result.push({ severity: 'warning', message: 'Blog is not bound for {target-id}' });
}
}
return result;
}
The following widget functions are called by the Mashup Builder to control the widget’s behavior.
widgetProperties() - Returns a JSON structure that defines the properties of the widget. Listed are the possible properties of the widget:
The only required property is:
There are a number of optional properties that can be contained in the returned JSON structure.
Property | Description |
---|---|
description | A description of the widget, used for its tooltip. iconImage - File name of the widget icon/image |
category | An array of strings for specifying one or more categories to which the widget belongs (such as “Common”, “Charts”, “Data”, “Containers”, and “Components”), enabling widgets to be filtered by type/category. |
isResizable | true (default) or false |
defaultBindingTargetProperty | Name of the property to use as the data/event binding target |
borderWidth | If the widget has a border, set this property to the width of the border. This property ensures pixel-perfect WYSIWG between design and runtime. If you set a border of one pixel on the widget-content element at design time, you are making the widget two pixels taller and two pixels wider (one pixel on each side). To account for this discrepancy, set the borderWidth property to make the design-time widget the same number of pixels smaller. This property places the border inside the widget that you created and makes the width and height in the widget properties accurate. |
isContainer | true or false (default). Controls whether an instance of the widget can be a container for other widget instances. |
customEditor | The name of the custom editor dialog for entering and editing the widget configuration. The system assumes there is a dialog you created named TW.IDE.Dialogs.. |
customEditorMenuText | The text that appears on the flyout menu of the widget and the tooltip text for the Configure Widget Properties button. For example, “Configure Grid Columns.” |
allowPositioning | true (default) or false |
supportsLabel | true or false (default). If true, the widget exposes a label property used to create a text label that appears next to the widget in the Composer and at runtime. |
supportsAutoResize | true or false (default). If true, the widget can be placed in responsive containers (such as columns, rows, responsive tabs, responsive mashups). |
properties | A collection of JSON objects for the widget that describe the properties of the widget that can be modified when the widget is added to a mashup. These properties are displayed in the Properties window of the Mashup Builder - the name of each object is used as the property name and the corresponding attributes control how the property value is set. |
afterLoad() | Called after the object is loaded and properties have been restored from the file, but before the object has been rendered |
renderHtml() [required] | Returns HTML fragment that the Composer will place in the screen; the widget’s content container (e.g. div) must have a ‘widget-content’ class specified. After this container element is appended to the DOM, it becomes accessible via jqElement and its DOM element ID will be available in jqElementId |
widgetEvents() | A collection of events. |
warnIfNotBound | true or false; If true, the property will be checked by Composer to determine whether it is bound, then generate a to-do item if/when it is not. |
widgetServices() | A collection of services. |
warnIfNotBound | true or false; If true, the property will be checked by the Composer to determine whether it is bound, then generate a to-do item if/when it is not. |
afterRender() | Called after the HTML fragment is inserted into the DOM. |
beforeDestroy() | Called right before the widget’s DOM element gets removed and the widget is detached from its parent widget and delocated; this is the place in which to perform any clean-up of resources (e.g. plugins, event handlers) acquired throughout the lifetime of the widget. |
beforeSetProperty(name,value) [Mashup Builder only - not at runtime] | Called before any property is updated within Composer; this is a good place to perform validation on the new property value before it is committed. If a message string is returned, then the message will be displayed to the user, and the new property value will not be committed. If nothing is returned, then the value is assumed to be valid. |
afterSetProperty(name,value) [Mashup Builder only - not at runtime] | Called after any property is updated within Composer. Return true to have the widget re-rendered in Composer. |
afterAddBindingSource(bindingInfo) | Whenever data is bound to the widget, you will call back with this (if you implemented it … it’s optional). The only field in bindingInfo is targetProperty which is the propertyName that was just bound. |
validate() | Called when Composer refreshes its to-do list. The call must return an array of result objects with severity (optional and not implemented) and message (required) properties. The message text may contain one or more pre-defined tokens, such as {target-id}, which will contain a hyperlink that allows the user to navigate to or select the specific widget that generated the message. |
The following attributes can be specified for each property object:
Aspect | Description |
---|---|
description | A description of the widget, which is used for its tooltip. |
baseType | The system base type name - if the baseType value is FIELDNAME the widget property window displays a dropdown list of fields available in the INFOTABLE bound to the sourcePropertyName value based on the baseTypeRestriction. |
mustImplement | If the baseType is THINGNAME and you specify “mustImplement”, the Mashup Builder will restrict popups to those implementing the specified EntityType and EntityName [by calling QueryImplementingThings against said EntityType and EntityName]. |
baseTypeInfotableProperty | If baseType is RENDERERWITHFORMAT, baseTypeInfotableProperty specifies which property’s infotable is used for configuration. |
sourcePropertyName | When the property’s baseType is FIELDNAME, this attribute is used to determine which INFOTABLE’s fields are to be used to populate the FIELDNAME dropdown list. |
baseTypeRestriction | When specified, this value is used to restrict the fields available in the FIELDNAME dropdown list. |
tagType | If the baseType is TAGS this can be ‘DataTags’ (default) or ‘ModelTags.’ |
defaultValue | Default undefined; used only for ‘property’ type. |
isBindingSource | true or false; Allows the property to be a data binding source, default to false. |
isBindingTarget | true or false; Allows the property to be a data binding target, default to false. |
isEditable | true or false; Controls whether the property can be edited in Composer, default to true. |
isVisible | true or false; Controls whether the property is visible in the properties window, default to true. |
isLocalizable | true or false; Only important if baseType is STRING - controls whether the property can be localized or not. |
selectOptions | An array of value / (display) text structures. |
warnIfNotBoundAsSource | true or false; If true, the property will be checked by Composer to determine whether it is bound and generate a to-do item if/when it is not. |
warnIfNotBoundAsTarget | true or false; If true, the property will be checked by Composer to determine whether it is bound and generate a to-do item if/when it is not. |
Additional special baseTypes:
this.afterAddBindingSource = function (bindingInfo) {
if(bindingInfo['targetProperty'] === 'Data') {
this.resetPropertyToDefaultValue('ValueFormat');
}
};
An example of the properties property:
properties: {
Prompt: {
defaultValue: 'Search for...',
baseType: STRING,
isLocalizable: true
},
Width: {
defaultValue: 120
},
Height: {
defaultValue: 20,
isEditable: false
},
}
An example of mustImplement:
'baseType': 'THINGNAME',
'mustImplement': {
'EntityType': 'ThingShapes',
'EntityName': 'Blog'
}
An example of selectOptions:
[
{value: ‘optionValue1’, text: ‘optionText1’},
{value: ‘optionValue2’, text: ‘optionText2’}
]
An example of validate function that allows you to navigate/select the specific widget that generated the message:
this.validate = function () {
var result = [];
var srcUrl = this.getProperty('SourceURL');
if (srcUrl === '' || srcUrl === undefined) {
result.push({ severity: 'warning', message: 'SourceURL is not defined for {target-id}'});
}
return result;
}
When a Widget is first created, the runtime will obtain any declared Properties by calling the runtimeProperties() function.
// note that this is a jQuery object
var widgetElement = this.domElement;
This is important because the runtime actually changes your DOM element ID (domElementId) and you should never rely on any ID other than the ID returned from this.
If you have defined an event that can be bound, whenever that event occurs, you should call the following:
var widgetElement = this.domElement;
// change ‘Clicked’ to be whatever your event name is that
// you defined in your runtimeProperties that people bind to widgetElement.triggerHandler('Clicked');
The following APIs can be accessed by a Widget in the context of the runtime:
The following functions on the widget are called by the runtime.
runtimeProperties() - [optional] Returns a JSON structure defining the properties of this widget. Optional properties are:
this.runtimeProperties = function () {
return {
'needsDataLoadingAndError': true,
'propertyAttributes': {
'TooltipLabel1': {'isLocalizable': true}
}
}
};
renderHtml() [required] - Returns HTML fragment that runtime will place on the screen; the widget’s content container (e.g. div) must have a ‘widget- content’ class specified. After this container element is appended to the DOM, it becomes accessible via jqElement and its DOM element ID will be available in jqElementId.
afterRender() [optional] - Called after the widget HTML fragment is inserted into the DOM. Use this domElementId to find the DOM element ID. Use this jqElement to use the jQuery reference to this DOM element.
beforeDestroy() [optional but highly recommended] - This is called anytime the widget is unloaded. This is where to:
resize(width,height) [optional – Only useful if you declare supportsAutoResize: true] - This is called anytime the widget is resized. Some widgets don’t need to handle this, for example, if the widget’s elements and CSS auto-scale. But others (most widgets) need to actually do something to accommodate the widget changing size.
handleSelectionUpdate(propertyName, selectedRows, selectedRowIndices) - Called whenever selectedRows has been modified by the data source you’re bound to on that PropertyName. selectedRows is an array of the actual data and selectedRowIndices is an array of the indices of the selected rows. Note: To get the full selectedRows event functionality without having to bind a list or grid widget, this function must be defined.
serviceInvoked(serviceName)- serviceInvoked() - Called whenever a service you defined is triggered.
updateProperty(updatePropertyInfo) - updatePropertyInfo An object with the following JSON structure:
{
DataShape: metadata for the rows returned
ActualDataRows: actual Data Rows
SourceProperty: SourceProperty
TargetProperty: TargetProperty
RawSinglePropertyValue: value of SourceProperty in the first row of ActualDataRows
SinglePropertyValue: value of SourceProperty in the first row of ActualDataRows converted to the defined baseType of the target property [not implemented yet],
SelectedRowIndices: an array of selected row indices
IsBoundToSelectedRows: a Boolean letting you know if this is bound to SelectedRows
}
For each data binding, the widget’s updateProperty() will be called every time the source data is changed. You need to check updatePropertyInfo. TargetProperty to determine what aspect of the widget needs to be updated. An example from thingworx.widget.image.js:
this.updateProperty = function (updatePropertyInfo) {
// get the img inside our widget in the DOM
var widgetElement = this.jqElement.find('img');
// if we're bound to a field in selected rows
// and there are no selected rows, we'd overwrite the
// default value if we didn't check here
if (updatePropertyInfo.RawSinglePropertyValue !== undefined) {
// see which TargetProperty is updated
if (updatePropertyInfo.TargetProperty === 'sourceurl') {
// SourceUrl updated - update the
NOTE: In the code above, we set a local copy of the property in our widget object, so if that property is bound as a data source for a parameter to a service call (or any other binding) - the runtime system can simply get the property from the property bag. Alternately, we could supply a custom getProperty_ {propertyName} method and store the value some other way.
getProperty_{propertyName}() - Anytime that the runtime needs a property value, it checks to see if the widget implements a function that overrides and gets the value of that property. This is used when the runtime is pulling data from the widget to populate parameters for a service call.
After you’ve completed your code updates, it’s time to build and import! Below are instructions for building with the framework you have set for the project.
Go to ThingWorx Composer. In the lower left, click Import/Export, then select Import under.
Select Extension for the type of import.
Choose the zip file in the distributions folder under the build directory. Click Import.
Click Close on the prompt to refresh your Composer instance.
After you have imported your new widget and refreshed Composer, you will see your new widget as an option on the Mashup Design tab.
When a new version of an extension contains model changes for one or more of its entities, the previous version of the extension and any entities created from that extension must be deleted before installing the new version of the extension. To avoid having to recreate the entities after the new extension is installed, you can include an extension migrator with the extension that imports the entities created with the previous version.
To create a migrator class for your extensions, follow these steps:
The Java source file is created and the configfiles/metadata.xml file is updated automatically.
NOTE: If a migrator has already been created, the previous migrator class name will be replaced in the metadata.xml file with the new migrator class name. The previous migrator Java file will not be changed.
Use this.jqElement to limit your element selections. This will reduce the chance of introducing unwanted behaviors in the application when there might be duplicate IDs and/or classes in the DOM.
///Don’t do the following:
$('.add- btn').click(function(e){...do something...});
///Do this:
this.jqElement.find('.add-btn').click(function(e){ ...do something...});
We recommend that you use the following methods to log in the Mashup Builder and runtime environment:
TW.log.trace(message[,message2,...][,exception])
TW.log.debug(message[,message2,...][,exception])
TW.log.info(message[,message2,...][,exception])
TW.log.warn(message[,message2,...][,exception])
TW.log.error(message[,message2,...][,exception])
TW.log.fatal(message[,message2,...][,exception])
You can view the log messages in the Mashup Builder by opening the log window via the Help->Log menu item; in the Mashup runtime, you can now click on the Show Log button on the top left corner of the page to show log window. If the browser you use supports console.log(), then the messages will also appear in the debugger console.
If you have a property with baseType of STYLEDEFINITION, you can get the style information by calling:
var formatResult = TW.getStyleFromStyleDefinition(
widgetProperties['PropertyName']);
If you have a property of baseType of STATEFORMATTING:
var formatResult = TW.getStyleFromStateFormatting({
DataRow: row,
StateFormatting: thisWidget.properties['PropertyName']
});
In both cases formatResult is an object with the following defaults:
{
image: '',
backgroundColor: '',
foregroundColor: '',
fontEmphasisBold: false,
fontEmphasisItalic: false,
fontEmphasisUnderline: false,
displayString: '',
lineThickness: 1,
lineStyle: 'solid',
lineColor: '',
secondaryBackgroundColor: '',
textSize: 'normal'
};
Congratulations! You’ve successfully completed the Create A Mashup Widget Extension tutorial. You’ve learned how to:
We recommend the following resources to continue your learning experience:
Capability | Guide | |
---|---|---|
Build | Application Development Tips & Tricks |
If you have questions, issues, or need additional information, refer to:
Resource | Link | |
---|---|---|
Community | Developer Community Forum | |
Marketplace | Extension Library | |
Help Center | Custom Widget Tips and Example |