10. Angular

Angular hat in den letzten Jahren eine rasanten Aufstieg erlebt. Angular ist ein Komponenten-Framework. Es ist ein Fundament für alle Applikationsarten, die auf HTML aufsetzen. Das sind natürlich Web-Anwendungen, können aber auch Apps für Smartphones sein.

10.1 Einführung

Im Gegensatz zu React, das im nächsten Kapitel behandelt wird – handelt es sich um ein vollständiges Framework, dass alle Aspekte der Entwicklung abdeckt. Es ist auch nicht mit einer einfachen Abstraktionsbibliothek wie jQuery vergleichbar.

10.1.1 Einführung in Angular

Mit Angular (ab Version 2) wurde ein völlig neuer Ansatz der Applikationsentwicklung entworfen. Während Angular 1 ein sauberes Entwurfsmuster (MVC = Model View Controller) für kleine und mittlere Anwendungen bietet, können mit Angular 2+ umfassende, komplexe und sich zügig entwickelnde Applikationen erstellt werden. Die Entwicklung erfolgt komponentenbasiert.

10.1.2 Die Programmiersprache

Erstmals seit vielen Jahren taucht in der Web-Welt etwas auf, was bislang unbekannt war – die Wahl einer Programmiersprache. Natürlich läuft im Browser weiterhin nur JavaScript. Aber mit ECMAScript6 (ES 2015) steht ein weiterer Dialekt bereit und die Programmierung erfolgt oft in TypeScript, was wiederum in JavaScript übersetzt wird. Angular wurde in TypeScript entwickelt. Es liegt also nahe, TypeScript auch bei der eigenen Anwendungsentwicklung zu benutzen. Natürlich muss sichergestellt werden, dass in der Entwicklungsumgebung TypeScript in JavaScript transpiliert wird. Visual Studio macht dies automatisch. Ansonsten eignen sich diverse Module aus npm (node package manager) dazu, den Prozess zu automatisieren. Das ist der einzige Nachteil – du benötigst einen Build-Prozess. Dazu findest du mehr im Abschnitt Werkzeuge und dort speziell zu Gulp.

Angular nutzt zwingend TypeScript. Es geht zwar rein technisch auch mit reinem JavaScript (oder Googles Programmiersprache Dart), in der Praxis ist diese Vorgehensweise aber bedeutungslos und verringert außerordentlich die Produktivität. Wenn du noch nicht TypeScript gelernt hast, dann lies das entsprechende Kapitel in diesem Buch zuerst. Es lohnt sich!

10.1.3 Ein triviales Beispiel

Angular benutzt als Entwurfsmuster Komponenten. Die Applikation selbst ist eine Komponente. Für ein einfaches Beispiel bietet sich der Name “HalloApp” an. Die Darstellung erfolgt über Tags. Generell werden hier viele Elemente benutzt, die nichts mit HTML zu tun haben. Es handelt sich praktisch um eine neue Markup-Sprache zur Beschreibung von Komponenten. Ein bisschen HTML ist am Ende natürlich immer dabei. Der Browser bekommt davon nichts mit – Angular erstellt HTML 5 aus den Bausteinen, die Sie erstellen.

Die Applikation sieht nun folgendermaßen aus:

1 <hallo-app>Laden...</hallo-app>

Dieser Teil wird in die HTML-Seite eingebaut. Im statische HTML steckt nun eine aktive Komponente. Diese muss nun erstellt werden.

10.1.3.1 Komponenten als Klassen

Die Basis der Komponente ist eine Klasse. Klassen sehen generell folgendermaßen aus:

1 class HalloApp {
2 }

Werden sie mit import referenziert, müssen sie exportiert werden:

1 export class HalloApp {
2 }

Dies ist eine syntaktische Vereinfachung der bekannten prototypischen Vererbung. Nach dem Übersetzen in JavaScript (oder beim Blick in die Interna von ES2015) zeigt sich, dass hier weiter Prototypen am Werke sind.

Die Bausteine von Angular sind stark modularisiert. Die benötigten Teile werden mittels import bereitgestellt:

 1 import {Component} from '@angular/core';
 2 import {bootstrap} from '@angular/platform/browser';
 3 
 4 @Component({
 5   selector: 'hallo-app',
 6   template : `<h1>
 7                 Hallo App
 8               </h1>`
 9 })
10 export class HalloAppComponent {
11 }
12 
13 bootstrap(HalloAppComponent);

Die fachliche Logik wird nun in einer Klassen geschrieben, aus der die Komponente mittels Annotation entsteht. die Annotation ist hier @Component (Zeile 4). Beschrieben wird hier das Tag (selector) und die HTML-Vorlage. Danach wird wird Komponente exportiert und dann sofort gestartet (Zeile 13).

10.1.3.2 Bindungen

Die bidirektionale Bindung war eines der augenfälligsten Effekte in AngularJS und hat – ähnlich wie die Animationen in jQuery – erheblich zur Verbreitung beigetragen. Allerdings gab es bei größeren Anwendungen manchmal Probleme mit der Performance. Sehr viele nachlässig erstellte Bindungen benötigen einiges an Verarbeitungsleistung und wirken vor allem auf mobilen Geräten eher negativ.

In Angular können Bindungen feingranularer erstellt werden. Sowohl Eigenschaften als auch Ereignisse lassen sich binden. Explizite Direktiven sind nicht mehr erforderlich. Als generische Syntax werden verschiedene Klammerformen benutzt.

 1 import {Component} from '@angular/core';
 2 
 3 @Component({
 4     selector: 'hallo-app',
 5     template: `
 6     <input 
 7       type="text"
 8       (keyup)="onKeyUp()"
 9       (input)="color=event.target.value"
10       [style.background-color]="color"
11     >`
12 })
13 export class HalloAppComponent {
14     public color: string;
15     onKeyUp() {
16         console.log('keyup: ' +         
17         this.color)
18     }
19 }

In diesem Beispiel werden zwei unidirektionale Bindungen erstellt. () liest Werte, während [] schreibt. Dies lässt sich kombinieren, sodass eine bidirektionale Bindung mit [()] bezeichnet wird. Um sich die Klammerreihenfolge zu merken, hat sich der Begriff “Banana in the Box” etabliert.

10.1.3.3 Die Bindung von Direktiven

Direktiven sind elementare Bausteine, die die Kernfunktionalität von Angular bereitstellen. Diese werden noch ausführlich behandelt. Auch diese Kernfunktionen müssen importiert werden. Eine wichtige Direktive zur Bindung ist die Zuweisung an ein Model – ngModel beim Umgang mit Formulardaten. Hier ist außerdem zu beachten, dass die Komponente FormsModule global importiert werden muss.

 1 import {Component} from '@angular/core';
 2 
 3 @Component({
 4     selector: 'hallo-app',
 5     template : `
 6     <h1>
 7         Angular Hallo App</h1>
 8     <form>
 9         <input 
10            type="text"
11            [(ngModel)]="search" >
12      <p>
13          Suchwert ist {{search}}
14      </p>
15     </form>
16 `
17 })
18 export class HalloAppComponent {
19 }

Das Formular enthält hier ein Eingabefeld mit bidirektionaler Bindung. Änderungen am Model – hier search – werden sowohl gelesen als auch geschrieben. Die Direktive sorgt für den Transport. Im Text der Vorlage erfolgt der Zugriff wie bei Angular 1 mit Hilfe der Handlebar-Syntax ({{}}).

10.1.3.4 Bindungsausdrücke

Bindungsausdrücke dienen dazu, Daten zu manipulieren. Die Ausdrücke sind pures JavaScript.

1 {{search.toUpperCase() + "!"}}
2 {{1 + 2 + 3}}

Beachten Sie, dass komplexe Ausdrücke dazu führen, dass Logik in die Vorlagen wandert. Das ist keine gute Idee und deshalb sollten Ausdrücke einfachen Ausgabenaufbereitungen vorbehalten bleiben.

10.2 Direktiven und Komponenten

Direktiven und Komponenten sind eng verwandt. Beides sind Definitionen für eigene Elemente bzw. Attribute, die eine bestimmte Funktionalität bezeichnen. Komponenten sind Träger der Anwendungslogik, während Direktiven elementare und oft generische Funktionen bereitstellen.

10.2.1 Direktiven

Direktiven gibt es in zwei Ausführungen:

Strukturelle Direktiven sind an dem vorangestellten * zu erkennen.

10.2.2 Komponenten

Komponenten sind der Kern einer Anwendung. Eine Komponente verknüpft eine Vorlage (template) mit einer Klasse über ein eigenes Element.

1 import {Component} from '@angular/core';
2 
3 @Component({
4     selector: 'hallo-app',
5     template: `Inhalt der Komponente`
6 })
7 export class HalloAppComponent {
8     // 
9 }

Der Selektor ist wie CSS zu lesen; er bezeichnet die Form der Erkennung in der HTML-Seite.

1 <body>
2   <div class="container">
3     <pizza-app>
4     </pizza-app>
5   </div>
6 </body>
10.2.2.1 Strukturelle Direktiven

Wie schon erklärt sollten strukturelle Direktiven immer dann verwendet werden, wenn der DOM verändert wird, also Elemente hinzugefügt oder entfernt werden sollen. Ein Beispiel dafür ist die *ngIf-Direktive.

1 <button (click)="isVisible = !isVisible">anzeigen | verstecken</butt\
2 on>
3 <div *ngIf="isVisible">Wir sind Ihr Dienstleister!</div>

Die Variable isVisible wird als Boolean-Wert interpretiert. Falls diese auf true steht, wird der div-Knoten in den DOM eingehangen, andernfalls wird er entfernt.

Wie bereits gezeigt, gibt es Ereignis- und Eigenschaftsbindungen in Angular. Deine Anwendung kann also durch die Verwendung von Klammern – () und [] – gesteuert werden. Im Falle einer strukturellen Direktive wird das *-Symbol benutzt.

10.2.3 Das *-Symbol in Angular

Das Asterisk-Zeichen (Stern) stellt die Kurzschreibweise einer strukturellen Direktive dar. Sie stellt auch automatisch eine Datenbindung her.

<user-list-item *ngFor="let pizza of menu"></user-list-item>

Strukturelle Direktiven würden mit ihrer erweiterten Syntax den eigenen Quellcode sehr aufblähen. Intern wandelt Angular jedoch immer die Kurzschreibweise in die ausführliche um!

10.2.4 Attribut-Direktive

Direktiven, die Attribute beeinflussen, werden als Attribut-Direktiven bezeichnet. Als Attribut wird hier ein DOM-Element beschrieben und damit kannst du dessen Aussehen oder Verhalten verändern. Als einfaches Beispiel soll hier die Schriftfarbe eines Elementes mittles einer Attribut-Direktive manipuliert werden.

<div [style.color]="'red'">Wir sind Ihr Dienstleister!</div>

Es gibt eine weitere Direktive, [ngStyle], die erst benutzt werden sollte, wenn mehrere Style-Attribute gesetzt werden.

<div [ngStyle]="{'color': 'red'}">Wir sind Ihr Pizza-Dienstleister!<\
/div>
10.2.4.1 Eigene Attribut-Direktiven

Eigene Direktiven sind ein charmantes Mittel, schnell Effekte zu erstellen, die global verfügbar sind. Als kleines Beispiel soll hier das Ändern der Schriftfarbe in eine eigene Direktive gepackt werden.

 1 import {Directive, ElementRef, Renderer} from '@angular/core';
 2 
 3 @Directive({
 4     selector: '[redFont]'
 5 })
 6 export class RedFontDirective {
 7     constructor(el: ElementRef, renderer: Renderer) {
 8         renderer.setElementStyle(el.nativeElement, 'color', 'red');
 9     }
10 }

Eine Direktive wird über den Decorator @Directive definiert. Als wichtigste Meta-Daten muss wieder ein Selektor angegeben werden, damit die Direktive überhaupt ausgeführt wird. Im Unterscheid zur Komponente wird der Selektor in [] geschrieben, wodurch ein Attribut-Name definiert wird. Dies entspricht der Schreibweise in CSS.

Im Beispiel werden zwei wichtige Bestandteile für die Arbeit mit Direktiven genutzt.

Es ist natürlich möglich, das DOM-Element direkt zu ändern. Das kann jedoch in vielen Situationen auf Kosten der Anwendungs-Performance passieren. Der Renderer ermöglicht es beispielsweise, das Rendern an Web-Worker auszulagern und so asynchron auszuführen.

10.2.5 Benutzen von Komponenten und Direktiven

Damit eine Direktive oder Komponente überhaupt in einem Teil deiner Anwendung genutzt werden kann, müssen diese der entsprechenden Komponente bekannt gemacht werden. Hierzu wird die Direktive via import importiert (das benötigt TypeScript) und dem @Component-Dekorater mit dem Parameter directives übergeben. Somit kann eine klare Abgrenzung geschaffen werden, welche Direktive wo benutzt werden kann und Namenskollisionen werden vermieden oder geschickt als Konfiguration genutzt.

 1 import {Component} from '@angular/core';
 2 
 3 import {RedFontDirective} from '../directives/redFont.directive';
 4 
 5 @Component({
 6     selector: 'user-app',
 7     directives: [RedFontDirective],
 8     template: `
 9     <button (click)="isVisible = !isVisible" redFont>anzeigen | vers\
10 tecken</button>
11     <div *ngIf="isVisible" [style.color]="'red'">Wir sind Ihr Dienst\
12 leister!</div>
13     `
14 })
15 export class UserAppComponent {
16     public isVisible:boolean = true;
17 }

Am Ende wird die Direktive – mit Hilfe des festgelegten Attributnamen – an einen DOM-Knoten mit Text gebunden.

Alternativ zur Registrierung in der Komponente selbst lassen sich Direktiven ebenso wie Komponenten global im Modul deklarieren. Dies sollte immer dann erfolgen, wenn sie tatsächlich mehrfach benutzt werden.

10.2.6 Schleifen mit *ngFor

In Angular existiert mit *ngFor eine Direktive, die das Wiederholen von DOM-Elementen erlaubt. Als strukturelle Direktive wird diese an einen bestehenden DOM-Knoten wie folgt gebunden.

1 <div *ngFor="let number of [1, 5, 34, 47]">
2     Aktuelle Zahl ist: {{number}}
3 </div>

Das *-Symbol gibt an, dass es sich um eine strukturelle Direktive handelt. Das aktuelle Element der Schleife wird auf eine neue lokale Variable number abgebildet. Die Definition einer Variable wird über das #-Symbol ausgezeichnet. Die Liste an Elementen kann dabei natürlich auch aus einer Variable kommen.

Auf den aktuellen Index der Schleife kann über eine Erweiterung der Schleifenanweisung zugegriffen werden. Dies sieht dann folgendermaßen aus:

1 <div *ngFor="let number of [1, 5, 34, 47]; let currentIndex=index">
2     Aktuelle Zahl ist: {{number}} ({{currentIndex}})
3 </div>

Nach der Angabe der Liste kann der aktuelle Index auf eine eigene Variable geschrieben werden, um auf sie zugreifen zu können. Es gibt viele weitere Funktionen der Schleife für gerade/ungerade, Rückruffunktionen, asynchrone Datenquellen und vieles mehr.

10.2.7 Pipes

In Angular sind Pipes dynamische Filter für Daten. Sie erlauben das Transformieren von Daten in Ausdrücken. Pipes leiten Daten von den Ausdrücken weiter an eine Funktion, die die Daten manipuliert. Einige eingebaute Pipes sind bereits vorhanden. Die Anwendung erfolgt mit dem Pipe-Symbol |:

<span>{{10.99 | currency}}</span>

Die Pipes haben oft Parameter:

<span>{{10.99 | currency:'EUR':true}}</span>
10.2.7.1 Eigene Pipes

Mit dem Dekorator @Pipe lassen sich eigene Pipes erstellen. Die Basisfunktion wird aus PipeTransform geerbt. Hier ein Beispiel:

1 import {Pipe, PipeTransform} from '@angular/core';
2 
3 @Pipe({name: 'makeUpper'})
4 export class UpperCasePipe implements PipeTransform {
5     transform(text:string, args:string[]) : any {
6         return text.toUppercase();
7     }
8 }

Um dieses Pipe benutzen zu können, muss es bekannt gemacht werden:

 1 import {Component} from '@angular/core';
 2 
 3 import {UpperCase} from '../pipes/addTwo.pipe';
 4 
 5 @Component({
 6     selector: 'user-app',
 7     pipes: [UpperCasePipe],
 8     template: `
 9         <span>{{price | currency}}</span>
10         <span>{{price | currency:'EUR':true}}</span>
11         <div>{{product | makeUpper}}</div>
12     `
13 })
14 export class UserAppComponent {
15     private product = "kleiner artikel";
16     private price = 10.99;
17 }

10.2.8 Dienste

Wiederverwendbare Bestandteile der Applikation ohne Bezug zur UI werden mittels Diensten (services) implementiert. Dabei gibt es zwei Arten von Diensten:

Dienste werden mittels Dependency Injection bereitgestellt. Sie werden damit bei der Benutzung injiziert, also quasi von außen bereitgestellt. Die nutzende Seite hat keine Informationen über Herkunft und Konstruktion des Dienstes. Die Annotation @Injectable dient dazu, Dienste zu kennzeichnen.

 1 import {Injectable} from '@angular/core';
 2 
 3 @Injectable()
 4 export class GadgetService {
 5     getPizza() {
 6         return [{
 7             "id": 1,
 8             "name": "Micro Mouse",
 9             "price": 5.99
10         }, {
11             "id": 2,
12             "name": "Wireless Mouse",
13             "price": 10.99
14         }, {
15             "id": 3,
16             "name": "USB-Hub",
17             "price": 7.99
18         }, {
19             "id": 4,
20             "name": "Our Catalogue",
21             "price": 0
22         }]
23     }
24 }

Um einen Dienst zu nutzen, wird die Eigenschaft providers der Komponente gesetzt (Zeile 7):

 1 import {Component} from '@angular/core';
 2 
 3 import {HalloService} from '../services/hallo.service';
 4 
 5 @Component({
 6     selector: 'hallo-app',
 7     providers: [HalloService],
 8     template: `
 9     <span>Anzahl an Geräte: {{devices.length}}</span>
10     `
11 })
12 export class DeviceAppComponent {
13     public devices = [];
14 
15     constructor(private gadgets: GadgetService) {
16         this.devices = this.gadgets.getDevices();
17     }
18 }

Durch die Angabe des Services als Provider der Komponente wird beim Erstellen eine neue Instanz des Services erzeugt. Diese ist auch nur für diese Komponente und ihre Kind-Komponenten, welche diesen Service gegebenenfalls auch benutzen, verfügbar.

Soll ein Dienst global – also anwendungsweit – verfügbar sein, kann dieser in der Hauptkomponente der Anwendung, einem Modul oder bereits zum App-Start in der bootstrap-Methode geladen und verfügbar gemacht werden.

bootstrap(AppComponent, [GadgetService]);

Neben dieser Kurzschreibweise wird in der Regel die Deklaration in der Eigenschaft providers des Dekorators @NgModule() erfolgen.

10.2.9 HTTP in Angular

Ein wichtiger Bestandteil von Web-Anwendungen ist die Kommunikation mit Schnittstellen. Typischerweise basieren diese Schnittstellen auf dem HTTP-Protokoll. Für die Kommunikation mit einer Schnittstelle sollte ein eigener Service angelegt werden. Aus diesem Grund wird der Gadget-Service so abgewandelt, dass er die Angebots-Daten aus einer JSON-Datei abfragt. Diese wird über eine GET-Anfrage abgerufen und dann in das JSON-Format umgewandelt, um damit in der Anwendung umgehen zu können.

 1 import {Http} from '@angular/http';
 2 import {Injectable} from '@angular/core';
 3 import 'rxjs/add/operator/map'; // map Operator aus RX
 4 
 5 @Injectable()
 6 export class GadgetService {
 7     constructor(private http: Http) {
 8     }
 9 
10     getDevices() {
11         return this.http('assets/devices.json')
12             .map(response => response.json());
13     }
14 }

Zuerst wird der Http-Service von Angular importiert und dann über die Dependency-Injection dem Service bereitgestellt. Die Funktion getDevices kann dann innerhalb einer Komponente aufgerufen werden, um die Daten abzurufen. Ein Request läuft asynchron, daher liefert der Http-Service ein so genanntes Observable zurück, welches über die RxJS-Bibliothek erzeugt wird. Dazu mehr im Abschnitt zu Rx (Reactive Extensions).

Die map-Funktion muss erst explizit geladen werden, damit sie auf dem Observable ausgeführt werden kann! Die Komponente kann nun den Service benutzen.

10.2.9.1 Observables

Ein Observable erlaubt das Überwachen asynchron ausgeführten Codes. Ist der Programmcode des Observable abgeschlossen, werden alle Abonnenten über die erfolgte Ausführung informiert. Auf diesen Observables basiert auch das Ereignis-System von Angular (speziell die Klasse EventEmitter). Observables erlauben es also, Programmteile asynchron ausführen zu lassen.

Um ein Observable zu abonnieren, muss dessen subscribe-Funktion aufgerufen werden. Als Callback erhält diese eine Funktion, welche wiederum als Parameter geänderte oder neue Daten erhält. In unserem Fall sind dies die Gadgets aus der JSON-Datei.

 1 import {Component} from '@angular/core';
 2 import {HttpModule} from '@angular/http';
 3 
 4 import {GadgetService} from '../services/devices.service';
 5 
 6 @Component({
 7     selector: 'gadget-app',
 8     providers: [GadgetService, HttpModule],
 9     template: `
10     <span>Anzahl an Geräte: {{devices.length}}</span>
11     `
12 })
13 export class GadgetAppComponent {
14     public devices = [];
15 
16     constructor(private gadgetService: GadgetService) {
17         this.loadData(); 
18     }
19 
20     loadData(){
21          this.gadgetService.getDevices()
22                            .subscribe(g => this.devices = g);
23     }
24 }

10.2.10 Lebendauer der Komponenten

Eine Komponente in Angular durchläuft verschiedene Zustände während der Ausführung. Diese werden auch Lebenszyklen genannt. Über die Lifecycle-Hooks kannst du hier an verschiedenen Stellen eingreifen. Folgende Funktionen können dazu genutzt werden:

Das Beispiel zur Verwendung des Http-Services wird nun so erweitert, dass die Geräte nicht direkt im Konstruktor der GadgetAppComponent abgerufen werden, sondern erst, wenn die Komponente initialisiert wurde.

 1 import {Component, OnInit} from '@angular/core';
 2 import {HttpModule} from '@angular/http';
 3 
 4 import {GadgetService} from '../services/devices.service';
 5 
 6 @Component({
 7     selector: 'gadget-app',
 8     providers: [GadgetService, HttpModule],
 9     template: `
10     <span>Anzahl an Geräte: {{devices.length}}</span>
11     `
12 })
13 export class GadgetAppComponent implements OnInit {
14     public devices = [];
15 
16     constructor(private gadgetService: GadgetService) {
17     }
18 
19     ngOnInit(){
20          this.gadgetService.getDevices()
21                            .subscribe(g => this.devices = g);
22     }
23 }

10.3 Architektur einer Angular-Anwendung

Eine Angular-Anwendung erfordert eine stringente und gut geplante Architektur, andernfalls kommt es schnell zu einer verwirrenden Sammlung von Code-Schnippseln, die kaum wartbar sind. Die Sprachmerkmale von TypeScript und die Modularisierung von Angular helfen dabei und unterstützen die Vorgehensweise explizit.

10.3.1 Bestandteile der Anwendung

Technisch kann eine Anwendung in folgende Bausteine zerlegt werden:

Die Komponenten lassen sich weiter aufteilen. Zum einen sind dies universelle – meist als Widget bezeichnete – UI-Elemente, wie beispielsweise Tabellen (Grid), Baumansichten (Treeview) oder Tabulatoren (Tabs). Zum anderen sind dies die elementaren Bausteine der Applikation, die mit dem Benutzer interagieren. Das sind dann Formulare, Dashboards usw.

Bei den Konfigurationen werden alle globalen Dinge abgelegt. Dies betrifft vor allem das Routing und Mehrsprachigkeit. Dienste dienen der Kommunikation zwischen Komponenten und der Bereitstellung einer Dienstschicht zum Server. Modelle fassen alle Klassen zusammen, die Daten behandeln. Im Wesentlichen sind dies View-Modelle und deren Hilfsbausteine wie Validatoren.

10.3.2 Benennungsregeln

Lege dir am Anfang klare Benennungsregeln auf. Die vielen Formen machen es sonst schwer, die richtigen Dateinamen zu finden. Generell sollte der Name der Datei dem Namen der Klasse entsprechen.

Klassen können einen Suffix bekommen, wenn es sehr viele einer Sorte gibt. Das vermeidet Konflikte und damit unglücklich gewählte Namen:

Auf Dateiebene bietet es sich eher an, durch Punkte getrennte Infixe zu verwenden. Das hat praktische Gründe, auch wenn es auf den ersten Blick inkonsequent erscheint. Dateilisten sind oft alphabetisch sortiert, und dann hast du die Gruppen schnell im Blick, auch wenn sie im Client als lange Liste von JS-Dateien ankommen.

10.3.3 Komponenten

Komponenten bilden am Ende eine Baumstruktur, viele universelle Komponenten sind aber mehrfach im Einsatz. Ein reiner Baum eignet sich deshalb nicht zur Anordnung der Komponenten im Projekt. Eine fachliche Anordnung ist oft sinnvoller. Als Beispiel soll hier eine Applikation beschrieben werden, die Veranstaltungen verwaltet. Diese hätte dann folgende Komponenten-Struktur:

 1 App -
 2     |- components
 3     |   |- widgets
 4     |   |   |- datagrid 
 5     |   |   |  | - models
 6     |   |   |  |   |- datagrid.helper.ts  
 7     |   |   |  |   |- datagrid.model.ts               
 8     |   |   |  |- pagination.component.ts               
 9     |   |   |- Treeview  
10     |   |   |  | - Models
11     |   |   |  |   |- index.ts  
12     |   |   |  |   |- vm-treeview-baseinterface.ts               
13     |   |   |  |   |- datagrid.helper.ts  
14     |   |   |  |   |- datagrid.model.ts               
15     |   |   |  |- treeview.component.ts  
16     |   |   |  |- treeview-node.component.ts  
17     |   |   |- infobox.component.ts  
18     |   |   |- sidemenu.component.ts  
19     |   |   |- tabs.component.ts  
20     |   |   |- webpart.component.ts  
21     |   |- events
22     |   |   |- list.component.ts
23     |   |   |- new.component.ts
24     |   |   |- edit.component.ts
25     |   |   |- delete.component.ts
26     |   |- users 
27     |       |- list.component.ts
28     |       |- new.component.ts
29     |       |- edit.component.ts
30     |       |- delete.component.ts
31     |- Configurations
32     |   |- routes.config.ts
33     |- Decorators
34     |   |- diverse Dekoratoren 
35     |- Services 
36     |   |- diverse Dienste
37     |- Utils
38     |   |- diverse Hilfsklassen 
39     |- viewmodels        
40         |- event.viewmodel.ts 
41         |- user.viewmodel.ts

Oft sind viele Komponenten in einem Rutsch zu importieren. Wenn du zwei oder mehr in einem Ordner hast, fügst du immer eine Datei index.ts ein:

1 export * from './treeview/ac-treeview.component';
2 export * from './treeview/ac-treeview-node.component';
3 export * from './ac-infobox.component';
4 export * from './ac-breadcrumb.component';
5 export * from './ac-webpart.component';
6 export * from './datagrid/ac-datagridpagination.component';
7 export * from './ac-tabs.component';
8 export * from './ac-sidemenu.component';

Beim Import einer oder mehrerer Komponenten sieht es dann folgendermaßen aus:

1 import * as cmp from './components/index';
2 import * as wd from './components/widgets/index';

Du kannst dann mit cmp.ComponentenName oder wd.WidgetName auf die Komponenten zugreifen, ohne jede Datei einzeln importieren zu müssen. Die Dateierweiterung *.ts wird automatisch benutzt, sodass es reicht index zu schreiben (keine Option, die Benutzung der Dateierweiterung ist nicht möglich).

10.4 Routing – Der Weg zur SPA

Das Routing in Angular basiert auf dem Modul @angular/router. Der Router verbindet lokale (clientseitige) Pfade mit Komponenten. Klickt der Benutzer auf einen entsprechend konfigurierten Link, wird die Komponente geladen. Dadurch wird bei einem Seitenwechsel nicht mehr die gesamte Seite getauscht, sondern nur ein Teil mit einer Komponente. Die Applikation selbst beleibt im Speicher und kann dadurch Zustände speichern und verhält sich wie eine klassische Desktop-Applikation.

10.4.1 Konfiguration

Da sich alles um die Routen rankt, müssen diese konfiguriert werden.

Eine typische Konfiguration sieht folgendermaßen aus:

 1 import { Routes } from '@angular/router';
 2 import * as cmp from '../components/index';
 3 
 4 const routes: Routes = [
 5   {
 6     path: '',
 7     redirectTo: 'dashboard',
 8     pathMatch: 'full'
 9   },
10   {
11     path: 'dashboard',
12     component: cmp.DashboardComponent,
13     data: { 'title': 'Dashboard', 'subtitle': 'Dashboard' }
14   },
15   {
16     path: 'widgets',
17     component: cmp.WidgetDemoComponent,
18     data: { 'title': 'Widget Demo', 'subtitle': 'Diverse Components'\
19 , 'breadcrumb': true },
20     children: [
21       {
22         path: '',
23         redirectTo: 'list',
24         pathMatch: 'full'
25       },
26       {
27         path: 'list',
28         component: cmp.ListWidgetsComponent,
29         data: {
30           'title': 'Overview', 'subtitle': 'Show all widgets',
31           'active': true, 'disabled': false, 'breadcrumb': true
32         }
33       },
34       {
35         path: 'tree',
36         component: cmp.TreeviewComponent,
37         data: {
38           'title': 'Tree View', 'subtitle': 'Tree Demo',
39           'active': false, 'disabled': false, 'breadcrumb': true
40         }
41       }
42     ]
43   }
44 ];
45 
46 export default routes;

Der Typ Routes ist ein Array auf Route. Die wesentlichen Teile einer Route sind:

10.4.2 Definition

Die Routendefinition wird dann im Modul importiert und bekannt gemacht:

 1 import { RouterModule } from '@angular/router';
 2 import { LocationStrategy, HashLocationStrategy } from '@angular/com\
 3 mon';
 4 
 5 import routes from './configurations/routes';
 6 
 7 @NgModule({
 8   imports: [
 9     RouterModule.forRoot(routes)
10   ],
11   declarations: [...],
12   bootstrap: [...],
13   providers: [
14     { provide: LocationStrategy, useClass: HashLocationStrategy }
15   ]
16 })
17 export class RootModule {
18 }

Dem RouterModule werden die Routen hier mitgeteilt (Zeile 8). Optional kann noch die Abtrennungsstrategie der Route mitgeteilt werden. Die URL für den Router ist Teil der gesamten URL und betrifft auch den Serverteil:

http://server:8080/das/ist/die/route?mehr=daten

Nun muss der Browser erkennen, dass es sich hier um einen clientseitigen Pfad handelt, nicht um den für den Server. Es gibt aber kein Trennzeichen dafür. Eine Möglichkeit ist bei modernen Browsern das Base-Tag:

<base href="/">

Das hilft aber nicht immer, vor allem bei Lesezeichen ist das eher hinderlich. Deshalb kann statt dieser Methode – PathLocationStrategy genannt – auch mit dem Hash # gearbeitet werden. Dies trennt clientseitige Operationen ab und funktioniert auch in alten Browsern. Konsequenterweise heisst es HashLocationStrategy. Die Nutzung findest du im Beispiel auf Zeile 12.

10.4.3 Nutzung

Nun gibt es eine Route und eine Komponenten, die geladen wird. Es fehlt noch ein Ziel. Dieses sieht folgendermaßen aus:

<router-outlet></router-outlet>

An der Stelle im Vorlagencode irgendeiner Komponente landen die Inhalte. Sinnvollerweise ist dies meist die Wurzelkomponente. Natürlich geht das Routing auch lokal, in Modulen und mit benannten Zielen. Die Dokumentation zeigt dazu weitere Beispiele.

Damit die Route ausgelöst wird, werden Schaltflächen oder Hyperlinks benutzt. Diese werden mit der Direktive routerLink erweitert.

 1 <div class="collapse navbar-toggleable-xs" id="collapsingNavbar">
 2   <ul class="nav navbar-nav pull-right">
 3     <li class="nav-item active">
 4       <a class="nav-link" href="#" [routerLink]="['/dashboard']">
 5         Home 
 6         <span class="sr-only">Home</span>
 7       </a>
 8     </li>
 9     <li class="nav-item">
10       <a class="nav-link" href="#" [routerLink]="['/editor']">
11         Editor Demo
12       </a>
13     </li>
14     <li class="nav-item">
15       <a class="nav-link" href="#" [routerLink]="['/about']">
16         About
17       </a>
18     </li>
19   </ul>
20 </div>

Die Daten können gebunden werden und damit dynamisch sein oder ungebunden und statisch (also mit oder ohne []-Klammern).

10.5 Rx – Die Reactive Extensions

Das Konzept des Reactive Programming ist seit geraumer Zeit ein Fundament für viele Applikationen. Microsofts Rx-Bibliothek, die für viele Plattformen und Programmiersprachen verfügbar ist, setzt das Prinzip konsequent um. Die Variante für JavaScript, RxJS, ist ein integraler Bestandteil von Angular. Der fundamentale Einstieg in den Code gelingt über den Quellcode auf Github.

10.5.1 Geschichte

Rx-Bibliothek stammt aus dem Hause Microsoft. Das Cloud Programmability Team hat die Erweiterungen entwickelt. Die geschah unter der Federführung von Eric Meijer, der auch maßgeblich an der Entwicklung von LINQ (Language Integrated Query) beteiligt war. Vielen Operatoren sieht man die Ähnlichkeit mit LINQ an und Kenntnisse in LINQ sind vorteilhaft beim Lernen. Rx geht jedoch inzwischen weit über LINQ hinaus.

10.5.2 Observables

Ein Observable repräsentiert eine abstrakte Menge von Daten, die in einer noch nicht bekannten Menge zu einem noch nicht bekannten Zeitpunkt bereitstehen werden. Ein Observer abonniert ein Observable und damit die später ankommenden Daten. Ein Observable kann gefiltert, gruppiert oder transformiert werden. Ebenso sind Berechnungen möglich.

Das Konzept erinnert an Streams und oft werden derartige Umgebungen auch als solche bezeichnet. Bei der Verarbeitung von großen oder gar unbekannten Datenmengen ist es ein fundamentaler Unterschied, ob die Daten erst lokal gespeichert und dann zusammen abgearbeitet werden, oder ob sie beim Eintreffen sequenziell verarbeitet werden. Werden 1000 Objekte zu je 1 KByte empfangen, liegt erstmal 1 MByte im Speicher. Soll nun ein Filter benutzt werden, das drei Objekte herausfiltert, so wird 1 MByte allokiert, die Aktion ausgeführt, und dann werden drei weitere KByte für das Resultat allokiert. Irgendwann räumt dann ein Aufräumprozess den Speicher auf und beseitigt das obsolete 1 Mbyte. Dies ist belastend für die Maschine und damit schlecht für die Performance. Vielleicht wird durch den Filter auch eine Störung der Benutzeroberfläche erzeugt, denn es entsteht eine synchron blockierende Aktion. Ein Observable enthält Daten, die einer Aktion schrittweise, Element für Element, übergeben und sodann verworfen werden. Das Filter benötigt zu jedem Zeitpunkt nur 1 KByte an Speicher und am Ende zusätzlich 3 KBytes für das Resultat.

10.5.2.1 Beispiele

Observables arbeiten immer auf aufzählbaren Daten, in JavaScript sind dies Arrays oder Map-Objekte. Das kann im Extremfall auch ein einziges Objekt sein, sodass technisch Observables auch mit skalaren Daten umgehen können.

Zum Start kannst du einfach ein Observable mit statischen Daten erzeugen:

1 var array = [10, 20, 30];
2 var result = Rx.Observable.from(array);
3 result.subscribe(x => console.log(x));

Es ist aber auch möglich, das Verhalten dynamisch festzulegen, indem der Aufzählvorgang manipuliert wird:

 1 var observable = Rx.Observable.create(function (observer) {
 2   observer.next(1);
 3   observer.next(2);
 4   observer.next(3);
 5   observer.complete();
 6 });
 7 observable.subscribe(
 8   value => console.log(value),
 9   err => {},
10   () => console.log('this is the end')
11 );

Hier wird eien Funktion benutzt, um den jeweils nächsten Wert anzuliefern. Der Observer kennt die Funktionen next, complete und error. Damit wird dem Konsumenten (Subscriber) mitgeteilt, was als nächstes passiert. Man kann die Funktionsaufrufe als Trigger verstehen, die beim Konsumenten Ereignisse auslösen.

Das nächste Beispiel nutzt erneut statischen Werte, die bequem generiert werden:

1 var numbers = Rx.Observable.range(1, 10);
2 var even = numbers.filter(n => n % 2 === 0);
3 even.subscribe(e => console.log(e));

Iteration und Filter erfolgen nicht mit Schleifen, sondern sequenziell mit Rückruffunktionen. Der Lambda-Ausdruck in Zeile 2 ist eine verkürzte Schreibweise einer Methode. Derartige Ausdrücke sind häufig in Gebrauch.

10.5.2.2 Asynchrone Observables

Mit interval steht ein Operator zur Verfügung, der zeitlich gesteuerte Abläufe ermöglicht. Der Wert 1000 liefert jede Sekunden ein Ereignis. Eintreffende Daten im Stream lassen sich mit dem Operator zip synchronisieren (“zip” ist in Englischem das Wort für Reißverschluss). Das folgende Beispiel nimmt eine Datenquelle und liefert deren geradzahlige Werte jede Sekunde aus:

1 var numbers = Rx.Observable.range(1, 10);
2 var even = numbers.filter(n => n % 2 === 0);
3 var interval = Rx.Observable.interval(1000);
4 var stream = interval.zip(even, (i, e) => e));
5 stream.subscribe(x => console.log(x));

Der Ausdruck in Zeile 4 sorgt dafür, dass im Ergebnis die Werte der Zahlenfunktion und nicht die des Intervals zurückgegeben werden.

10.6 Zusammenfassung

Angular ist eines der spannendsten Frameworks der letzten Jahre. Mit dem Support durch Google und der Integration von Microsofts starker RX-Landschaft hat man so ziemlich alles aus einer Hand, was große Applikationen brauchen. Allerdings ist die Lernkurve steil und der “Angular-Weg” mag dem einen oder anderen nicht immer passen. Dir Größe der Applikationen prädestiniert darüber hinaus den Einsatz im Intranet. Stimmen die Rahmenbedingungen, führt kaum ein Weg am Angular vorbei und man erhält schnelle, stabile und gut test- und wartbare Umgebungen.

Im nächsten Schritt wird ein Konkurrenzprodukt aus dem Hause Facebook vorgestellt – React. Es ist schlanker und auf den ersten Blick einfacher, dafür aber keineswegs vollständig und in einigen elementaren Fällen eher rudimentär.