Angular-Formulare aus Annotationen in Geschäftsobjekten

HTML-Templates in Angular enthalten meist redundant Feldnamen aus den verwendeten Modellen. Insbesondere Template-driven Forms erfordern viel unnötige Schreibarbeit, Reactive Forms machen dies nur wenig besser.

Über die Verwendung von TypeScript Annotationen (Decorators) innerhalb der Geschäftsobjekte können Feldinformationen an die Masken übertragen und Redundanzen somit eliminiert werden.

Viel Schreibarbeit mit herkömmlichen Angular Formularen

In der Makler-Software eines Versicherungskonzerns sollte eine Pflegemaske für jede Versicherungsart erstellt werden. Ein unerfahrener Kollege nahm sich der Aufgabe an und erstellte pro Modell ein HTML-Template auf Basis der Template-driven Forms. Allein die Masse an erstellten Komponenten machte sie schwer wartbar. Außerdem entstanden schon bei der Entwicklung die „üblichen“ kleinen Probleme, zum Beispiel:

  • Groß-/Kleinschreibung von Feldnamen in Modell und Maske nicht korrekt
  • Inkonsistente Beschriftung
  • Ähnliche Modellfelder wurden in Masken unterschiedlich dargestellt

Könnte dies einfacher gelöst werden, indem vorhandene Informationen aus den Modellen ausgenutzt werden? Vermutlich spukten mir dabei bereits Annotationen im Kopf herum, von denen das Angular Framework starken Gebrauch macht. In TypeScript heißen Annotationen Decorators:

A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter.

Die Lösung: Die Informationen des Modells werden über Decorators bereitgestellt

Die Verwendung von Decorators wird in Angular standardmäßig aktiviert. Die Aktivierung erfolgt in der Konfigurationsdatei tsconfig.json.

tsconfig.json
8
9
"emitDecoratorMetadata": true,
"experimentalDecorators": true,

Für das Setzen und Auslesen von Metadaten wird zusätzlich das Modul Metadata Reflection API benötigt. Nach Hinzufügen der Abhängigkeit in package.json muss es einmalig importiert werden. In Angular-Applikationen ist dies in polyfills.ts vorgesehen.

Der Aufbau des Decorators

Wie bei dem Angular-Konzept soll dem Decorator ein einzelnes Objekt übergeben werden.

detail-config.js
export interface DetailConfigParams {
label?: string; // Alle Parameter optional und typisiert
validation?: ValidatorFn[];
// Weitere Parameter, siehe folgende Liste
}

In unserer PASCADA-internen Bibliothek bereits umgesetzte Parameter:

  • Optionen für Dropdown- oder Type-Ahead-Listen
  • disabled, dynamisch basierend auf anderen Werten einer Objektinstanz oder anderer Instanzen im Objektnetz
  • Placeholder, Tooltip
  • addonText (Hinter dem Eingabefeld visualisierter Text)
  • Ober- und Untergrenze bei Zahl- und Datumseingaben
  • Anzahl dargestellter Nachkommastellen bei Nummern-/Betragseingabe

 Weitere berücksichtigte Features:

  • Sprachabhängige Angaben
    • Bei einsprachigen Web-Applikationen direkt
    • Ansonsten über einen Schlüssel, der z.B. mittels ngx-translate übersetzt wird
  • Angabe von Optionen ebenfalls direkt oder über Name und Methode eines Angular-Service
  • Auswertung der Typinformationen der Modelle

Der Decorator ist als Factory implementiert.

detail-config.ts
   export interface DetailConfigDecorator {
   (detailConfig?: DetailConfigParams): any;
   }
   export const MY_ANNOTATIONS = '__my_annotations__';
   export const DetailConfig: DetailConfigDecorator = (params?: DetailConfigParams) => {
    return (cls, propertyOrMethodName: string): void => {
      const propType = Reflect.getMetadata('design:type', cls, propertyOrMethodName);
             const props = new DetailConfigImpl(propertyOrMethodName, propType, params);
      Reflect.defineMetadata(MY_ANNOTATIONS, [props], cls, propertyOrMethodName);
      // Die Annotation wird zusätzlich in der Klasse selbst ergänzt,
       // damit alle Annotationen der Klasse auf einfache Art ausgelesen werden können
      const metadataBefore = Reflect.getMetadata(MY_ANNOTATIONS, cls) || [];
      Reflect.defineMetadata(MY_ANNOTATIONS, metadataBefore.concat(props), cls);
    };
   };

Eine ausgelagerte Klasse DetailConfigImpl wertet das dekorierte Feld oder die dekorierte Methode aus. Hier kann man sich nach Herzenslust austoben, wie Name, Typ und Parameter verwendet werden sollen.

detail-config-impl.ts
export class DetailConfigImpl implements DetailConfigParams {
 public readonly label: string;
 public readonly validation: ValidatorFn[];
 ...
 constructor(public readonly name: string, public readonly propType: any, config: DetailConfigParams) {
   config = config || {};
   this.label = config.label || name;
   this.validation = config.validation || [];
   ...
 }
}

Die Verwendung des Decorators in einem Angular-Formular

Wie werden die Informationen des Decorators auf Angular-Komponenten bzw. eine Maske abgebildet?

Hier kommt das Konzept der Dynamic Forms ins Spiel, wo deklarativ Reactive Forms erzeugt werden. In der Angular-Dokumentation ist dazu ein Cookbook hinterlegt, weitaus ausführlicher beschreibt Todd Motto dies in seinem Blog Configurable Reactive Forms in Angular with dynamic components.

Das Konfigurationsobjekt für jede Formularkomponente ist das Interface FieldConfig. Ein Service sorgt für die Umwandlung von DetailConfigImpl nach FieldConfig.

field-config.service.ts
@Injectable({
 providedIn: 'root'
})
export class FieldConfigService {
 public getDetailConfig(cls: Type): FieldConfig[] {
  const annotations: DetailConfigImpl[] = Reflect.getMetadata(MY_ANNOTATIONS, cls.prototype) || [];
  return annotations.map(annotation => {
    return {
       name: annotation.name,
       label: annotation.label,
       validation: annotation.validation
    };
   });
 }
}

Die Umsetzung am Beispiel eines Vertrags

Angenommen, es liegt folgendes Interface für das Geschäftsobjekt Vertrag vor:

vertrag-interface.ts
export interface IVertrag {
 vertragsnummer: string;
 rateNetto: number;
}

Decorators können nicht an Interfaces annotiert werden, da diese im kompilierten JavaScript-Code nicht mehr existieren. Daher benötigen wir für den Vertrag eine konkrete Klasse.

vertrag.ts
export class Vertrag implements IVertrag {
 @DetailConfig({
   validation: [Validators.required, Validators.maxLength(10)]
 })
 vertragsnummer: string;
 @DetailConfig({
    addonText: '€',
    label: 'Rate (Netto)',
    precision: 2,
    min: 0
 })
 rateNetto: number;
}

Statt wie im Blog Configurable Reactive Forms in Angular with dynamic components unter Using the dynamic form auf hartkodiertem Weg dargestellt, ermittelt nun der Service die Feldkonfiguration aus der Klasse.

app.components.ts
export class AppComponent {
 config: FieldConfig[];
 constructor(fieldConfigSrv: FieldConfigService) {
   this.config = fieldConfigSrv.getDetailConfig(Vertrag);
 }
}

Als Ergebnis enthält die Maske die parametrisierten Eingabefelder. Die Daten sind gemäß dem Mechanismus Reactive Forms zu verwalten.

Über Decorators generiertes Angular Formular

Über Decorators generiertes Angular Formular

Fazit

Die Kombination aus Decorators und dynamischen Formularen erfordert viel Vorarbeit. Bei PASCADA haben wir den Code in einer Angular-Bibliothek gekapselt.

Auf dieser Basis können Pflegemasken zum Anlegen, Darstellen, Ändern und Löschen von Geschäftsobjekten (CRUD) sehr schnell erstellt werden, da

  1. die Angaben der Feldinformationen direkt im TypeScript-Modell erfolgen,
  2. die Ausprogrammierung des HTML-Templates entfällt und
  3. die Ausprogrammierung im Controller entfällt.

Wir bei PASCADA entwickeln smarte Web-Applikationen zur Bestandpflege von Geschäftsobjekten. Der Annotation-Ansatz reduziert messbar die Entwicklungsdauer und liefert somit einen echten Mehrwert für unsere Kunden.

Vorstellung des JavaScript Frameworks SAPUI5

Unser Software Engineer Björn ist JavaScript-Experte und unterstützte 2015 verschiedene SAPUI5-Projekte eines Automobilherstellers und eines Netzbetreibers. Björn gibt uns in diesem Beitrag einen kurzen Überblick zur Leistungsfähigkeit des Toolkits SAPUI5 der SAP SE, dem führenden Softwarehersteller für Business-Software.

Die Entstehungsgeschichte von SAPUI5

Im Jahr 2012 veröffentlichte der Standardsoftwarehersteller SAP das UI Development Toolkit for HTML5, kurz SAPUI5. Es handelt sich um eine browserbasierte UI-Technologie basierend auf HTML5, CSS3, JavaScript und der allgegenwärtigen JavaScript-Bibliothek jQuery, die von SAP mit folgenden Worten charakterisiert wird:

Bei der SAPUI5-Laufzeit handelt es sich um eine Client-seitige HTML5-Rendering-Bibliothek, die eine Palette von Standard- und Erweiterungscontrols sowie ein leichtgewichtiges Programmierungsmodell zur Verfügung stellt. Sie können diese Controls erweitern sowie neue Custom-Controls entwickeln.

Seit Ende 2013 sind die meisten Teile des SAPUI5-Toolkit als Open-Source-Variante OpenUI5 erhältlich, womit SAP sich die große Community der Web-Entwickler erschließt.

SAPUI5 bietet eine als feature-rich bezeichnete Kernbibliothek. Diese ermöglicht eine klare Trennung der Verantwortlichkeiten nach dem Model-View-Controller-Paradigma: Es erfolgt ein Data-Binding von Models an die Controls im View. Letzterer wird idealerweise deklarativ erstellt, wie weiter unten vorgestellt.

Zu jedem View existiert ein Controller, der selbst mehrere Views unterstützen kann. Abgerundet wird die Kernbibliothek durch Funktionalitäten wie die vereinfachte Unterstützung von Klassenhierarchien nach dem Prototyp-Konzept von JavaScript und das Laden von lokalisierten Textdateien.

Auf die Kernbibliothek aufbauend existiert eine große Palette an ausgelieferten UI-Controls, Anfang 2015 belief sich die Anzahl auf etwa 200. Out-of-the-box unterstützen diese ein Standard Theme für ein einheitliches Look-And-Feel, Animationen, Barrierefreiheit, Touch-Bedienung und Rechts-nach-links-Sprachen.

Nach einem Seitenhieb „SAP, ehemals als die Vorhölle der User Interfaces bekannt“ (Java Magazin 01/2016, Editorial S. 3) lobte das Java Magazin kürzlich die neue Technologie, die Design und Nutzerinteraktion für Business-Apps auf verschiedenen Geräten regelte.

Das Model

Models sind Instanzen von sap.ui.model.Model, die über Data Binding an Controls gehängt werden können. Haupteigenschaft dieser Klasse ist, dass Änderungen der Daten des Models propagiert werden, woraufhin die Adressaten des Binding reagieren können.

Gängig ist die Nutzung eines JSON-Models, dessen Daten beliebig programmatisch manipuliert oder initial von einer URL mit JSON-Response gefüllt werden können.

Models können an ein Control, einen View oder die Applikation gehängt werden, optional unter Angabe eines Namens. Beim Data Binding wird das in der Hierarchie nächstliegende Model genutzt, das dem Namen entspricht.

Beispielsweise kann ein Controller eines Views in selbigen ein unbenanntes Model hängen, das nur Einstellungen oder Bewegungsdaten dieses Views beziffert, während an der Applikation zum Beispiel anwendungsweite Konfigurationsdaten in einem Model namens config bereitgestellt werden.

Die View

Ein SAPUI5 View ist eine Ansammlung von Controls einer Bildschirmmaske. Die Models werden über Data Binding an die Controls der View und der View selbst verbunden. Über das Data Binding werden die Änderungen zwischen dem Model und der verbunden View automatisch aktualisiert.

Das Data-Binding kann über verschiedenen Wege erfolgen, entweder programmatisch als JavaScript-View, als HTML-View oder deklarativ als XML-View. Ich bevorzuge die deklarative Definition als XML-View. Dem nachfolgenden Listing 1 kann man die Trennung zwischen View und Controller sehr gut erkennen.

Listing 1: Beispielhafter XML-View

<core:View controllerName="com.example.view.main.MainMenu" xmlns="sap.m" xmlns:exmp="com.example.control" xmlns:core="sap.ui.core">
 <Page enableScrolling="true" class="exmplPage exmplMainMenu" showNavButton="false">
 <customHeader>
 <exmp:PageHeader id="pageHeader" pageTitle="{i18n>MainMenu.header.title}"
 backButtonVisible="false" mainMenuButtonVisible="false" infoButtonVisible="false" confMsgVisible="false"
 userInfoButtonVisible="{appSettings>/multipleUserRoles}" userInfoPress="openRoleSelectionDialog"/>
 </customHeader>
 <VBox alignItems="End" class="exmplWdsSwitch" visible="{wds>/visible}">
 <HBox alignItems="Center">
 <Label text="{i18n>MainMenu.wds.mode}"/>
 <Switch state="{wds>/enabled}" change="changeWdsMode"/>
 </HBox>

Eigenschaften eines XML-Views:

  1. Über herkömmliche XML-Syntax werden Typ und Eigenschaften des Controls angegeben. Der Namensraum entspricht dem Package. Ein weiterer Vorteil ist, dass auf diesem Weg eigenentwickelte Controls angegeben werden können.
  2. Notation in geschweiften Klammern, um ein Attribut eines Controls über das Data-Binding zu verbinden: Der XML-Ausdruck <Switch state=“{wds>/enabled}“ überträgt den Inhalt des Feldes enabled aus dem Model wds in die Eigenschaft state eines Controls vom Typ Switch.
  3. Aggregationen sind Kind-Elemente von Controls. Beispielsweise bietet die sap.m.VBox die Aggregation items, die beliebige Controls aufnehmen kann. Beim Rendern werden diese Controls in der Box untereinander angeordnet.
  4. Einerseits können die Kind-Elemente fix in der XML-Datei angegeben werden. Sehr viel häufiger gibt es vermutlich den Fall, dass die Daten der Elemente aus einer Liste in einem Model gespeist werden. Kommen Listenelemente hinzu, werden automatisch mehr entsprechende Kindelemente im View gerendert.
  5. Im XML-Tag <Switch state=“{wds>/enabled}“ change=“changeWdsMode“/> bezieht sich das zweite Attribut auf einen Event-Handler. changeWdsMode ist eine Funktion im Controller, die mit einem Event-Objekt aufgerufen wird, wenn der Anwender den Status des Controls ändert.

Der Controller

Im Controller ist das Verhalten des Views implementiert. Jeder Controller implementiert optionale Lifecycle-Methoden, von denen onInit und onBeforeRendering am gebräuchlichsten sind. Es folgen die Event-Handler- und Formatierer-Funktionen, die der oder die assoziierten Views erfordern.

Listing 2: Beispielhafte Controller-Klasse

jQuery.sap.require("com.example.BasePageController");
jQuery.sap.require("com.example.util.OrgUnits");
jQuery.sap.declare("com.example.view.main.MainMenu");
com.example.BasePageController.extend("com.example.view.main.MainMenu", {
 onInit: function(evt) {
 this.getView().setModel(sap.ui.getCore().getModel("tileGroups"), "tileGroups");
 this.getView().setModel(sap.ui.getCore().getModel("wds"), "wds");
 },
 openRoleSelectionDialog: function() {
 ExampleStatics.appController.openRoleSelectionDialog();
 },

Nutzt man die deklarative XML-View Beschreibung, wird der Klassenname des Controllers im Kopf der View angegeben. Das Toolkit instanziiert pro View einen Controller, der in einer entsprechend benannten Datei vorliegen muss.

Wie in Listing 2 ersichtlich, können Abhängigkeiten synchron eingebunden werden. Optional wird der Controller von einer bestehenden Klasse abgeleitet, um redundante Code-Strecken in Basisklassen zu bündeln.

Der Test

Als Unit-Test-Framework ist in SAPUI5 QUnit vorgesehen. Auch modernere Kandidaten sind einsetzbar, zum Beispiel Mocha.

Zur Unterstützung der Testimplementierung bietet das Toolkit die Klasse sap.ui.core.util.MockServer, mit der das Backend simuliert werden kann. Im einfachsten Fall liefert eine solche Mock-Instanz fix JSON-Dateien als Antwort, programmatisch können aber auch parametrisierte GET- sowie POST-, PUT- und DELETE-Requests behandelt werden.

Der Mock-Server wird idealerweise auch im Rahmen der One Page Acceptance Tests genutzt, kurz OPA5. Dieses Framework für Akzeptanztests berücksichtigt, dass UI5-Anwendungen Single-Page-Applications mit Verzögerungen im Seitenaufbau durch Animationen und Data-Binding sind. Eine beispielhafte Ausführung der OPA-Tests ist in Abbildung 1 dargestellt.

Ausführung des Akzeptanztests

Abbildung 1: Ausführung der Akzeptanztests

Die Tools

Seit langem existieren Plugins für die Eclipse IDE, mit denen Anwendungen leicht auf einen NetWeaver deployed werden können. Alternativ zu Eclipse und NetWeaver können Tools aus dem JavaScript-Ökosystem verwendet werden. Speziell für das Build-Management-Framework Grunt veröffentlichte SAP entsprechende Plugins.

Die Dokumentation

Das offizielle Dokumentationsportal stellt die API-Referenz von SAPUI5 in einem Umfang bereit, wie man das bisher von SAP-Produkten nicht kannte. Hervorzuheben ist der Entwicklerleitfaden. In diesem findet man zu jedem der rund 200 Controls eine Beispielanwendung mit Code-Schnipseln.

Die Quellen der Open-Source-Variante OpenUI5 finden sich auf GitHub.

Bei traditionellen SAP-Produkten war das SAP Community Network die primäre Quelle für Hilfe. Unter den Tags sapui5 oder openui5 findet man diese inzwischen auch auf Stackoverflow.

Die Zukunft von SAPUI5

SAP hat UI5 als strategische Frontend-Technologie für künftige Produkte auserkoren, um die Benutzer auf unterschiedlichen Gerätetypen zu erreichen. Unter dem Namen Fiori veröffentlicht der Konzern Applikationen, die alternative Benutzeroberflächen für Anwendungen der traditionellen SAP-Systeme bieten.

In der neuen Produktlinie Simple for HANA, die auf der SAP-eigenen In-Memory-Datenbank HANA aufsetzt, ist SAPUI5 die führende Oberflächentechnologie. Über die Open-Source-Variante ist das Toolkit allerdings auch für Nicht-SAP-Applikationen einsetzbar. Wegen des großen Repertoires an Controls halte ich es sogar für eine erwägenswerte Alternative von Angular.