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.