Wie schreibe ich eine Komponente

Eine Komponente ist eine einfach Javascript-Datei die für die Komponenten alle notwendigen Resourcen (Css, Javascript, Templates, ...) enthält und über ein Script-Tag in die Konsole eingebunden wird.

Es gibt zwei Arten von Komponenten: Funktionskomponenten 2 die eine bestimmte Funktionalität innerhalb der Konsole bereitstellen - zum Beispiel Tastaturkürzel - und Kontextkomponenten 1, die eine Ausgabe habe und über die Navigation angesteuert werden - zum Beispiel ein Dashboard.

Der Basisaufbau beider Typen ist identisch. Kontextkomponenten1 verfügen zusätzlich über einen View.

Vorbereitung

Als erstes muss man eine Verzeichnisstruktur anlegen.

Komponente
   ┣━━ assets         ⤑ Assets wie Bilder, PDF, etc.
   ┣━━ css            ⤑ CSS-Dateien
   ┣━━ javascript     ⤑ Javascriptdateien
   ┣━━ resource       ⤑ Ressourcen wie Sprachdateien
   ┗━━ vendor         ⤑ Herstellerdateien

Der grundlegende Aufbau einer Komponente besteht aus einem einfachen JS-Gerüst.

try {

    (function() {
        var globalContext = Function('return this')()||(42, eval)('this');

        // Hier Code einfügen

        return globalContext;

    })();

} catch(e) {

    // Fehlerdialog anzeigen
    try {
        Alvine.Package.Console.Host.showNothingWorksAnymoreDialog("%%FILENAME%%", e);
    } catch(nothing) {
    }

    // Event senden
    try {
        if(typeof jQuery!=='undefined') {
            jQuery(window).trigger('exception.ALVINE', e);
        }
    } catch(nothing) {
    }

    // Ausgabe in der Konsole
    try {
        Alvine.Util.Logger.logError("%%FILENAME%%", e);
    } catch(nothing) {
        console.error(e);
    }
}

Wir speichern nun diese Datei als HTML-Datei in unser Komponentenverzeichnis.

Komponente
   ┗━━ component.js ⤑ Komponentendatei

Nun haben wir eine Komponente und können diese je nach Typ über die Konfiguration in die Konsole einbinden.

Beispiel für das Einbinden eine Funktionskomponente 2

"module": [
    {         
        "component": "example.MyComponent",
        "location": "http://www.example.com/component.js"
}, ...
],

Und ein Beispiel für die Konfiguration einer Kontextkomponente 1

"components": {
    "component": [
        {
            "module": {
                "prototype": "MyComponent",
                "location": "http://www.example.com/component.js"
            },
            "handler": "mycomponent",
            "arguments": {
               "key": 'value
            }
        }, ...

Die Kontextkomponente verfügt in der Konfiguration über einen Schlüssel zur Angabe des Handlers und einen Schlüssel zur Angabe von zusätzlichen Argumenten. Dadurch lässt sich eine Komponente mehrfach nutzen.

Komponenten in Javascript definieren

Jede Komponente wird über das Modulsystem vom Host eingebunden. Innerhalb der Komponente legen wir ein zentrales Komponenten-Objekt an. Dies wird in der JS-Datei definiert. Hierzu wird unser Beispiel um ein script-Bereich erweitert.

var component = Alvine.Package.Factory.createComponent('MyComponent');

Bei einer Kontextkomponenten1 wird beim Verlassen des Kontextes die Methode MyComponent.cleanUp() aufgerufen. In dieser Methode sollten alle Ressourcen (Registry, Datenbank, ..) die von der Komponente verwendet werden, geschlossen und gelöscht werden.

component.cleanUp = function() {
    // Aufräumarbeiten
};

Kontextkomponente definieren

Eine Kontextkomponete stellt die Schnittstelle zwischen dem System und dem Benutzer her. Dafür benötigt die Komponente einen View (Darstellung der Steuerelemente). Für den View wird eine Struktur benötigt, die wir über ein Template in die Komponente einbinden.

Die Definition des Templates erfolgt über das templates-Schlüsselwort in den startComponent-Optionen. Das Template muss das Attribut data-container enthalten. Innerhalb dieser Node wird die Ausgabe der Komponente eingefügt.

<template id="template">
    <div data-container></div>
</template>

Über die Methoden Alvine.Package.Factory.createComponent(componentName, componentVersion, requirements) und Component.createView() lässt sich die Komponente anschliessend initialisieren. Leichter und einfacher geht es aber über die Methode Alvine.Package.Factory.startComponent(componentName, componentVersion, requirements).

Die Methode Alvine.Package.Factory.startComponent(componentName, componentVersion, requirements) fügt alle Bausteiner zusammen und erlaubt so eine schnelle und sichere Umsetzung einer Komponente. Alvine.Package.Factory.startComponent(componentName, componentVersion, requirements) erwartet neben dem Namen der Komponente auch die Version und optional Abhängigkeiten.


var namespace = "example";
var componentName = "MyComponent";
var componentVersion = "1.0.0";


Alvine.Package.Console.Host.startComponent(namespace+'.'+componentName, componentVersion, {

    /** Notwendige Versionsnummern des Alvine-Framework und jQuery */
    libraries: {
        alvine: '1.9.0',
        jquery: '2.3.0'
    },

    /** Javascript-Bibliotheken die geladen werden müssen */
    externals: [
        "/app/console/js/mylib.js",
        "/app/console/css/mylib.css",
    ],

    /** Notwenige Funktionskomponenten die eingebunden sein müssen */
    modules: {
        'Alvine.Package.UI.Dialog.Bootstrap': '1.0.0',
        'Alvine.Package.i18n.Globalize': '1.0.0'
    },

    /** URL in der die Sprachdateien liegen */
    locales: [
        "resource/"
    ],

    /** Templates */
    templates: [
        "<template></template>"
    ],

    component: {

        /** Wird beim Verlassen des Kontextes aufgerufen */
        cleanUp: () => {
            // do something
        }
    },

    /** Initialisierung des Views */
    view: {
        init: initUI
        activate: activateUI,
        template: '#template'
    }

}).catch(error => {
    Alvine.Util.Logger.logError(namespace+'.'+componentName, componentVersion, 'startComponent', error);
    Alvine.Package.Console.Host.notifyDanger('i18n:error.somethingwentwrong');
});

function activateUI(configuration) { 

}

Die beiden Funktionen initUI und activateUI werden nacheinander aufgerufen. Während des Aufrufs von initUI werden alle Controls und Dialoge erstellt und nach dem Aufruf der Funktion ins DOM eingebunden.

function initUI(configuration, doneCallback) { 
     var view = $this; 
     /** do something */ 
     doneCallback();
}

doneCallback dient dazu, die Ablaufkontrolle im init-Teil anzuhalten, bis alle externen Ereignisse (wie z.B. das Nachladen von Daten) erfolgt sind. Der doneCallback-Parameter ist optional. Wird die Methode ohne zweite Parameter definiert initUI(configuration) so wartet der Host nicht auf den Aufruf und fährt sofort mit dem Aufruf von activateUI() fort.

Best practice ist es alle Promises von externen Anfragen in einen Iterator zu sammeln und anschliessend über Promise.all() den doneCallback() aufzurufen.


var promises = []; //iterator

// Externe Daten abfragen
promises.push(Alvine.Package.Console.Host.fetch(dataURL1, {});
// Weitere Daten ....
promises.push(Alvine.Package.Console.Host.fetch(dataURL2, {});

/** do something */ 


Promise.all(promises).then(() => {
    doneCallback();
});
Nachdem das DOM erstellt wurde, wird vom Host der Callback activateUI aufgerufen. Hier können Eventhandler und Änderungen am DOM durchgeführt werden.

function activateUI(configuration) { 
     var view = $this; 
     /** do something */ 
}

Der Ablauf der Initialisierung ist hier nochmal bildlich dargestellt:

uml diagram

Über den View können Events einfach zentral Abgefangen werden. Dazu definieren wir zum Beispiel:

function activateUI(configuration) { 
     var view = $this; 
    view.on('click', 'button', function(event) {
        /** do something */ 
    });
}

Die Methode view.on(event, selector, callback) erwartet den gewünschten Event-Typ (click, change, ...), einen Selektor zur identifizierung des Elements und eine Callback-Funktion.

Bilder und CSS-Dateien

Bilder werden im Verzeichnis asset und CSS-Dateien im Verzeichnis css der Komponente gespeichert. Die Dateien können relativ zur Komponente referenziert werden.

Javascript

Dateien von externen Projekten werden entweder direkt über ein CDN eingebunden oder im vendor-Verzeichnis der Komponente gespeichert.

Laden der lokalisierten Sprachdatei

Alle Übersetzungen der Komponente werden in einer einfachen JSON-Datei gespeichert. Der Aufbau der Datei ist sehr einfach. Die Schlüssel entsprechen den in der Komponente verwendeten Zeichenketten und der Wert ist jeweils die Übersetzung in der betreffenden Landessprache.

{
    "i18n:ok": "OK",
    "i18n:cancel": "Abbruch",
}

Hinweis

Die Schlüssel von lokalisierten Texten soll immer mit dem Prefix i18n: beginnen.

Die Lokale-Datei kann absolut oder relativ zur Komponente - am besten im Verzeichnis resource gespeichert werden. Der Dateiname ist der Name der Lokale, zum Beispiel: de-DE.json oder nur de.json.

resource
   ┣━━ de.json    ⤑ Deutsche Sprachdateien
   ┗━━ en.json    ⤑ Englische Sprachdateien

Wichtig

Einfache Ausdrücke wir die folgenden sind bereits über die Bootstrap-Komponente verfügbar und dürfen nicht neu definiert werden.

    "i18n:ok": "OK",
    "i18n:cancel": "Abbruch",
    "i18n:submit": "absenden",
    "i18n:delete": "löschen",
    "i18n:add": "hinzufügen",
    "i18n:remove": "entfernen"

Wichtig

Alle Schlüssel werden in einer zentralen Datenbank des Host gespeichert. Die eigenen Schlüssel müssen deshalb immer im Namensraum der Komponente definiert werden, damit diese keine anderen Schlüssel überschreiben. Statt i18n:mykey sollte i18n:mycomponent.mykey definiert werden.

Die Sprachdatei kann entweder direkt über startComponent oder manuell über folgende Anweisung geladen werden:

var baseurl = component.getModule().getBaseURL();
var resourcePath = 'resource';
var promise = Alvine.Package.Console.Host.loadLocale(baseurl+resourcePath);

promise
  .then(function(result) {
      // Lokale wurden geladen
   })
  .catch(function(result) {
     // Fehler
   });

Das von Host.loadLocale zurückgegebene Promise transportiert als ersten Wert ein Objekt mit dem Status der Queue. Im Erfolgsfall wird {state:'done'} und im Fehlerfall {state:'error'} zurückgegeben.

Siehe hierzu auch die Anleitung im Alvine Frontend Framework

Zugriff auf die Settings

Speichert die Komponente Werte im Settingsbereich, so kann die Komponente auf diese über die Registry zugreifen.

Alvine.Registry.getValueFromPath('console.settings.my');

  1. Kontextkomponenten verfügen, anders als Funktionskomponenten 2 über einen View und lassen sich über die Navigation ansteuern. 

  2. Funktionskomponenten werden zentral eingebunden und stellen eine Funktionalität ohne expliziten Userview zur Verfügung.