let
und const
for..of
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.
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.
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.
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!
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.
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).
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.
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 ({{}}).
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.
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.
Direktiven gibt es in zwei Ausführungen:
*ngIf
, *ngSwitch
und *ngFor
.ngStyle
und ngClass
.Strukturelle Direktiven sind an dem vorangestellten * zu erkennen.
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
>
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.
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!
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>
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.
ElementRef
– erlaubt Zugriff auf das verbundene DOM-Element (praktisch der Eltern-Knoten im Baum)Renderer
– Framework zum performanten Ändern von DOM-ElementenEs 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.
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.
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.
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
>
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
}
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.
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.
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
}
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:
ngOnInit
: Komponente wird initialisiert (nach erstem ngOnChanges → Eigenschaften initialisiert)ngOnDestroy
: bevor Komponente zerstört wirdngDoCheck
: eigene ÄnderungserkennungngOnChanges
: Änderungen in Bindings wurden erkanntngAfterContentInit
: Inhalt wurde initialisiertngAfterContentChecked
: jedes Mal, wenn Inhalt überprüft wurdengAfterViewInit
: Views wurden initialisiertngAfterViewChecked
: jedes Mal, wenn Views überprüft wurdenDas 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
}
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.
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.
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.
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).
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.
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:
path
: Die Konstruktion der Route; Parameter werden mit geschweiften Klammern umfasstcomponent
: Wenn die Route erkannt wird, wird diese Komponenten geladendata
: Optionale Daten, die an die Komponenten weitergereicht werdenchildren
: Optionale Kindelemente für verschachtelte RoutenredirectTo
und pathMatch
helfen bei der Auflösung von RückfallroutenDie 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.
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).
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.
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.
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.
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.
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.
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.