Lade...
 

WebWidgets

WebWidgets

WebWidget Entwicklung mit AngularJS

Für das Erstellen von AngularJS-WebWidgets wird ein Grundwissen zu AngularJS vorausgesetzt. Im Folgenden wird nur beschrieben, wie ein WebWidget als AngularJS-Controller definiert werden kann. Die Variante mit der Direktive ist mächtiger und vielseitiger, der Funktionsumfang einer Direktive wird aber nur selten benötigt. WebWidgets werden in widgetPath angelegt (falls es generische Widgets sind) oder in widgetPath/(Kunde | Projekt) falls es sich um kunden-/projektspezifische Widget handelt.

Ein WebWidget, besteht aus folgenden Teilen:

  • Einer .html-Datei (dem Einstiegspunkt), die in dem APP-Flag angegeben wird und den Controller des Widgets referenziert und eine Art Template darstellt.
  • Einer .js-Datei mit dem Schema (*-directive.js), die optimaler Weise einen ähnlichen Namen hat, wie die .html-Datei. Hier kommt der JavaScript-Code für den Controller rein, der in der .html-Datei referenziert wird. Die Datei wird im gleichen Verzeichnis abgelegt, wie die .html-Datei.
  • Beliebig vielen .css-Dateien, die Style-Definitionen enthalten, die zur Darstellung des WebWidgets notwendig sind. Style-Definitionen sollten nicht in der .html Datei aufgeführt (style-Tag) oder refrenziert (link-Tag) werden, da sie ansonsten mehrfach und zum falschen Zeitpunkt eingebunden werden. Die .css-Dateien werden im Verzeichnis dependencies/ abgelegt, welches relativ zur .html-Datei angelegt wird. Falls eine .css nur Styles für ein bestimmtes WebWidget definiert, dann sollte die Datei ähnlich benannt werden. Gehört die .css zu einer eingebundenen Bibliothek, dann sollte der Name beibehalten werden. CSS-Selektoren, die sich auf ein Widget beziehen, sollten möglichst spezifisch sein, damit sich zwei unterschiedliche Widgets nicht beeinflussen.
  • Beliebig vielen .js-Dateien, in die JavaScript-Abhängigkeiten enthalten, die das WebWidget benötigt, um zu funktionieren. Einige geläufige Bibliotheken, wie JQuery, JQuery-UI, JSTree und lodash sind immer verfügbar und müssen nicht als Abhängigkeit abgelegt werden, da sie auch von MorphIT benutzt werden. JavaScript-Abhängigkeiten sollten ebenfalls in dem Ordner dependencies/ abgelegt werden, relativ zur .html-Datei.

 

Eine Verzeichnisstruktur kann dann beispielsweise wie folgt aussehen:

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

 

Anhand dieser Struktur wird im folgenden ein WebWidget entworfen, welches zwei Zahlen (a&b) an ClassiX schickt, und das Ergebnis (Produkt) darstellt.

Die WebWidget-Definition in InstantView würde wie folgt aussehen:

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 } ]

 

Das HTML-Template zu dem Widget könnte wie folgt aussehen: (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>

Das dazugehörige myWidget.css könnte wie folgt aussehen:

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

 

Der dazugehörige Controller (die Logik des Widgets) würde wie folgt aussehen: (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); }); }]);

 

Auf diese Weise lässt sich mit relativ wenig Code ein gänzlich neues Widget definieren, welches hoch interaktiv ist. Zu beachten ist die Konvention, dass die Nachricht, die das WebWidget sendet durch Umwandeln in Großbuchstaben und anfügen eines _SOCKET zum Namen der ausgelösten Message wird. (multiply -> MULTIPLY_SOCKET). Für das Empfangen und Senden von asynchronen Nachrichten an die verbundene ClassiX-Instanz bietet MorphIT den service webwidgetCommunication an, der wie im Codebeispiel gezeigt verwendet werden kann. Der Service bietet ab MorphIT 3.16.1 bietet der Service zusätzlich zu dem alten Interface ein vereinfachtes Interface (Channels), das wie folgt benutzt wird:

//... //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 kann prinzipiell zu jedem Zeitpunkt über Widget Call(PushSocket) eine Nachricht an das HTML-WebWidget schicken. Es kann aber sein, dass das WebWidget im Browser zu dem Zeitpunkt noch nicht initialisiert oder gar nicht offen ist und die Nachricht somit nie ankommt. Als Best-Practice sollte sich also das HTML-WebWidget immer zuerst melden. Es ist nicht gefordert, dass ClassiX auf eine Message vom Browser-Widget eine Antwort schickt.

 

Testen

Zur Unterstützung bei der Entwicklung und beim Testen von WebWidgets gibt es (aktuell nur in QM) das WebWidget QM/test.html, welches ein einfaches Interface definiert, um dem verbundenen nativen WebWidget beliebige Nachrichten zu schicken. So kann vor der Entwicklung des HTML-WebWidgets sichergestellt werden, dass das WebWidget auf InstantView-Seite korrekt implementiert ist. So können auch auf einfache Weise Fehler in der Implementierung gefunden werden.

 

WebWidgets ohne Angular

4.15.0

MorphIT bietet zusätzlich eine Schnittstelle für die Entwicklung von WebWidgets an, die rein auf JavaScript und HTML basiert. Damit lassen sich WebWidgets ohne Kenntnis von AngularJS/Angluar entwickeln, die zudem Kompatibel mit beiden Major-Versionen von MorphIT sind (4 & 5).

Nachfolgend wird kurz skizziert, wie ein WebWidget als HTML-Custom-Element definiert werden kann.

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>

Das Custom-Element wird in der Script-Datei myWidget/widget.js definiert. Diese Script-Datei wird über den webWidgetLoader-Service geladen. Der Rückgabewert von loadWidget(...) ist ein Promise, welches erfüllt wird, sobald das Script geladen und ausgeführt wurde. Dabei wird das angegebene Script nur einmalig geladen auch wenn das Webwidget mehrfach geöffnet wird. Das Promise enthält ein Widget-Objekt mit den folgenden Eigenschaften:

Name Wert
id Die Widget-Id des WebWidgets.
Wird u.a. für die Kommunikation mit ClassiX benötigt.
url

Die URL der .html-Datei. Dieser Pfad kann verwendet werden, um
Ressourcen anzufragen, die in einem Verzeichnis relativ zum WebWidget selbst liegen.

element Das
-Element, in welches die .html-Datei geladen wurde.
Über dieses Element können die Elemente der .html-Datei lokalisiert werden.
onDestroy

Eine Funktion, die Aufgerufen werden kann, um Aufräumaufgaben anzumelden.
Die angemeldete Funktionen (1-n) werden ausgeführt, sobald das WebWidget geschlossen
wird, oder sich dessen HTML-Inhalt ändert (aufgrund von SetApp oder PutValue).

 

In der .js-Datei des Widgets kann dann das Custom-Element definiert werden. Das nachfolgende Beispiel öffnet einen Kommunikationskanal (webwidgetCommunication) zum ClassiX-Widget und reagiert auf Sprachwechsel (localization).

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);

 

Die dazugehörigen Übersetzungsdateien werden wie in registerTranslation() angegeben im Verzeichnis /translations als JSON-Dateien nach dem Schema: de.json, en.json, ... erwartet. Nachfolgend die in dem Beispiel verwendete Übersetzungsdatei:

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

Eine Dokumentation der MorphIT-API finden Sie hier.

WebWidgets mit statischem MorphIT

Der MorphIT-Server bietet die Möglichkeit an, die Clients ohne gebundene ClassiX-Instanz bis zu einem gewissen Grad (vor Login) direkt aus vorher exportierten statischen Views zu bedienen. WebWidgets können im eingeschränkten Umfang auch auf diesen statischen Views verwendet werden.

Erkennt der Client, dass er keine stehende Verbindung zu einer ClassiX-Instanz hat, schaltet das der Service webwidgetCommunication intern auf HTTP-Anfragen um. Daraus ergeben sich direkt einige Einschränkungen und notwendige Anpassungen für das WebWidget. Gut geschriebene WebWidgets können sowohl im statischen, als auch im dynamischen Modus verwendet werden.

 

WebSerivce-Interface: Die HTTP-Anfragen der WebWidgets werden vom MorphIT-Server einer beliebigen verfügbaren WebService-ClassiX-Instanz zugewiesen und an deren WebService-Interface weitergeleitet. Die Nachrichten gehen also nicht direkt beim Widget ein (..._SOCKET), sondern werden als WebService-Message (..._POST) in das System gebroadcasted. Anstatt eines JSON-Objekts oder analogem InstantView-Typ liegt auf dem Stack ein CX_HTTP_REQUEST-Objekt, welches im Body einen JSON-String enthält, der mit dem CX_JSON_PARSER geladen werden kann.
Die Antwort kann wie im WebService-Interface üblich als CX_JSON_OBJECT mit ReturnStack zurückgegeben werden. Falls manuell ein CX_HTTP_RESPONSE-Objekt zurückgegeben wird, muss darauf geachtet werden, dass der Body ein gültiges JSON enthalten, ansonsten wird die Antwort verworfen.

 

Stateless: Da die Anfragen aller vorhandenen statischen MorphIT-Clients von einer (oder einigen wenigen) ClassiX-Instanzen beantwortet werden, dürfen die Anfragen nicht voraussetzen, dass sich ClassiX Informationen aus vorherigen Anfragen merkt und auf diese in nachfolgenden Anfragen zurückgreift. Jeder Aufruf für sich muss also in sich abgeschlossen beantwortbar sein.

 

Request-Reply: Da die Kommunikation auf HTTP-Anfragen vom Client an ClassiX reduziert ist, muss für ein kompatibles WebWidget auf die Vorteile der WebSockets verzichtet werden. So kann das WebWidget immer nur dann eine Nachricht von ClassiX erhalten, wenn es vorher eine Nachricht an ClassiX geschickt hat. Im Gegensatz zur WebSocket-Verbindung ist ClassiX im statischen Fall auch dazu gezwungen auf jede Nachricht mit einer Nachricht zu antworten.

 

Antwort-Typ: Falls ClassiX mit einem JSON-Objekt antwortet, dann wird der Typ aus dem Feld type übernommen (falls dieser gesetzt ist). Sollte dies nicht der Fall sein, dann ist der Typ der Antwort gleich dem Typ der Anfrage. In unserem Beispiel wäre die Antwort auf multiply also ebenfalls vom Typ multiply, weil ClassiX kein JSON-Objekt zurückgibt und damit den Typ nicht anders setzen kann.

 

Error-Typ: Der MorphIT-Server oder ClassiX können, um Fehler zu signalisieren (Bsp: keine WebSerivce-Instanz verfügbar) auf eine HTTP-Anfrage mit einem JSON-Objekt vom Typ error antworten. Solche Antworten werden ebenfalls an das WebWidget geschickt. Falls das WebWidget keinen expliziten 'error'-Handler am Channel registriert hat, dann wird die Fehlermeldung in der Konsole und über dem WebWidget in rot ausgegeben.

Das Objekt ist wie folgt definiert:

Feld Typ Beschreibung
type string Enthält den Wert "error"
errorId string Optional - Falls es eine mehrsprachige Beschreibung des Fehlers gibt (de.json, en.json), dann enthält dieses Feld die Übersetzungs-Id
error string Enthält die Fehlermeldung in englischer Sprache