Loading...
 

WebWidgets

WebWidgets

WebWidget development with AngularJS

A basic knowledge of AngularJS is required to create AngularJS WebWidgets. The following only describes how a WebWidget can be defined as an AngularJS controller. The variant with the directive is more powerful and versatile, but the functionality of a directive is rarely needed. WebWidgets are created in widgetPath (if they are generic widgets) or in widgetPath/(Customer | Project) if they are customer/project-specific widgets.

A WebWidget, consists of the following parts:

  • An .html file (the entry point), which is specified in the APP flag and references the widget's controller and is a kind of template.
  • A .js file with the schema (*-directive.js), which optimally has a similar name to the .html file. This is where the JavaScript code for the controller comes in, which is referenced in the .html file. The file is stored in the same directory as the .html file.
  • Any number of .css files containing style definitions necessary to display the WebWidget. Style definitions should not be listed (style tag) or referenced (link tag) in the .html file, otherwise they will be included multiple times and at the wrong time. The .css files are stored in the dependencies/ directory, which is created relative to the .html file. If a .css only defines styles for a specific WebWidget, the file should be named similarly. If the .css file belongs to an included library, then the name should be kept. CSS selectors that refer to a widget should be as specific as possible so that two different widgets do not affect each other.
  • Any number of .js files that contain JavaScript dependencies that the WebWidget needs to function. Some common libraries, such as JQuery, JQuery-UI, JSTree and lodash are always available and do not need to be stored as dependencies, since they are also used by MorphIT. JavaScript dependencies should also be placed in the dependencies/ folder, relative to the .html file.

A directory structure could look like this:

widgetPath/ myWidget.html myWidget-directive.js dependencies/ myWidget.css fancy-lib.css fancy-lib.js

Based on this structure, a web widget is designed in the following, which sends two numbers (a&b) to ClassiX and displays the result (product).

The web widget definition in InstantView would look like this:

Web(web, APP("myWidget.html"), 0,0,100,100) [ Msg(MULTIPLY_SOCKET) //Websocket-Message deklarieren (wird ausgelöst über websocketCommunication.send('multiply')) MULTIPLY_SOCKET: // Auf dem Stack liegt das, was an send übergeben wurde {a:(factor), b:(factor)} { LocalVar(json, result) -> json json Copy(a) json Copy(b) * -> result //Werte multiplizieren result "product" Widget Call(PushSocket) //Ergebnis per PushSocket über den Websocket an das HTML-Widget senden } ]

The HTML template for the widget could look like this: (myWidget.html)

<div ng-controller="myWidgetController"> <! Controller referenzieren --> <div ng-if="initialized" class="success">Result: {{result}}</div> <! Ergebnis darstellen, wenn es verfügbar ist (Klasse wird in .css definiert) --> <div ng-if="!initialized">Not yet initialized</div> <! Ansonsten standardmeldung anzeigen --> </div>

The associated myWidget.css could look like this:

div[ng-controller="myWidgetController"] .success { color: green; }

The associated controller (the logic of the widget) would look like this: (myWidget-directive.js):

morphIT.controller('myWidgetController', ['$scope', '$rootScope', 'webwidgetCommunication', function($scope, $rootScope, webwidgetCommunication) { var id = $scope.id; //<- die Widget-ID ist immer über $scope.id erreichbar var url = $rootScope.widget[id].url; //<- die vollständige URL aus APP(...) inklusive eventueller Query-Parameter //Scope initialisieren $scope.initialized = false; //Callback registrieren, der ausgelöst wird, wenn ClassiX Daten sendet webwidgetCommunication.registerWidget(id, function(type, data) { if (type === 'product') { $scope.initialized = true; $scope.result = data; //<- empfangene Daten zuweisen } }); //Message ('multiply',{a:21,b:2}) an ClassiX senden... ClassiX sollte mit ('product', 42) antworten webwidgetCommunication.send(id, 'multiply', {a:21, b:2}); //Widget beim Löschen des Scopes wieder deregistrieren, um Ressourcen freizugeben $scope.$on('$destroy', function() { webwidgetCommunication.deregisterWidget(id); }); }]);

In this way, with relatively little code, an entirely new widget can be defined, which is highly interactive. Note the convention that the message the WebWidget sends is converted to uppercase and a _SOCKET is appended to the name of the triggered message. (multiply -> MULTIPLY_SOCKET). For receiving and sending asynchronous messages to the connected ClassiX instance MorphIT offers the service webwidgetCommunication, which can be used as shown in the code example. As of MorphIT 3.16.1 the service offers a simplified interface (channels) in addition to the old interface, which is used as follows:

//... //Widget registrieren var channel = webwidgetCommunication.registerWidget(id); //Callback function(data,type) am channel für Nachrichten vom Typ 'product' registrieren //Der Typ '*' kann angegeben werden, um auf alle Nachrichtentypen zu reagieren channel.on('product', function(data) { $scope.initialized = true; $scope.result = data; //<- empfangene Daten zuweisen }); //Message ('multiply',{a:21,b:2}) an ClassiX senden... ClassiX sollte mit ('product', 42) antworten channel.send('multiply', {a:21, b:2}); //Widget beim Löschen des Scopes wieder deregistrieren, um Ressourcen freizugeben $scope.$on('$destroy', function() { channel.close(); });

ClassiX can in principle send a message to the HTML WebWidget at any time via Widget Call(PushSocket). However, it is possible that the WebWidget in the browser is not yet initialised or not even open at that time and the message will therefore never arrive. As a best practice, therefore, the HTML WebWidget should always report first. It is not required that ClassiX sends a response to a message from the browser widget.

Testing

To support the development and testing of web widgets there is (currently only in QM) the web widget QM/test.html, which defines a simple interface to send arbitrary messages to the connected native web widget. This allows you to ensure that the WebWidget is correctly implemented on InstantView pages before developing the HTML WebWidget. This also makes it easy to find errors in the implementation.

WebWidgets without Angular

4.15.0

MorphIT also offers an interface for the development of WebWidgets that is based purely on JavaScript and HTML. This allows WebWidgets to be developed without knowledge of AngularJS/Angluar, which are also compatible with both major versions of MorphIT (4 & 5).

The following is a brief outline of how a WebWidget can be defined as an HTML custom element.

myWidget/widget.html
<script> (function() { morphitApi.services.webwidgetLoader.loadWidget('widget.js').then((widget) => { // Now locate the <my-widget> element in this widget // widget.element refers to the parent <div>, which now includes the <script> and <my-widget> elements let myWidget = widget.element.getElementsByTagName('my-widget')[0]; myWidget.initialize(widget); // <- defined in MyWidget.initialize() in myWidget/widget.js }); })(); </script> <my-widget></my-widget>

The custom element is defined in the script file myWidget/widget.js. This script file is loaded via the webWidgetLoader service. The return value of loadWidget(...) is a promise that is fulfilled as soon as the script has been loaded and executed. The specified script is only loaded once, even if the web widget is opened multiple times. The promise contains a widget object with the following properties:

NameValue
idThe widget id of the WebWidget.
Required, among other things, for communication with ClassiX.
url

The URL of the .html file. This path can be used to
request resources located in a directory relative to the WebWidget itself.

elementThe
-element into which the .html file was loaded.
This element can be used to locate the elements of the .html file.
onDestroy

A function that can be called to register clean-up tasks.
The registered functions (1-n) are executed as soon as the WebWidget is closed.
or its HTML content changes (due to SetApp or PutValue).

The custom element can then be defined in the widget's .js file. The following example opens a communication channel(webwidgetCommunication) to the ClassiX widget and reacts to language changes(localisation).

myWidget/widget.js
class MyWidget extends HTMLElement { constructor() { super(); // Build widget DOM structure this.attachShadow({mode:"open"}); this.myTitle = document.createElement("div"); this.shadowRoot.appendChild(this.myTitle); this.myStatus = document.createElement("div"); this.shadowRoot.appendChild(this.myStatus); this.myStatus.textContent = "Widget constructed"; } // called externally initialize(widget) { this.widget = widget; // Setup communication with ClassiX this.channel = morphitApi.services.webwidgetCommunication.registerWidget(widget.id); widget.onDestroy(() => { this.channel.close(); }); // Setup localization this.translator = morphitApi.services.localization.registerTranslation(widget.url, 'translations/'); widget.onDestroy(morphitApi.services.localization.onLanguageChanged(this.languageChanged.bind(this))); } // called whenever the language is changed languageChanged(newLanguage) { const variables = { id: this.widget.id, lang: {to: newLanguage } }; // Request and await translation for text literals this.translator.translate(['title', 'language.set'], variables).then((t) => { // Update displayed content this.myTitle.textContent = t['title']; this.myStatus.textContent = t['language.set']; }); } } customElements.define("my-widget", MyWidget);

The corresponding translation files are expected in the /translations directory as JSON files according to the scheme: de.json, en.json, ... are expected. The following is the translation file used in the example:

myWidget/translations/en.json
{ "title": "WebWidget: {{id}}", "language": { "set": "Sprache wurde auf {{lang.to}} gesetzt" } }

Documentation of the MorphIT API can be found here.

WebWidgets with static MorphIT

The MorphIT server offers the possibility to serve the clients without a bound ClassiX instance to a certain extent (before login) directly from previously exported static views. WebWidgets can also be used on these static views to a limited extent.

If the client detects that he has no standing connection to a ClassiX instance, the service webwidgetCommunication internally switches to HTTP requests. This directly results in some restrictions and necessary adjustments for the web widget. Well-written WebWidgets can be used in static as well as in dynamic mode.

WebSerivce interface: The HTTP requests of the WebWidgets are assigned by the MorphIT server to any available WebService ClassiX instance and forwarded to its WebService interface. The messages are not sent directly to the widget (..._SOCKET), but are broadcast as WebService messages (..._POST) in the system. Instead of a JSON object or analog InstantView type, there is a CX_HTTP_REQUEST object on the stack, which contains a JSON string in its body that can be loaded with the CX_JSON_PARSER.
The response can be returned as CX_JSON_OBJECT with ReturnStack as usual in the WebService interface. If a CX_HTTP_RESPONSE object is returned manually, you must ensure that the body contains a valid JSON, otherwise the response is discarded.

Stateless: Since the requests of all existing static MorphIT clients are answered by one (or a few) ClassiX instances, the requests must not require that ClassiX remembers information from previous requests and uses it in subsequent requests. Each call itself must be able to be answered in a self-contained way.

Request Reply: Since communication on HTTP requests from the client to ClassiX is reduced, the advantages of WebSockets must be foregone for a compatible WebWidget. So the WebWidget can only receive a message from ClassiX if it has sent a message to ClassiX before. In contrast to the WebSocket connection, ClassiX is forced to reply to each message with a message in the static case.

Reply-Type: If ClassiX replies with a JSON object, the type is taken from the field type (if this is set). If this is not the case, then the type of the answer is equal to the type of the request. In our example, the answer to multiply would therefore also be of type multiply, because ClassiX does not return a JSON object and therefore cannot set the type differently.

Error type: To signal errors (e.g. no WebSerivce instance available), the MorphIT server or ClassiX can respond to an HTTP request with a JSON object of type error. Such responses are also sent to the WebWidget. If the WebWidget has not registered an explicit 'error' handler on the channel, the error message is displayed in the console and above the WebWidget in red.

The object is defined as follows:

Field Type Description
type string Contains the value "error
errorId string Optional - If there is a multilingual description of the bug (de.json, en.json), this field contains the translation id
error string Contains the error message in English language