Durchsuchbare Dokumentation aufrufen

Zurück zur Dokumentationsübersicht

Einführung zu agorum core aguila

aguila (agorum user interface layer) ist ein in agorum core integriertes UI-Framework. Es bildet die Grundlage für bei agorum core mitgelieferte Oberflächen und ermöglicht es, auf einfache Weise eigene Bedienoberflächen auf Basis von agorum core zu entwickeln.

Die Logik einer aguila-Oberfläche wird vollständig serverseitig abgebildet, typischerweise in Form einer oder mehrerer JavaScript-Dateien. Hierbei wird Event-basiert gearbeitet, Aktionen werden immer als Reaktion auf Ereignisse ausgelöst. Ein Ereignis kann etwa die Bestätigung einer Schaltfläche, eine Änderung in einem Textfeld oder ein abgelaufener Timer sein.

Durch diese Verlagerung auf den Server ist dort pro aktivem Client eine gewisse Menge an Hauptspeicher nötig, abhängig von der Zahl und Komplexität der eingesetzten Widgets und Datenstrukturen. Gleichzeitig werden die Clients entlastet und benötigen weniger Ressourcen.

Die rein serverseitige Entwicklung hat außerdem den Vorteil, dass Anpassungen mit zukünftigen Versionen von agorum core nutzbar bleiben. Eventuelle Änderungen am Client-Framework werden durch eine neue Implementierung des Client-Teils von aguila durch agorum core selbst abgedeckt.

Durch den vollen Zugriff auf die agorum core javascript library sind alle Funktionen von agorum core direkt nutzbar. 

Durch die automatische Übertragung des Zustands der Bedienoberfläche von Server zu Client ergeben sich weitere Vorteile:

Widgets


Ein Widget ist eine Komponente eines grafischen Fenstersystems.

Oberflächenelemente werden serverseitig durch Widgets repräsentiert. Hierbei kann ein Widget entweder ein Basis-Widget, etwa eine Checkbox, oder eine Textanzeige sein oder ein Container, der selbst weitere Widgets enthält.

Um eine Struktur aus Widgets zu definieren, wird das aon-Format (aguila object notation) verwendet, das für jedes zu erzeugende Widget Typ und Parameter definiert.


Beispiel

Dieses Beispiel verwendet eine einfache aon-Struktur, die eine Textausgabe mit einer darunter rechtsbündig angeordneten Schaltfläche kombiniert:

module.exports = {
  type: 'agorum.vbox',
  items: [
    {
      type: 'agorum.textArea',
      text: 'Hallo, Welt!',
      name: 'textArea',
      width: 300,
      flexible: true
    },
    {
      type: 'agorum.hbox',
      items: [
        {
          type: 'agorum.spacer',
          flexible: true
        },
        {
          type: 'agorum.button',
          text: 'Hallo, aguila!',
          name: 'button'
        }
      ]
    }
  ]
};


Ergebnis

Ergebnis

Lebenszyklus

Der Lebenszyklus eines Widgets besteht aus folgenden Phasen und Übergängen:

Übergang Phase Beschreibung
Widget wird erzeugt   Danach ist das Widget im serverseitigen Kontext registriert und auch für andere Widgets zugänglich.
  Erzeugt
Widget wird gerendert   Übermittlung an den Client zur Darstellung.
  Gerendert
Widget wird geschlossen   3 Möglichkeiten existieren:
  • Widget wird selbst direkt geschlossen (etwa ein Fenster).
  • Der übergeordnete Container wurde geschlossen.
  • Das Widget wurde aus seinem bisherigen Container entfernt, ohne es in einen neuen Container zu legen.
Widget löst das Event „destroying“ aus  
  • Wird direkt durch das Schließen ausgelöst.
  • Hier kann das Widget noch verwendet werden, um etwa ungespeicherte Daten zu sichern.
  Geschlossen
Widget löst das Event „destroyed“ aus  
  • Hierdurch wird das Widget aus dem Kontext entfernt und ist nicht mehr verwendbar.
  • Zusätzlich wird die zugehörige Darstellung auf dem Client entfernt.
  Zerstört

Wurde ein Widget noch nicht geschlossen, kann es in einen beliebigen Container im selben Kontext verschoben werden. Gerade in größeren Widget-Strukturen mit vielen dynamisch generierten Widgets empfiehlt es sich aus Performancegründen, nach Möglichkeit vorhandene Widgets wiederzuverwenden, statt sie zu zerstören und durch neue Widgets zu ersetzen, wenn die Darstellung geändert wird.

Benennung

Widget-Typen werden nach folgendem Schema benannt:

<Paket>.<Typ>

Hierbei kann der Paketname noch weiter in Pakete und Unterpakete strukturiert sein, die ebenfalls mit . voneinander getrennt werden.

Der oberste / erste Paketname entspricht typischerweise der Firmenbezeichnung des Widget-Urhebers. So beginnen etwa alle mitgelieferten Widget-Typen mit agorum.

Verwenden Sie für eigene Widgets eindeutige Namen nach einem einheitlichen Schema, etwa:

<Firma>.<Projekt>[.<Teil>].<Typ>

Layout-Container


aguila-Oberflächen bestehen aus mehr als einem Widget. Um die Widgets zu gruppieren und die Anordnung zu definieren, gibt es Container-Widgets oder nur Container.

Sie können den Inhalt von Containern je nach Typ auf zwei verschiedene Arten definieren:

Art Beschreibung
Als Array von Elementen (items) für Container
  • Enthalten eine beliebige Anzahl von Widgets in einer definierten Reihenfolge.
  • Diese können jederzeit nach der Erzeugung des Containers verändert werden.
  • items werden etwa von hbox-/vbox-Widgets genuzt. D. h. hier können beliebig viele Widgets nacheinander kommen, die dann nebeneinander / untereinander angezeigt werden.
Unter direkter Verwendung von definierten Regionen (docked) des Containers
  • Jeweils ein Widget findet Platz.
  • Die pro Region eines Containers eingedockten Widgets können ab der Phase Gerendert nicht mehr geändert werden.
  • docked wird innerhalb des Widgets border verwendet. Hier gibt es festgelegte Regionen wie north, west oder center. Auch im Widget Toolbars (right, top) werden weitere Widgets per docked eingebaut.

Welcher Mechanismus jeweils verwendet wird, ist abhängig von dem konkret verwendeten Container-Typ.

Die vollständige Referenz der Container-Widgets finden SIe hier: Container-Widgets

agorum.hbox und agorum.vbox

Die einfachsten Container-Typen sind agorum.hbox und agorum.vbox. Sie ordnen die enthaltenen Elemente (items) horizontal nebeneinander (hbox) oder vertikal untereinander (vbox) an.

Das System stellt im Standard jedes Widget nur so breit (hbox) oder hoch (vbox) dar, wie es selbst festlegt. Das kann eine natürliche Größe sein, etwa die Höhe eines Textfelds, oder eine explizit über height oder width festgelegte Größe.

Wenn sich der zur Verfügung stehende Platz dynamisch ändern kann (etwa wenn ein Benutzer ein Fenster vergrößert / verkleinert), dann kann für ein oder mehrere Elemente stattdessen das Attribut flexible auf true gesetzt werden. Eine hbox / vbox verteilt den zusätzlich zur Verfügung stehenden Platz in seiner Hauptrichtung gleichmäßig auf alle Elemente, die als flexibel definiert wurden. Meistens enthält eine hbox / vbox höchstens ein flexibles Element, etwa ein mehrzeiliges Textfeld oder auch einen weiteren Container, der seinerseits wieder flexible Elemente enthält.

In der Nebenrichtung (vertikal für vbox, horizontal für hbox) streckt das System Elemente grundsätzlich auf die vorhandene Breite (vbox) oder Höhe (hbox).

Die äußere vbox verteilt den zur Verfügung stehenden Platz folgendermaßen:

Höhe


Breite

Sowohl textArea als auch hbox werden auf die verfügbare Breite erweitert


Die untere hbox verteilt den zur Verfügung stehenden Platz wie folgt:


Höhe

Da die hbox selbst nicht flexibel ist, wird sie nur so hoch dargestellt wie ihr höchstes Element. Da ein spacer-Element von sich aus keine Höhe hat, ist das die Schaltfläche, die seine natürliche Größe erhält. Damit wird das unsichtbare spacer-Element ebenfalls auf diese gemeinsame Höhe vergrößert.


Breite

Wenn das Fenster vergrößert / verkleinert wird, wächst / schrumpft die textArea und die Schaltfläche wird in fester Größe unten rechts verankert.

Ist eine hbox / vbox nicht groß genug, um alle nicht flexiblen Elemente gleichzeitig darzustellen, erscheinen Scrollbalken.

agorum.border

Das agorum.border-Layout stellt ein zentrales Element (immer vorhanden, center) und bis zu vier optionale Rand-Elemente (north, south, west, east) dar, die Sie einzeln in ihrer Größe verändern und weglappen können.

Abgesehen davon, dass die Randelemente einzeln in ihrer Größe geändert werden können, sind sie in ihrer Höhe (north / south) und Breite (west / east) statisch. Eventuelle Größenänderungen von außen werden direkt an das center-Element weitergegeben.


Beispiel

Dieses Beispiel verdeutlicht die Aufteilung in die fünf Regionen:

module.exports = {
  type: 'agorum.border',
  width: 600,
  height: 400,
  docked: {
    center: {
      type: 'agorum.textArea',
      text: 'center'
    },
    north: {
      type: 'agorum.textArea',
      text: 'north',
      height: 100
    },
    south: {
      type: 'agorum.textArea',
      text: 'south',
      height: 100
    },
    west: {
      type: 'agorum.textArea',
      text: 'west',
      width: 100
    },
    east: {
      type: 'agorum.textArea',
      text: 'east',
      width: 100
    }
  }
};


Ergebnis

Ergebnis

Das border-Layout wird typischerweise an oberster Ebene einer Widget-Struktur verwendet, um die Darstellung in den Hauptinhalt (center) und zusätzliche Informationen / Bedienelemente zu gliedern.

Hinweis: Im Gegensatz zu den anderen Layout-Arten werden die Elemente des border-Layouts nicht per items, sondern ausschließlich per docked deklariert.

agorum.tabContainer

Das agorum.tabContainer-Layout stellt jedes seiner Elemente in einer eigenen Registerkarte dar. Das title-Attribut des Elements wird dabei als Überschrift des Reiters verwendet.


Beispiel

Dieses Beispiel stellt die beiden vorangegangen Beispiele in zwei getrennten Registerkarten dar:

let tab1 = require('./hello-world-aon');
let tab2 = require('./border-layout');

tab1.title = 'hbox und vbox';
tab2.title = 'border';

module.exports = {
  type: 'agorum.tabContainer',
  width: 800,
  height: 600,
  items: [
    tab1,
    tab2
  ]
};


Ergebnis (Tab1)

Ergebnis Tab1

 

Ergebnis (Tab2)

Ergebnis Tab2

agorum.toolBar

Das agorum.toolBar-Layout stellt seine Elemente (typischerweise Schaltflächen) in einer horizontalen oder vertikalen Toolbar dar.

Container verwenden das toolBar-Layout in der Regel nur für die vier zusätzlichen Regionen top, bottom, left und right, vergleichbar mit north, south, west und east, jedoch immer mit fester Größe ganz am Rand des Containers angedockt.


Beispiel

Dieses Beispiel verdeutlicht die Anordnung der Regionen um eine Erweiterung des border-Beispiels mit allen vier möglichen Toolbar-Positionen:

module.exports = {
  type: 'agorum.border',
  width: 600,
  height: 400,
  docked: {
    top: {
      type: 'agorum.toolBar',
      items: [
        {
          type: 'agorum.button',
          text: 't'
        }
      ]
    },
    bottom: {
      type: 'agorum.toolBar',
      items: [
        {
          type: 'agorum.button',
          text: 'b'
        }
      ]
    },
    left: {
      type: 'agorum.toolBar',
      items: [
        {
          type: 'agorum.button',
          text: 'l'
        }
      ]
    },
    right: {
      type: 'agorum.toolBar',
      items: [
        {
          type: 'agorum.button',
          text: 'r'
        }
      ]
    },
    center: {
      type: 'agorum.textArea',
      text: 'center'
    },
    north: {
      type: 'agorum.textArea',
      text: 'north',
      height: 100
    },
    south: {
      type: 'agorum.textArea',
      text: 'south',
      height: 100
    },
    west: {
      type: 'agorum.textArea',
      text: 'west',
      width: 100
    },
    east: {
      type: 'agorum.textArea',
      text: 'east',
      width: 100
    }
  }
};


Ergebnis

Ergebnis

Verwenden Sie bei left und right in der toolBar nur Schaltflächen mit Symbol und ohne Text, da das System diese sonst unnötig breit darstellt.

agorum.single

Das agorum.single-Layout kann nur ein einzelnes Element darstellen, das den gesamten verfügbaren Platz einnimmt. Dabei versucht es nicht (so wie alle anderen Layout-Arten) zwischen sichtbaren Elementen einen festen Abstand einzuhalten.

Das System setzt das single-Layout nur als Platzhalter ein, um die Einschränkung zu umgehen, dass eingedockte (docked) Elemente von Containern nicht nachträglich geändert werden können. Stattdessen verwendet das System als eingedocktes Element einen single-Container und tauscht dessen item.

Verwenden Sie aufgrund der Besonderheiten bei den Widget-Abständen das single-Layout nur für diesen Zweck.

Im Gegensatz zu den anderen Layouts kann das single-Layout nicht in einem statischen Beispiel demonstriert werden, da es für den dynamischen Austausch von Widgets zur Laufzeit gedacht ist. Daher wird hier nicht wie bisher nur eine Struktur per aon definiert, sondern es wird zusätzlich noch Event-Handling verwendet, das hier noch nicht näher besprochen wird.


Beispiel

let aguila = require('common/aguila');

let widget = aguila.create({
  type: 'agorum.border',
  width: 600,
  height: 400,
  docked: {
    top: {
      type: 'agorum.toolbar',
      items: [
        {
          type: 'agorum.button',
          text: 'Rotate',
          name: 'rotate'
        }
      ]
    },
    west: {
      type: 'agorum.single',
      title: 'west',
      name: 'west',
      width: 100,
      items: [
        {
          type: 'agorum.vbox',
          background: 'white'
        }
      ]
    },
    center: {
      type: 'agorum.single',
      title: 'center',
      name: 'center',
      items: [
        {
          type: 'agorum.vbox',
          background: 'light'
        }
      ]
    },
    east: {
      type: 'agorum.single',
      title: 'east',
      name: 'east',
      width: 100,
      items: [
        {
          type: 'agorum.vbox',
          background: 'dark'
        }
      ]
    }
  }
});

let west = widget.down('west');
let center = widget.down('center');
let east = widget.down('east');

widget.down('rotate').on('clicked', () => {
  let tmp = east.items;

  east.items = center.items;
  center.items = west.items;
  west.items = tmp;
});

widget;


Ergebnis

Ergebnis


Ergebnis (Schaltfläche „Rotate“ 1x geklickt)

Ergebnis (Schaltfläche Rotate 1x geklickt)


Ergebnis (Schaltfläche „Rotate“ noch einmal geklickt)

Ergebnis (Schaltfläche Rotate noch einmal geklickt)

Input-Widgets


Folgende Widgets erwarten eine Eingabe des Benutzers und stellen einen Textinhalt dar:

Input-Widgets

Andere Widgets


Folgende Widgets stellen weder einen Input noch einen Container dar:

Weitere Widgets

Beispiel mit Events


Nachdem die Widget-Struktur als aon (aguila object notation) definiert wurde, können aus ihr konkrete Widgets erzeugt werden, auf deren Ereignisse / Events reagiert werden kann und deren Parameter abgefragt und geändert werden können.


Beispiel

// aguila-Bibliothek einbinden
let aguila = require('common/aguila');

// extern definierte aon-Struktur einbinden
let aon = require('./hello-world-aon');

// Widget(s) aus aon-Struktur erzeugen
let widget = aguila.create(aon);

// später verwendete Unter-Widgets nach ihrem Namen (siehe aon) suchen
let textArea = widget.down('textArea');
let button = widget.down('button');

// Event-Handler für das Event 'clicked' der Schaltflächen definieren
button.on('clicked', () => {
  // aktuellen Text aus der Text Area auslesen und verdoppeln
  textArea.value = textArea.value + ' ' + textArea.value;
});

// Event-Handler für das Event 'valueChanged' der Text Area definieren
textArea.on('valueChanged', value => {
  // aktuellen Text aus der TextArea als Titel des Fensters verwenden, in dem das Widget aktuell angezeigt wird
  widget.form.title = value;
});

/**
// erzeugtes Widget in einem neuen Fenster öffnen
widget.popup({
  title: 'Test mit Events',
  width: 600,
  height: 400
});
/**/
// Alternativ, für den Test im Skript-Editor: Rückgabewert des Skripts == Das Widget, das angezeigt werden soll
widget;


Ergebnis (Schaltfläche 1x geklickt)

Ergebnis (Schaltfläche 1x geklickt)

Beispiel für eigenes Widget


Neben den von agorum core mitgelieferten Typen können Sie eigene Widget-Typen in den folgenden Fällen definieren.

  1. Die Oberfläche ist groß und soll in logische Einheiten (Module) getrennt werden, die nur lose miteinander verbunden sind. Dadurch wird die Lesbarkeit und Wartbarkeit des Codes deutlich erhöht.
  2. Eine Gruppe von Widgets soll zusammen mit ihrem Verhalten an verschiedenen Stellen wiederverwendet werden.

Als Beispiel wird hier Fall 2 besprochen, die zugrundeliegenden Prinzipien sind aber für Fall 1 identisch:

Widget-Konstruktor erstellen

Jedes Mal, wenn eine neue Instanz des neuen Widget-Typs erzeugt werden soll, wird ein definiertes Skript aufgerufen, das als Widget-Konstruktor bezeichnet wird. Dafür bekommt der Konstruktor zusätzlich noch eine Kopie der aon-Struktur, durch die es erzeugt werden soll, in der globalen Variable parameters zur Verfügung gestellt. Als Rückgabewert des Konstruktor-Skripts wird das fertige Widget erwartet.

/* global parameters */

let Calendar = java.util.Calendar;
let LocalDate = java.time.LocalDate;

let aguila = require('common/aguila');

/*
Beschreibung:
type: mein_konfigurationsprojekt.dateTime (Konstruktor für diese Registrierung)

event

properties
   increment
   value
   label
*/

// [A]
// Widgets erzeugen
let widget = aguila.create({
  type: 'agorum.hbox',
  properties: [
    'value',
    'label'
  ],
  items: [
    {
      type: 'agorum.dateInput',
      name: 'date',
      flexible: true
      // FALSCHE Verwendung:
      // das wäre eine falsche Nutzung von "label", da es als properties definiert ist
      // würde aber gehen
      //label: parameters.label
    },
    {
      type: 'agorum.numberInput',
      integer: true,
      name: 'hour',
      width: 50,
      // Dieser Parameter wird beim Aufruf des Widget übergeben - siehe 
      // Beispiel später, wo wir das widget dann aufrufen werden
      // (es wird noch registriert und kann dann verwendet werden)
      // Hier dafür sorgen, dass auch ein nicht gesetzter Parameter richtig funktioniert
      // Also hier den Defaultwert setzen, wenn parameters.increment nicht belegt ist
      increment: parameters.increment || 1,
      
    },
    {
      type: 'agorum.numberInput',
      integer: true,
      name: 'minute',
      width: 50
    },
    {
      type: 'agorum.button',
      name: 'clear',
      icon: 'aguila-icon clear'
    }
  ]
});

console.log('parameters: ' + JSON.stringify(parameters, null, 4));

// Referenzen auf verwendete Widgets hinterlegen
let date = widget.down('date');
let hour = widget.down('hour');
let minute = widget.down('minute');
let clear = widget.down('clear');

// [B]
// Event-Handler für alle Properties
widget
// Wenn sich Property "value" ändert, neuen Wert auf die einzelnen Felder date, hour und minute verteilen
.on('valueChanged', value => {
  if (value) {
    let cal = Calendar.instance;

    cal.time = new Date(value);
    
    date.value = LocalDate.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH));
    hour.value = cal.get(Calendar.HOUR_OF_DAY);
    minute.value = cal.get(Calendar.MINUTE);
  }
  else {
    date.value = null;
    hour.value = null;
    minute.value = null;
  }
})
// Wenn sich Property "label" ändert, neuen Wert als Beschriftung des date-Felds übernehmen
.on('labelChanged', label => date.label = label);

// [C]
// Alle Felder zurücksetzen
clear.on('clicked', () => {
  //widget.value = null;
  
  date.value = null;
  hour.value = null;
  minute.value = null;
  
});


// Interne handler - hier ein Beispiel mit mehreren Handlern, auf ein Event 
// hour und minute - hier registriueren 2 x "valueChanged"
// die abarbeitung erfolgt hier in der Reihenfolge der Registrierung

// Wertebereich des Felds "hour" einschränken
hour.on('valueChanged', v => {
  if (v < 0) v += 24;
  if (v > 23) v -= 24;

  hour.value = v;
});

// Wertebereich des Felds "minute" einschränken
minute.on('valueChanged', v => {
  if (v < 0) v += 60;
  if (v > 59) v -= 60;

  minute.value = v;
});

// Bei Änderung eines Felds die Werte aller Felder lesen und als Wert von Property "value" zusammenfassen
let calcDate = () => {
  if (date.value) {
    let cal = Calendar.instance;

    cal.time = new Date(date.value);

    cal.set(Calendar.HOUR_OF_DAY, hour.value || 0);
    cal.set(Calendar.MINUTE, minute.value || 0);
    cal.set(Calendar.SECOND, 0);
    cal.set(Calendar.MILLISECOND, 0);
    
    widget.value = cal.time;
  }
  else {
    widget.value = null;
  }
};

date.on('valueChanged', calcDate);
hour.on('valueChanged', calcDate);
minute.on('valueChanged', calcDate);

/**
setImmediate(() => {
  widget.label = 'Hallo';
  widget.value = new Date();
});
/***/

// [D]
widget;

Das durch diesen Konstruktor erzeugte Widget stellt eine kombinierte Datums- und Uhrzeit-Eingabe (Stunden + Minuten) dar, grob zerlegt in folgende Schritte.

Äußeres Widget erstellen (A)

Benutzerdefinierte Widgets sind ihrerseits immer aus anderen Widgets zusammengesetzt. Meistens ist das ein äußerer Container (hier die umgebende hbox) zusammen mit seinen Elementen. Dieser Container stellt das äußere Widget dar, dem eine besondere Bedeutung zukommt: Es ist das Widget, das dem Aufrufer zurückgegeben wird, und ist daher für beide Seiten direkt sichtbar. An diesem Widget findet daher der Datenaustausch in beide Richtungen statt.

In unserem Fall werden dafür auf diesem Widget zwei benutzerdefinierte Parameter value und label definiert, die eine hbox normalerweise nicht hat und mit der uns sowohl der Aufrufer mitteilen kann, was Beschriftung und Inhalt unseres Widgets sein soll als auch wir den Aufrufer über den aktuellen Inhalt in Kenntnis setzen können.

Für Widgets dieser Art, die einen einzelnen Wert darstellen, hat es sich eingebürgert, diesen im Parameter value zu hinterlegen. Das hat für den Aufrufer den Vorteil, dass er etwa über mehrere Felder iterieren und jeweils .value abrufen kann, ohne den Feldtyp vorher zu prüfen. Außerdem ist es so einfacher, später einzelne Widgets auszutauschen, ohne die Logik zu ändern.

Zwei weitere Möglichkeiten des Datenaustauschs zwischen Aufrufer und Widget sind Parameter (in dem Beispiel increment) und explizite Events:

Property-Events behandeln (B)

Bei jeder Änderung eines als Property xyz definierten Werts am äußeren Widget wird automatisch das Event xyzChanged ausgelöst mit dem neuen Wert als Parameter. Dieses Event muss von einem Widget für jedes von ihm definierte Property verarbeitet werden. In diesem Fall sind das valueChanged und labelChanged, die jeweils an die zugehörigen Unter-Widgets verteilt / weitergegeben werden.

Diese Events werden auch ausgelöst, wenn diese Werte von innen durch das Widget selbst geändert werden, sodass auch der Aufrufer benachrichtigt wird, wenn sich etwa der Wert ändert.

Widget-Events behandeln (C)

Danach wird das Zusammenspiel der Unter-Widgets durch entsprechende Event-Handler festgelegt. Hierbei wird bei einer Änderung des nach außen sichtbaren Werts stets widget.value angepasst.

In diesem Beispiel ist außerdem zu sehen, dass aus Gründen der Übersichtlichkeit die Beschränkung des Wertebereichs von hour und minute und die Neuberechnung des Datum-/Uhrzeit-Werts in getrennte Event-Handler ausgelagert wurden. Werden für dasselbe Event mehrere Event-Handler definiert, so werden diese in der Reihenfolge ihrer Definition aufgerufen (hier also zuerst die Beschränkung und danach die Berechnung).

Äußeres Widget zurückgeben (D)

Der Rückgabewert des Skripts muss das oben erzeugte äußere Widget sein. In JavaScript ist der Rückgabewert eines Skripts der Rückgabewert der letzten ausgeführten Anweisung, daher genügt hier die Angabe der Variablen widget.

Eigenes Widget registrieren

Um das Widget verwenden zu können, muss es unter einem geeigneten Namen registriert werden. Für diesen Namen gelten folgende Einschränkungen:


Beispiele für gültige Namen

Da Widgets im Allgemeinen wiederverwendbar sein sollten, sollte hier ein möglichst eindeutiger Name gewählt werden, um spätere Konflikte zu vermeiden.

  1. Erstellen Sie in der MetaDb ein Property-Bunde mit einem Property-Entry unter folgendem Pfad:
    MAIN_MODULE_MANAGEMENT/aguila/control/widgets/<Widget-Name>
  2. Verweisen Sie dort auf das Skript:
    <Pfad zum Konstruktor-Skript>
  3. Aktualisieren Sie den Widget-Cache, etwa durch den Schaltfläche Initialize im Skript-Editor oder durch einen Neustart von agorum core.

Eigenes Widget per agorum core template manager registrieren

Zur einfachen Registrierung von neuen Widgets kann auch der agorum core template manager verwendet werden.

Dieser erwartet Widget-Konstruktoren in der folgenden normierten Ablagestruktur:

/agorum/roi/customers/<Projektname>/.../aguila/<Segment1>/../<Widget-Konstruktor>.js

Der Name des Widgets wird automatisch aus dem Namen des Projekts, der Ordner unterhalb des Ordners aguila und dem Dateinamen des Widget-Konstruktors gebildet. Für das Widget aus unserem Beispiel war das etwa:

/agorum/roi/customers/mein_konfigurationsprojekt‎/doc‎/Einführung‎/aguila‎/date-time.js

Dies führt zu folgendem Namen:

mein_konfigurationsprojekt.dateTime

Eigenes Widget verwenden

Zum Abschluss hier noch ein Beispiel für die Verwendung des gerade erstellten Widget-Typs:

let aguila = require('common/aguila');
let templates = require('common/templates');

// Widgets erzeugen
let widget = aguila.create({
  type: 'agorum.vbox',
  items: [
    {
      type: 'agorum.hbox',
      items: [
        {
          label: 'Datum/Uhrzeit',         // Belegung properties "label"
          type: 'mein_konfigurationsprojekt.dateTime',
          name: 'input',
          flexible: true,
          increment: 2,      // Belegung parameters "increment"
          // value: new Date()  // Vorbelegung des properties "value"
        },
        {
          type: 'agorum.button',
          name: 'now',
          text: 'Now'
        }
      ]
    },
    {
      type: 'agorum.textDisplay',
      label: 'Eingabe',
      name: 'display'
    }
  ]
});

// Referenzen auf verwendete Widgets hinterlegen
let input = widget.down('input');
let display = widget.down('display');
let now = widget.down('now');

// Bei einem Klick auf "now" den Wert des Eingabefelds auf den aktuellen Zeitpunkt setzen
now.on('clicked', () => input.value = new Date());

// Wenn sich der Wert des Eingabefelds ändert, diesen als String im Anzeigefeld darstellen
let format = value => templates.fill('${value:dd.MM.yyyy HH:mm}', { value: value });

input.on('valueChanged', value => display.value = value ? format(value) : '');

widget;

Zur Illustration wurde hier noch eine weitere Schaltfläche namens now ergänzt, mit dem der aktuelle Zeitpunkt eingesetzt wird, außerdem ein Anzeigefeld display, das bei valueChanged aktualisiert wird.

Beispiel dataTree

Hier noch ein weiteres Beispiel für ein eigenes Widget. Im Gegensatz zu dem eingebauten Widget agorum.basicTree übernimmt dieser Baum zusätzlich die Verwaltung des übergebenen Baums. Das bedeutet, dass im Gegensatz zu basicTree bei Events und so weiter nicht IDs verwendet werden, sondern die tatsächlichen Knoten des Baums. Gerade bei Konfigurationsbäumen ist dieses Verhalten typischerweise praktischer:

/* global parameters */

let aguila = require('common/aguila');

let widget = aguila.create({
  type: 'agorum.border',
  width: 800,
  height: 600,
  docked: {
    west: {
      type: 'mein_konfigurationsprojekt.dataTree',
      name: 'tree',
      width: 200,
      border: true,
      // hideRoot: false
    },
    center: {
      type: 'agorum.textArea',
      name: 'display',
      border: true,
      monospace: true
    }
  }
});

let tree = widget.down('tree');
let display = widget.down('display');

tree.data = {
  text: 'root',
  expanded: true,
  items: [
    {
      text: 'root/item1',
      expanded: true,
      items: [
        {
          text: 'root/item1/item2'
        }
      ]
    },
    {
      text: 'root/item2'
    }
  ]
};

tree.on('selectionChanged', nodes => {
  display.value = JSON.stringify(nodes, null, 2);
});

// Hier zum zeigen, die Events des agorum.basicTree werden durchgeschleust und kommen auch hier an.
[
  'nodeClicked',
  'nodeDblClicked',
  'nodeRightClicked',
  'nodeExpanded',
  'nodeCollapsed'
]
//.forEach(event => tree.on(event, id => widget.fire(event, nodes[id])));
.forEach(event => tree.on(event, node => {
  console.log('Aussen: + ' + event + ': "' + node.text + '"');
}));

/**/
// Timeout führt alles nach 1 Sec. aus
tree.setTimeout(() => {
  // Hier machen wir einen weiteren Knoten in den Baum
  //
  let item = {
    text: 'root/item3'
  };

  // Knoten wird in den Baum gesetzt
  tree.data.items.push(item);
  tree.fire('refresh');

  tree.selection = [ item ];
}, 2000);
/**/


widget;

Wie Sie sehen, bekommt das selectionChanged-Event als Parameter nun den vollständigen Knoten zurück und kann direkt damit arbeiten.

/* global parameters */

let aguila = require('common/aguila');

let widget = aguila.create({
  type: 'agorum.single',
  width: 300,
  height: 500,
  properties: [
    'data',
    'selection'
  ],
  items: [
    {
      type: 'agorum.basicTree',
      name: 'tree',
      hideRoot: parameters.hideRoot
    }
  ]
});

let tree = widget.down('tree');

// internal node management
let id, nodes;

// Diese Funktion macht aus einem Baum eine Array, auf das
// Direkt mit der id zugegriffen werden kann
// Der Baum wird neu durchnummeriert
let decorate = node => {
  // Hier wird eine Kopie gemacht, von node in myNode
  let myNode = Object.assign({}, node);

  // Hier wird die id hochgezählt
  nodes[myNode.id = (id++).toFixed(0)] = node;
  
  if (node.items) {
    myNode.items = node.items.map(item => decorate(item) );
//    myNode.items = node.items.map(decorate);
  }

  return myNode;
};

// two-way property value replication
let replicate = (property, transformIn, transformOut) => {
  // echo suppression
  let replicating;

  let single = callback => {
    if (!replicating) {
      replicating = true;

      try {
        callback();
      }
      finally {
        replicating = false;
      }
    }
  };

  // in
  widget.on(property + 'Changed', value => single(() => tree[property] = transformIn(value)));

  // out
  tree.on(property + 'Changed', value => single(() => widget[property] = transformOut(value)));
};

// replicate "data" (inward only)
let update = data => {
  data = data || widget.data;

  id = 0;
  nodes = {};

  tree.data = decorate(data); // Nach diesem Aufruf ist der Baum neu durchnummeriert (id)
                              // und in einem Array (nodes) enthalten.
  console.log('nodes:' + JSON.stringify(nodes,null, 4) );
};

widget.on('dataChanged', update);
widget.on('refresh', update);

// replicate "selection" (inward and outward)
let transformIn = selection => selection.map(node => {
  console.log('transformIn: ' + JSON.stringify(selection, null, 4));
  for (let id in nodes) {
    if (nodes[id] === node) {
      return id;
    }
  }
})
// ignore missing nodes
.filter(id => id !== undefined);

let transformOut = selection => {
  console.log('transformOut: ' + JSON.stringify(selection, null, 4));
  return selection.map(id => nodes[id]);
};

replicate('selection', transformIn, transformOut);

// forward events while translating IDs to nodes (outward only)
[
  'nodeClicked',
  'nodeDblClicked',
  'nodeRightClicked',
  'nodeExpanded',
  'nodeCollapsed'
]
//.forEach(event => tree.on(event, id => widget.fire(event, nodes[id])));
.forEach(event => tree.on(event, id => {
  widget.fire(event, nodes[id]);
  console.log(event + ': "' + id + '"');
}));

widget;

Zur Isolation der Events des inneren basicTree-Widgets und des äußeren dataTree-Widgets wurde hier ein zusätzlicher agorum.single-Container verwendet, der den eigentlichen Baum enthält.

Der Rest des Codes ist prinzipiell die Übersetzung der Parameter und Events von ID-basiert (basicTree) zu Daten-basiert (dataTree) zusammen mit der Verwaltung der Baumknoten.

Die Funktion replicate ist ein Beispiel dafür, wie eventuelle Echos bei der Replikation von Parametern verhindert werden können. Wird etwa der innere Wert von selection geändert (ein Array von IDs) und dadurch der äußere Wert von selection neu berechnet (ein Array von Knoten-Objekten), dann wird dadurch nicht auch eine unnötige Neuberechnung des inneren Werts angestoßen.

UI-Thread


aguila arbeitet – wie praktisch alle UI-Frameworks – zur Garantie der Ereignisreihenfolge Single-Threaded: Pro aktivem Client (Browserfenster) wird maximal ein zugehöriger (serverseitiger) UI-Thread erzeugt. Um effektiv mit aguila arbeiten zu können, ist es hilfreich, die grundsätzliche Arbeitsweise eines UI-Threads zu verstehen. 

Der UI-Thread arbeitet in Zyklen:

Benötigt nun die Verarbeitung eines einzelnen Events (Tick) besonders lange, müssen alle darauffolgenden Events warten und auch die Aktualisierung der Oberfläche auf dem Client ist blockiert, da Signale erst am Ende zugestellt werden können. Daher gibt es die Möglichkeit, für zeitintensive Aktionen einen separaten Thread zu starten (fork) und das Ergebnis später wieder im UI-Thread zu verwenden. Der Zugriff auf vorhandene Widgets und die Erzeugung neuer Widgets darf dabei ausschließlich innerhalb des UI-Threads stattfinden.

Komplettes Beispiel mit Code

Das untere Schaubild basiert auf folgendem Code. Zum Ausführen muss das Widget aguila-flow.js registriert und der Name des Widgets in die Datei aguila-flow-start.js im aon Teil (mein_konfigurationsprojekt.aguilaFlow) ausgetauscht werden.

Die im Code vorhandenen Kommentare sind im unteren Schaubild zu finden und zeigen den kompletten Ablauf der Widget-Erzeugung, der Events, Parameter und von setImmediate.


aguila-flow-start.js

// Vor dem Starten mit F12 die Browser-Console öffnen und leeren

// Widget Erzeugung startet
let aguila = require('common/aguila');

console.log('01: aguila-flow-start start');

// Es wird ein Widget erzeugt und ein weiteres Widget eingebunden: aguilaFlow
let widget = aguila.create({
  type: 'agorum.vbox',
  width: 200,
  height: 100,
  items: [
    {
      type: 'mein_konfigurationsprojekt.aguilaFlow',
      
      // propertyEins wird durch aon gesetzt auf aguilaFlow (Reihenfolge nicht definiert)
      propertyEins: 'Wert von aussen 1',
      
      // propertyZwei wird durch aon gesetzt auf aguilaFlow (Reihenfolge nicht definiert)
      propertyZwei: 'Wert von aussen 2',
    }
  ]
});

// setImmediate(4) wird definiert
setImmediate(() => {
  console.log('16: setImmediate aguila-flow-start');
});

// Widget Erzeugung beendet
console.log('09: aguila-flow-start ende');
widget;


aguila-flow.js

// Widget Erzeugung startet
let aguila = require('common/aguila');
console.log('02: aguila-flow start');

let widget = aguila.create({
  type: 'agorum.vbox',
  properties: [
    'propertyEins',
    'propertyZwei'
  ],
  items: [
    {
      type: 'agorum.textDisplay',
      name: 'display'
    }
  ]
});

let display = widget.down('display');

widget.on('propertyEinsChanged', value => {
  // Event propertyEinsChanged wird aufgerufen
  console.log('03/07: propertyEinsChanged: ' + value);
  
  // propertyZwei wird gesetzt
  widget.propertyZwei = 'Wert 2';
  
  display.value = value;
  
  // setImmediate(1) wird definiert
  setImmediate(() => {
    console.log('11/15: setImmediate(1) aus propertyEinsChanged mit Wert: ' + value);
  });
});

widget.on('propertyZweiChanged', value => {
  // Event propertyZweiChanged wird aufgerufen
  console.log('04/06/08: propertyZweiChanged: ' + value);
  display.value = value;
  
  // setImmediate(2) wird definiert
  setImmediate(() => {
    console.log('10/13/14: setImmediate(2) aus propertyZweiChanged mit Wert: ' + value);
  });
});

// propertyEins wird gesetzt
widget.propertyEins = 'Wert 1';
display.value = 'Wert start';

// setImmediate(3) wird definiert
setImmediate(() => {
  console.log('12: setImmediate(3) aguila-flow');
});

console.log('05: aguila-flow ende');

// Widget-Erzeugung Ende
widget;


Ablaufdiagramm zu den obigen Programmen

Beispiel mit aguila.fork()


In fast allen Fällen wird aguila.fork() dafür verwendet, um I/O-Operationen (Datenbank, Index, externe Datenquellen) in einem separaten Thread auszuführen. Grundsätzlich gilt: Im UI-Thread sollte ausschließlich mit In-Memory-Ressourcen gearbeitet werden.

Beispiel für eine einfache Suchoberfläche

let aguila = require('common/aguila');
let objects = require('common/objects');

let widget = aguila.create({
  type: 'agorum.vbox',
  items: [
    {
      type: 'agorum.textInput',
      name: 'query'
    },
    {
      type: 'agorum.basicGrid',
      name: 'results',
      flexible: true,
      border: true,
      columns: [
        {
          text: 'ID',
          name: 'id',
          width: 75
        },
        {
          text: 'Name',
          name: 'name'
        }
      ]
    }
  ]
});

let query = widget.down('query');
let results = widget.down('results');

// auf die Eingabetaste im Suchfeld reagieren
query.on('enter', () => {
  // [A]
  // aktuellen Wert des Widgets zuvor im UI-Thread abrufen
  let q = query.value + ' inpath:${ID:/agorum/roi/customers/mein_konfigurationsprojekt} ';

  aguila
  .fork(() => {
    // [B]
    // Suche in separatem Thread durchführen und Ergebnis zurückliefern
    return objects.query(q).sort('name').limit(100).search('id', 'name');
  })
  .then(result => {
    // [C]
    // Suchergebnis im UI-Thread verwenden, um Fenstertitel und Inhalt der Tabelle zu aktualisieren
    widget.form.title = q + ': ' + result.total + ' Treffer';

    // Anmerkung: Da die Feldnamen mit den Spaltennamen übereinstimmen, kann das Suchergebnis ohne Transformation verwendet werden
    results.rows = result.rows;
  })
  .finally(() => {
    // [D]
    // Eingabefeld in jedem Fall wieder freigeben, nachdem die Suche beendet ist, auch nach einem Fehler
    query.locked = null;
  });

  // [E]
  // Eingabefeld sperren (ohne Text)
  query.locked = '';
});

// UI-Thread     A > E ..... C > D
//                \         /
// fork-Thread     B -------

// Widget in neuem Fenster öffnen
widget.popup({
  title: 'I/O mit aguila',
  width: 400,
  height: 600
});

// Tastaturfokus initial auf das Eingabefeld setzen
query.focus();

Zur einfacheren Referenz wurden die relevanten Code-Regionen im Event-Handler für enter mit den Buchstaben A-E bezeichnet.

Reihenfolge

Für die Reihenfolge, in der diese Regionen in welchem Thread ausgeführt werden, gilt:

Aufgaben verteilen

Daraus ergibt sich für die Regionen folgende Aufteilung:

Oft genügen A, B und C oder sogar nur B und C.

Einschränkungen

Wie schon erwähnt, gelten einige Einschränkungen für Code, der nicht auf dem UI-Thread ausgeführt wird:

Promises

Die Syntax für .then() und .finally() ist übrigens nicht aguila-spezifisch, sondern rührt daher, dass aguila.fork() ein Promise-Objekt zurückliefert (siehe https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Dadurch ist es auch einfach möglich, mehrere asynchron ausgeführte Prozesse miteinander zu verketten/verknüpfen, etwa das Warten auf die Ergebnisse mehrerer parallel per aguila.fork() gestarteter Prozesse per Promise.all() (siehe https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all).

Beispiel verschachtelter Promises

let aguila = require('common/aguila');

// ACHTUNG:
// Wenn das Script gestartet wird, sollte mit F12 die Console eingeblendet werden.
// Dort sieht man dann an der Ausgabe, wie das Sript abläuft.
//

console.log('vor fork');

aguila.fork(() => {
  console.log('fork 1');
  java.lang.Thread.sleep(1000);
  console.log('fork 1 end');
})
.then(() => {
  console.log('then 1');
  // Wenn der return ein fork ist, dann bezieht sich 
  // das nächste then auf diesen fork.
  // Wenn in diesem fork auch ein Wert returnt wird,
  // wird dieser in das nächste then übergeben
  return aguila.fork(() => {
    console.log('fork 2');
    java.lang.Thread.sleep(1000);
    console.log('fork 2 end');    
    // dieser Fehler wird durchgeschleust in den catch(err)
    //let i; i.hallo();
    return 'zurück';
  })
  .finally(() => {
    console.log('then 1 finally');
  });
})
.then(p => {
  console.log('then 2: ' + p); // p = return fork 2
})
.catch(err => {
  // egal wo ein error passiert,
  // das kommt dann hier an
  console.log('error: ' + err);
})
.finally(() => {
  // das finally wird erst aufgerufen wenn auch alle unter-forks erledigt sind
  // also tatsächlich erst am ende, wenn alle forks abgeschlossen sind
  console.log('finally');
});

console.log('nach fork');

Wenn dieses Beispiel gestartet wird und die Konsole mit F12 aktiv ist, erhalten Sie folgende Ausgabe:

vor fork
nach fork
fork 1
fork 1 end
then 1
fork 2
fork 2 end
then 1 finally
then 2: zurück
finally

Anhand dieser Ausgabe sehen Sie, wie der fork genau abläuft und wann was gemacht wird.

widget.synchronizer()

In manchen Situationen ist es wünschenswert, dass von mehreren Stellen im Code aus auf denselben asynchronen Prozess zugegriffen werden kann. Die Funktion synchronizer(), die auf jedem Widget aufgerufen werden kann, liefert zu diesem Zweck wiederverwendbares Promise-Objekt zurück. Synchronizer-Instanzen sind jeweils an das initiale Widget gebunden: Wird dieses zerstört, dann werden keine weiteren Callbacks darauf mehr ausgeführt, auch wenn noch asynchrone Operationen laufen. Im Folgenden zwei Beispiele für typische Einsatzszenarien:

Warten auf einen asynchronen Prozess, der an einer anderen Stelle ausgelöst wurde:

let initializer = widget.synchronizer();

initializer.fork(() => {
  // längere Initialisierungs-Aufgabe
});

function a() {
  // ...
  initializer.then(() => {
    // ausführen, wenn Initialisierung erfolgreich abgeschlossen ist
  });
  // ...
}

function b() {
  // ...
  initializer.finally(() => {
    // ausführen, wenn Initialisierung abgeschlossen ist
  });
  // ...
}

Sicherstellen, dass nur das Ergebnis des jeweils zuletzt gestarteten asynchronen Prozesses beachtet wird:

let loader = widget.synchronizer();

widget.on('idChanged', id => {
  loader
  .fork(() => {
    // Benötigte Daten vom Objekt laden und zurückgeben
  })
  .then(result => {
    // Geladene Daten verwenden
  });
});

Die per then() übergebene Funktion wird hier nur aufgerufen, wenn inzwischen kein neuer fork() auf dem Objekt loader gestartet wurde und das Widget noch existiert. Somit wird sichergestellt, dass nur die jeweils letzte Änderung der Property-id beachtet wird, auch wenn sich der Wert schneller ändert als der Ladevorgang dauert. Zudem wird die Bearbeitung automatisch abgebrochen, wenn das Widget geschlossen wird.

aguila.enter(<function>)

Unter Umständen ist es erforderlich, gezielt Code auf dem UI-Thread auszuführen, diesen also von außen zu betreten. Typische Anwendungsbeispiele hierfür sind:

Hier ein Beispiel für die Einbindung im agorum core smart assistant:

Es soll ein Widget als Pop-up-Fenster ins Kontextmenü eingebunden werden, dazu werden zwei Komponenten benötigt:

  1. Registriertes Widget 
  2. JavaScript der Aktion im agorum core smart assistant

Das Widget wird über den agorum core template manager registriert, in diesem Beispiel testvt.test3. Um aguila-Widgets im agorum core smart assistant zu verwenden, muss vorher per aguila.enter() auf den aguila-Kontext-Thread gewechselt werden. Dann erst kann das Widget mit widget.popup in einem Pop-up-Fenster angezeigt werden.

/* global sc, sca, folder, objects, data */
let beans = require('common/beans');
let aguila = require('common/aguila');

aguila.enter(() => {
  
  let widget = aguila.create({
    type: 'testvt.test3'
  });
  
  widget.popup({
    title: 'Test Tree',
    width: 600,
    height: 400
  });

});

Nachfolgend ein Beispiel für die Anzeige von Zwischenergebnissen:

let aguila = require('common/aguila');

let widget = aguila.create({
  type: 'agorum.vbox',
  background: 'light',
  items: [
    {
      type: 'agorum.textInput',
      name: 'text',
      width: 200
    }
  ]
});

let text = widget.down('text');

// Funktion definieren, die im UI-Thread die "locked"-Property des Widgets aktualisiert
let lock = str => aguila.enter(() => widget.locked = str);

aguila
.fork(() => {
  // Eingabe sperren
  lock('Bitte warten...');

  // längeren Prozess simulieren
  for (let i = 0; i < 100; ++i) {
    java.lang.Thread.sleep(100);

    // periodisch den Fortschritt anzeigen
    lock('Bitte warten... (' + i.toFixed(0) + '%)');
  }

  // Eingabe freigeben
  lock();

  // "Ergebnis"
  return 100;
})
.then(result => {
  // Ergebnis anzeigen
  text.value = 'Fertig, Ergebnis: ' + result;
});

widget;

Beispiel für verzögerte Ausführung


Manchmal kann es nötig sein, eine Funktion nicht sofort auszuführen, sondern erst später oder sogar periodisch.

Aufgrund der asynchronen Natur dieser Operationen kann es sein, dass aufgrund von Änderungen (Fenster geschlossen und so weiter) die Ergebnisse des Funktionsaufrufs nicht mehr relevant sind und die Funktion nicht aufgerufen werden müsste. Insbesondere bei periodisch auszuführenden Operationen ist die Wahrscheinlichkeit hoch, dass diese wieder gestoppt werden müssen. Um diese Verwaltung zu vereinfachen, werden die betroffenen Operationen (mit einer Ausnahme) stets an die Lebensdauer eines existierenden Widgets gebunden. Wird dieses Widget zerstört, wird gleichzeitig auch ein eventuell noch laufender Timer deaktiviert.

Die hier beschriebenen Operationen führen die jeweils übergebene Funktion immer im UI-Thread aus. Zeitaufwändige Aufgaben müssen also trotzdem über aguila.fork() ausgelagert werden.

widget.setTimeout(<function>, <time>)

Durch setTimeout() kann eine übergebene Funktion später (definiert in ms) einmalig ausgeführt werden.

Ein typisches Anwendungsbeispiel hierfür ist automatisches Zwischenspeichern von Eingaben nach einer definierten Zeit nach der letzten Eingabe des Benutzers:

let aguila = require('common/aguila');

let widget = aguila.create({
  type: 'agorum.border',
  width: 800,
  height: 600,
  docked: {
    center: {
      type: 'agorum.textArea',
      name: 'input'
    },
    bottom: {
      type: 'agorum.toolbar',
      items: [
        {
          type: 'agorum.spacer'
        },
        {
          name: 'status',
          type: 'agorum.textDisplay'
        }
      ]
    }
  }
});

let input = widget.down('input');
let status = widget.down('status');

// Erzeugt aus "callback" eine gepufferte Funktion, die erst nach der angegebenen Verzögerung gestartet wird.
// Wird die gepufferte Funktion dazwischen erneut aufgerufen, startet die Verzögerung erneut.
let buffered = (widget, callback, timeout) => {
  let waiting;

  return function() {
    /* jshint -W120 */
    let me = waiting = {};
    /* jshint +W120 */

    widget.setTimeout(() => {
      if (waiting === me) {
        return callback.apply(this, arguments);
      }
    }, timeout);
  };
};

input.on('valueChanged', buffered(input, value => status.value = 'Gespeichert: ' + value.length + ' Zeichen', 2000));

widget;

widget.setInterval(<function>, <time>)

Die Syntax von setInterval() ist dieselbe wie die von setTimeout(), die übergebene Funktion wird aber stattdessen periodisch ausgeführt statt nur einmal.

Eine typische Anwendung von setInterval() ist die regelmäßige Prüfung (polling) eines Zustands, um ggf. bei Änderungen die Bedienoberfläche zu aktualisieren:

let aguila = require('common/aguila');
let objects = require('common/objects');

let widget = aguila.create({
  type: 'agorum.border',
  width: 800,
  height: 600,
  docked: {
    center: {
      type: 'agorum.textArea',
      name: 'input',
      monospace: true
    },
    bottom: {
      type: 'agorum.toolbar',
      items: [
        {
          type: 'agorum.spacer'
        },
        {
          name: 'status',
          type: 'agorum.textDisplay'
        }
      ]
    }
  }
});

let input = widget.down('input');
let status = widget.down('status');

let names;

let refresh = () => aguila
// alle Objekte im Home-Verzeichnis auflisten und deren Namen mit \n getrennt zurückgeben
.fork(() => objects.find('home:MyFiles').items().map(item => item.name).join('\n'))
.then(newNames => {
  if (newNames !== names) {
    // bei Änderung: Eingabefeld aktualisieren
    names = newNames;

    input.value = 'Objekte im Home-Verzeichnis:\n\n' + names;
    status.value = 'Änderung';
  }
  else {
    status.value = 'Keine Änderung';
  }
});

// direkt zu Anfang aktualisieren
refresh();

// periodisch aktualisieren (bis widget geschlossen wird)
widget.setInterval(refresh, 1000);

widget;

setImmediate(<function>)

Im Gegensatz zu setTimeout() und setInterval() wird die hier übergebene Funktion so bald wie möglich, aber dennoch später ausgeführt. Genauer gesagt ist das direkt nach Ende des aktuellen Ticks (und nach Ende eventuell zuvor eingereihter setImmediate()-Ausführungen), aber garantiert vor der Übertragung des neuen Zustands an den Client.

Die häufigste Verwendung findet setImmediate() in den folgenden beschriebenen Szenarien:

Operationen nach Erzeugung des vollständigen Widget-Baums ausführen

Während der Ausführung eines Widget-Konstruktors (siehe auch Beispiel für ein eigenes Widget) wurde die komplette Struktur noch nicht erzeugt, deren Teil dieses Widget ist. Auch eventuelle Event-Handler von außen sind noch nicht gebunden. Manchmal ist es aber nötig, nach Abschluss des gesamten Konstruktionsprozesses noch weitere Operationen durchzuführen, die etwa auf Widgets außerhalb zugreifen.

Ein typisches Beispiel hierfür ist das Setzen des Fenstertitels durch das Widget, das darin angezeigt wird:

let aguila = require('common/aguila');

let widget = aguila.create({
  type: 'agorum.vbox',
  width: 300,
  height: 200
});

// nach Konstruktion des vollständigen Widget-Baums inklusive umgebender Form deren Titel anpassen
setImmediate(() => widget.form.title = 'Geänderter Titel');

widget;

Hinweis: Da der Client erst aktualisiert wird, nachdem alle setImmediate()-Blöcke ausgeführt wurden, gibt es keine unschöne, für den Benutzer sichtbare Änderung des Fenstertitels – erst der finale Zustand wird übertragen und angezeigt.

Auf mehrere Events gleichzeitig reagieren

Bei Widgets, die mehrere Parameter nach außen veröffentlichen, kann es hilfreich sein, Änderungen nicht einzeln, sondern gesammelt zu betrachten, wenn der Aufrufer zwei oder mehr Parameter nacheinander ändert. Über setImmediate() kann bei Änderungen abgewartet werden, ob im selben Tick noch weitere Änderungen durchgeführt und diese dann danach einmalig abgearbeitet werden.

Nachfolgend ein Beispiel für ein Widget mit drei Parametern, nach deren Änderung der Inhalt eines Textfelds neu berechnet werden soll:

let aguila = require('common/aguila');

let widget = aguila.create({
  type: 'agorum.vbox',
  properties: [
    'a',
    'b',
    'c'
  ],
  background: 'light',
  items: [
    {
      type: 'agorum.textInput',
      name: 'text',
      width: 200
    }
  ]
});

let text = widget.down('text');

// Erzeugt aus "callback" eine verzögerte Funktion, die im selben Tick nur einmal gestartet wird.
// Erneute Aufrufe der verzögerten Funktion werden ignoriert.
let later = callback => {
  let wasCalled;

  return () => {
    if (!wasCalled) {
      wasCalled = true;

      setImmediate(() => {
        wasCalled = false;

        callback();
      });
    }
  };
};

// Neuberechnung von text.value aus den Properies a, b und c
let recalc = () => {
  console.log('recalc() called', widget.a, widget.b, widget.c);

  text.value = widget.a + widget.b + widget.c;
};

// Neuberechnung nur einmal pro Tick ausführen statt jedes Mal
// Hier demo - anklemmen 
//recalc = later(recalc);

widget.on('aChanged', recalc);
widget.on('bChanged', recalc);
widget.on('cChanged', recalc);

widget.a = 100;
widget.b = 200;
widget.c = 300;

widget.setTimeout(() => {
  widget.a = 600;
  widget.c = 900;
}, 1000);

widget.setTimeout(() => {
  widget.b = 1600;
}, 500);

widget;

aguila.pub()/widget.sub()


Für die Kommunikation von Widgets mit anderen Widgets, die außerhalb ihres Kontexts liegen (etwa in einer anderen Registerkarte, einem anderen Browser oder sogar bei einem anderen Benutzer) oder mit Hintergrundprozessen wie Workern wird von agorum core ein einfaches Mittel bereitgestellt, das asynchronen Austausch von Nachrichten auf frei definierbaren Kanälen zulässt.

aguila.pub(<channel>, <message>)

Die Funktion aguila.pub() (die auch außerhalb von aguila-Kontexten, etwa in einem Worker, verwendet werden kann) sendet die angegebene Nachricht auf dem angegebenen Kanal, sobald die aktuelle Transaktion erfolgreich abgeschlossen wurde. Damit ist sichergestellt, dass Nachrichten, die etwa auf Änderungen hinweisen, erst zugestellt werden, nachdem diese Änderungen sichtbar werden.

Der Kanal wird durch einen beliebigen String identifiziert, muss nicht gesondert angelegt oder wieder gelöscht werden und gilt immer auf dem gesamten Server. Soll ein Kanal stattdessen spezifisch für einen Benutzer sein, so kann etwa dessen UUID an den Namen angehängt werden.

Die Nachricht kann prinzipiell ein beliebiges Objekt sein – es ist jedoch empfohlen, hier ausschließlich statische Daten zu übertragen (etwa die UUID eines Ordners statt das Ordnerobjekt selbst), um Sender und Empfänger nicht unnötig zu koppeln. Ein Ordnerobjekt enthält etwa einen Verweis auf die Sitzung, aus der dieses Objekt stammt und die meistens nicht mit übertragen werden sollte.

widget.sub(<channel>, <handler>)

Die Registrierung eines Empfängers auf einem Kanal läuft analog zur Registrierung eines Event-Handlers ab: Es wird der Name des Kanals sowie eine Funktion erwartet, die für jede empfangene Nachricht auf diesem Kanal aufgerufen werden soll.

Der Empfänger ist stets an ein Widget gebunden, aus demselben Grund wie setInterval(), um Leaks zu verhindern.


Beispiel

Als Beispiel für den pub/sub-Mechanismus soll hier ein einfacher Chat-Client vorgestellt werden:

module.exports = {
  type: 'agorum.border',
  width: 600,
  height: 800,
  docked: {
    center: {
      type: 'agorum.textArea',
      name: 'chat',
      readOnly: true,
      monospace: true,
      value: ''
    },
    east: {
      width: 200,
      type: 'agorum.basicGrid',
      name: 'users',
      border: true,
      columns: [
        {
          name: 'name',
          text: 'Name'
        },
        {
          name: 'typing',
          text: 'Typing',
          width: 50
        }
      ]
    },
    south: {
      type: 'agorum.textInput',
      name: 'input'
    }
  }
};
/* global sc */

let aguila = require('common/aguila');

let widget = aguila.create(require('./simple-chat-aon'));

let chat = widget.down('chat');
let users = widget.down('users');
let input = widget.down('input');

let channel = 'simple-chat-broadcast';
let maxChatLength = 5000;

let id = sc.loginUserId + '_' + Date.now();
let name = sc.loginUser.fullName;

// message handling
let allUsers = {};

let handlers = {

  // do nothing
  beacon: () => {},

  // remove user from our list
  leave: user => delete allUsers[user.id],

  // update typing notification
  typing: (user, typing) => user.typing = typing ? '...' : '',

  // append text to chat
  text: (user, text) => chat.value = (chat.value + user.name + ': ' + text + '\n').slice(-maxChatLength)

};

widget.sub(channel, msg => {
  // ensure user is known
  let user = allUsers[msg.id] = allUsers[msg.id] || {
    id: msg.id,
    name: msg.name
  };

  // handle incoming message
  handlers[msg.type](user, msg.data);

  // update user list
  users.rows = Object.keys(allUsers).map(id => allUsers[id]);
});

let send = (type, data) => aguila.pub(channel, {
  type: type,
  id: id,
  name: name,
  data: data
});

// beacon
widget.setInterval(() => send('beacon'), 1000);

send('beacon');

// leave
widget.on('destroyed', () => send('leave'));

// typing notification
input.on('valueChanged', value => send('typing', !!value));

// sending
input.on('enter', () => {
  send('text', input.value);

  input.value = '';
});

widget;

Mit sehr einfachen Mitteln werden hier eine Liste der aktiven Benutzer, deren Status (tippt / tippt nicht) und gesendete Nachrichten ohne eine zentrale Kontrollinstanz (peer-to-peer) ausgetauscht.

Dieses Beispiel ließe sich leicht etwa um weitere Räume (Channel-Name) oder die Wahl des eigenen Anzeigenamens erweitern.

Eigene CSS-Klassen


Sollten die Gestaltungsmöglichkeiten nicht ausreichen, die aguila von Haus aus mitbringt, können eigene CSS-Klassen definiert und für ausgewählte Widgets verwendet werden.

Beispiel einer hervorgehobenen Textanzeige

MAIN_MODULE_MANAGEMENT/home/control/Styles/[ mein_konfigurationsprojekt ]/mein_konfigurationsprojekt.css

.test-highlight-red .x-form-display-field {
  color: red;
  font-weight: bold;
}

In der MetaDb wird die neue CSS-Klasse test-highlight-red definiert, die den Text rot und fett gestaltet. CSS können Sie über den agorum core template manager  registrieren.

let aguila = require('common/aguila');

let widget = aguila.create({
  type: 'agorum.vbox',
  items: [
    {
      type: 'agorum.textDisplay',
      value: 'Normaler Text'
    },
    {
      type: 'agorum.textDisplay',
      cls: 'test-highlight-red',
      value: 'Hervorgehobener Text'
    }
  ]
});

widget;

Mit der Kennzeichnung cls wird nun eine der beiden Textanzeigen mit der neuen Klasse versehen und dadurch farblich hervorgehoben. Diese Klasse kann auch nachträglich geändert werden, wenn das Widget schon existiert.

Bei der Benutzung sind zwei Dinge zu beachten: