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
89 | "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.
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
- die Angaben der Feldinformationen direkt im TypeScript-Modell erfolgen,
- die Ausprogrammierung des HTML-Templates entfällt und
- 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.