Routing und Services¶
Single- vs. Multi-Page-Applikationen¶
Wenn wir durch z.B. dieses Skript hangeln oder Wikipedia, dann stellen wir fest, dass sich nach jedem Klick auf einen Link eine neue HTML-Seite öffnet. Das wird insbesondere deutlich wenn die Entwicklungstools geöffnet sind. Jeder Klick auf einen Hyperlink erwirkt eine neue Anfrage an einen Webserver mit dem Request, eine neue HTML-Seite von diesem Webserver zu laden und im Browser zu öffnen. Es handelt sich dabei also um eine Webanwendung mit vielen (Unter-)Seiten, eine sogenannte Multi-Page-Applikation (MPA).
Wenn wir stattdessen z.B. die Angular-Seite https://angular.dev
öffnen und uns die Developertools anschauen, dann stellen wir fest, dass kaum HTML-Code im <body>
-Element enthalten ist. Stattdessen wird der gesamte HTML-Code per JavaScript im Browser eingebunden. Damit werden Inhalte in die Seite immer genau dann eingebunden, wenn sie angezeigt werden sollen. Um zwischen einzelnen Ansichten der Webanwendung zu wechseln, wird keine neue Webseite vom Webserver geholt. Stattdessen bleiben wir stets in derselben HTML-Seite (Single-Page-Applikation (SPA)), was sehr gut sichtbar wird, wenn wir die Developertools eingeschaltet lassen und innerhalb der Webanwendung umhernavigieren. Stattdessen werden nur Inhalte (über eine REST-API) vom Server geladen.
Das Hyperlink-Konzept bei Single-Page-Applikationen ist also ein anderes, als bei Multi-Page-Applikationen. Während in MPAs Hyperlinks verwendet werden, sprechen wir bei SPAs von Routen. Das dazugehörige Konzept heißt Routing.
Erste einfache Routen¶
Wir erstellen uns mithilfe von
ein neues Angular-Projekt. Alle Fragen beantworten wir einfach durch Bestätigung mit derEnter
-Taste. Um das Routing auszuprobieren, benötigen wir zunächst ein paar Komponenten, zwischen denen wir wechseln können. Wir wechseln in den Ordner routing
und erstellen wir uns folgende Komponenten:
Außerdem fügen wir unserem Projekt noch Bootstrap hinzu, damit wir ein besseres Design erzielen (hat aber nichts mit Routing zu tun). Dazu führen wir zunächst
aus und fügen dann folgende Zeilen in dieangular.json
ein:
Die Komponenten können Sie wie folgt implementieren:
Die app.component.html
sieht nun wie folgt aus:
und in der app.component.ts
sind die FooterComponent
und NavComponent
importiert:
Die AppComponent
ist nun so gestaltet, dass oben die nav
-Komponente und unten die footer
-Komponente eingebunden wird. Dazwischen steht jedoch der Komponentenselektor <router-outlet></router-outlet>
. An dessen Stelle wird nun jeweils die Komponente eingesetzt, die wir durch das Routing ausgewählt haben. Dies erledigen wir in den folgenden Schritten.
Routen definieren¶
Zunächst definieren wir die Routen und zu jeder Route, welche Komponente dafür eingebunden wird. Die Routendefinitionen erfolgen in der app.routes.ts
und dort im routes
-Array. Dazu wird das routes
-Array mit Objekten befüllt, die jeweils einen path
-Eintrag und einen component
-Eintrag erhalten. Ein solches Objekt legt fest, für welchen Pfad welche Komponente aufgerufen wird.
Testen Sie nun die URLs
und Sie sehen jeweils, dass die für die jeweilige Route angegebene Komponente eingebunden wird. Der <router-outlet></router-outlet>
-Selektor wird also dynamisch befüllt, je nachdem welche Route aufgerufen wird.
Eine Sache ist jetzt jedoch noch nicht optimal. Erstens ist ganz am Anfang, also für http://localhost:4200
gar keine Komponente eingebunden und zweitens soll unsere home
-Komponente gar nicht unter einer extra Route (http://localhost:4200/home
), sondern tatsächlich bereits unter http://localhost:4200
aufgerufen werden. Wir passen deshalb das routes
-Array entsprechend an:
Die neuhinzugefügte Eigenschaft pathMatch: 'full'
gibt an, dass diese Route nur aufgerufen wird, wenn danach nichts weiter in der URL folgt. Die Auswahl der Routen erfolgt nach dem first-match-Prinzip. Das heißt, dass für die angegebene URL die erste Route ausgewählt wird, die "passt". Mit pathMatch: 'full'
geben wir an, dass die Route zwar passen muss, aber nicht nur ein Präfix einer längeren Route sein darf. Nun funktionieren die Routen wie gewünscht:
Für die erste URL wird die home
-Komponente eingebunden, bei der zweiten die about
-Komponente und bei der dritten die login
-Komponente. Nun fehlt für die Definition nur noch eine Sache: Was soll passieren, wenn eine Route eingegeben wird, die gar nicht existiert, also z.B.
Für diesen Fall nutzen wir eine Wildcard **
und leiten auf die Route für unsere HomeComponent
um. Wir könnten stattdessen aber auch dafür eine PageNotFoundComponent
(404
-Seite) einfügen und diese für einen solchen Fall aufrufen.
Nun müssen wir noch organisieren, wie die Routen innerhalb unserer Anwendung aufgerufen werden können (und nicht nur durch Eingabe der jeweiligen URL).
Routen aufrufen¶
Wir wollen die Routen durch Mausklick aufrufen. Dafür bietet sich unser Navigationsmenü an. Routen werden nicht per href
-Attribut aufgerufen, sondern per routerLink
. Wir passen dazu unsere nav
-Komponente an:
Leider funktioniert das Routing jetzt noch nicht. Dazu müssen wir in der nav.component.ts
erst noch RouterLink
importieren:
Wir können das routerLink
-Attribut auch unter Verwendung von Attributbinding festlegen (dann kann Routing später sogar über Variablen erfolgen). Wenn Sie es als Attributdirektive gestalten (hat später einen Vorteil bei parametrisierten Routen), dann sieht es so aus:
Angenommen, Sie definieren sich noch eine eigene CSS-Klasse, in der sie festlegen, dass die Menüeinträge anders aussehen, wenn sie der aktuellen Route entsprechen, wenn also z.B. Login
im Menü fett erscheint, sobald http://localhost:4200/login
ausgewählt wurde. Die CSS-Definition könnte dann so aussehen:
Das heißt, Sie haben eine CSS-Klasse myactive
definiert. Diese Klasse kann aktiviert werden, wenn die Route aktiv ist. Dazu verwenden Sie das Attribut routerLinkActive
und weisen diesem Attribut den Wert "myactive"
zu. Das Menü sähe dann so aus:
Auch RouterLinkActive
muss in der nav.component.ts
importiert werden:
Wenn Sie Bootstrap verwenden, dann ist routerLinkActive
nur für eigene CSS-Klassen notwendig (so wie im Beispiel myactive
). Die Bootstrap-Klasse active
wird automatisch aktiviert, wenn die Route aktiv ist.
Routenparameter¶
Häufig sollen aus einer Liste von Objekten ein einzelnes Objekt ausgewählt und dargestellt werden. Angenommen, wir wollen folgende staedte.json
staedte,json
[
{
"id":1,
"jahr":1237,
"stadt":"Berlin",
"link":"http://de.wikipedia.org/wiki/Berlin",
"bild":"/assets/images/berlin.png"
},
{
"id":2,
"jahr":1624,
"stadt":"New York",
"link":"http://de.wikipedia.org/wiki/New_York_City",
"bild":"/assets/images/newyork.png"
},
{
"id":3,
"jahr":1252,
"stadt":"Stockholm",
"link":"http://de.wikipedia.org/wiki/Stockholm",
"bild":"/assets/images/stockholm.png"
},
{
"id":4,
"jahr":1827,
"stadt":"Bremerhaven",
"link":"http://de.wikipedia.org/wiki/Bremerhaven",
"bild":"/assets/images/bremerhaven.png"
},
{
"id":5,
"jahr":150,
"stadt":"Bremen",
"link":"http://de.wikipedia.org/wiki/Bremen",
"bild":"/assets/images/bremen.png"
},
{
"id":6,
"jahr":1202,
"stadt":"Bernau",
"link":"http://de.wikipedia.org/wiki/Bernau_bei_Berlin",
"bild":"/assets/images/bernau.png"
},
{
"id":7,
"jahr":929,
"stadt":"Brandenburg",
"link":"http://de.wikipedia.org/wiki/Brandenburg_an_der_Havel",
"bild":"/assets/images/brandenburg.png"
},
{
"id":8,
"jahr":805,
"stadt":"Magdeburg",
"link":"http://de.wikipedia.org/wiki/Magdeburg",
"bild":"/assets/images/magdeburg.png"
},
{
"id":9,
"jahr":1222,
"stadt":"Marburg",
"link":"http://de.wikipedia.org/wiki/Marburg",
"bild":"/assets/images/marburg.png"
},
{
"id":10,
"jahr":766,
"stadt":"Mannheim",
"link":"http://de.wikipedia.org/wiki/Mannheim",
"bild":"/assets/images/mannheim.png"
},
{
"id":11,
"jahr":782,
"stadt":"Mainz",
"link":"http://de.wikipedia.org/wiki/Mainz",
"bild":"/assets/images/mainz.png"
}
]
verwenden. Wir vereinfachen es und verwenden direkt das Array (von Objekten) und beschreiben die JavaScript-Objekte nicht in JSON, sondern direkt als Objekte (der Unterschied besteht darin, dass die Schlüssel nicht in Anführungsstrichen stehen, siehe JSON.parse()
und JSON.stringify()
.
staedte als Array
[
{
id: 1,
jahr: 1237,
stadt: "Berlin",
link: "http://de.wikipedia.org/wiki/Berlin",
bild: "/assets/images/berlin.png"
},
{
id: 2,
jahr: 1624,
stadt: "New York",
link: "http://de.wikipedia.org/wiki/New_York_City",
bild: "/assets/images/newyork.png"
},
{
id: 3,
jahr: 1252,
stadt: "Stockholm",
link: "http://de.wikipedia.org/wiki/Stockholm",
bild: "/assets/images/stockholm.png"
},
{
id: 4,
jahr: 1827,
stadt: "Bremerhaven",
link: "http://de.wikipedia.org/wiki/Bremerhaven",
bild: "/assets/images/bremerhaven.png"
},
{
id: 5,
jahr: 150,
stadt: "Bremen",
link: "http://de.wikipedia.org/wiki/Bremen",
bild: "/assets/images/bremen.png"
},
{
id: 6,
jahr: 1202,
stadt: "Bernau",
link: "http://de.wikipedia.org/wiki/Bernau_bei_Berlin",
bild: "/assets/images/bernau.png"
},
{
id: 7,
jahr: 929,
stadt: "Brandenburg",
link: "http://de.wikipedia.org/wiki/Brandenburg_an_der_Havel",
bild: "/assets/images/brandenburg.png"
},
{
id: 8,
jahr: 805,
stadt: "Magdeburg",
link: "http://de.wikipedia.org/wiki/Magdeburg",
bild: "/assets/images/magdeburg.png"
},
{
id: 9,
jahr: 1222,
stadt: "Marburg",
link: "http://de.wikipedia.org/wiki/Marburg",
bild: "/assets/images/marburg.png"
},
{
id: 10,
jahr: 766,
stadt: "Mannheim",
link: "http://de.wikipedia.org/wiki/Mannheim",
bild: "/assets/images/mannheim.png"
},
{
id: 11,
jahr: 782,
stadt: "Mainz",
link: "http://de.wikipedia.org/wiki/Mainz",
bild: "/assets/images/mainz.png"
}
]
Erstellen Sie sich im public
-Ordner Ihres Angular-Projektes einen Ordner assets
. In diesen Ordner kopieren Sie den images
-Ordner, den Sie durch Entpacken der images.zip erhalten.
Wir wollen nun über die Route auf ein einzelnes Objekt zugreifen. Wenn wir also z.B. http://localhost:4200/cities/0
eingeben, soll das Berlin
-Objekt ausgewählt werden, bei http://localhost:4200/cities/1
das New York
-Objekt usw.
Dazu erstellen wir uns zunächst eine neue Komponente cities
mit ng g c cities
und folgendem Code:
Parametrisierte Routen¶
Damit wir in der Lage sind, auf Routen, wie http://localhost:4200/cities/0
oder http://localhost:4200/cities/1
geeignet zu reagieren, müssen wir die jeweilige Zahl am Ende der Routen als Parameter definieren. Das machen wir in der app.routes.ts
wie folgt:
Wir wollen zunächst diesen Parameter einfach nur in der cities.component.ts
auslesen. Dazu benötigen wir das Modul ActivatedRoute
.
Wir implementieren den Konstruktor der Klasse CitiesComponent
. Per dependency injection wird darin ActivatedRoute
eingebunden. Wir definieren uns eine Objektvariable route
, die von diesem Typ ist. Die Spezifikation von ActivatedRoute
finden Sie hier.
Nun implementieren wir noch das Interface OnInit
in der Klasse CitiesComponent
. Damit haben wir einen Lifecycle-hook, in wir uns "reinhängen" können. Beim Initialisieren der Komponente wollen wir ermitteln, welche Route dazu geführt hat, dass die Komponente aufgerufen wurde. Zum Implementieren des Interfaces müssen wir dieses zunächst importieren. Wir schreiben neben die Klasse implements OnInit
. Dann schlägt der Quick-Fix vor, dass wir OnInit
importieren (aus @angular/core
). Dann ist jedoch die Klasse selbst noch rot unterstrichen, da wir zum Implementieren des Interfaces die Funktion ngOnInit()
implementieren müssen. Wir folgen erneut dem Quick-Fix und die Funktion erscheint:
Nun implementieren wir die Funktion ngOnInit()
. Die Implementierung sieht wie folgt aus:
Wir erstellen uns eine Objektvariable id
. Diese ist vom Typ string
, kann aber auch null
sein. Wir initialisieren sie mit dem leeren string
. Der Initialisierungswert ist aber egal, da diese Variable auf jeden Fall bei der Initialisierung der Komponente (ngOnInit()
) einen Wert bekommt (Zeile 20
). Zur Wertermittlung verwenden wir die Objektvariable route
, die vom Typ ActivatedRoute
ist (siehe hier). Die Eigenschaft snapshot
gibt die aktuelle Route zurück und paramMap
alle Parameter der Route. Mithilfe von get()
kann man aus der Menge der von paramMap
zurückgegebenen Parameter nach einenm konkreten Parameter filtern. Wir filtern nach dem Parameter id
, da wir diesen in app.routes.ts
mit { path: "cities/:id", component: CitiesComponent },
so benannt haben. Wir hätten dort auch jeden beliebigen anderen Namen wählen können und hätten dann nach diesem Namen gefiltert.
Die Funktion get()
gibt nun entweder den Wert dieses Parameters in der Route als String zurück, z.B. "0"
oder "1"
oder aber, falls kein Parameterwert für id
in der Route enthalten ist, den Wert null
. In Zeile 21
fragen wir ab, ob die Objektvariable id
nun einen Wert hat (oder null
ist). Wenn sie einen Wert hat, wird dieser auf der Konsole ausgegeben, wenn der Wert null
ist, wird auf der Konsole ohne Parameter
ausgegeben.
Wir erstellen uns nun noch eine Funktion, die im Falle eines Wertes für die Objektvariable id
diesen Wert nimmt und damit die Stadt aus dem Array aussucht, die den Index hat, der mit id
übereinstimmt.
Wir erstellen zunächst eine weitere Objektvariable city
. Diese ist ein Object
und wir definieren die Eigenschaften mithilfe von city: {id: number; jahr: number; stadt: string; link: string; bild: string }
. Der Wert dieser Variable kann null
sein, ist er initial auch. Wir verwenden diese Variable, um darin die Stadt zu speichern, die durch die parametrisierte Route ausgewählt wird.
Die filterStaedte()
-Funktion setzt bei parametrieserter Route den Wert der Variablen city
durch Zugriff auf das staedte
-Array. Da id
ein string
ist, wieder dieser mithilfe von Number
in eine number
konvertiert.
Nun führen wir in cities.component.html
nur noch die Fallunterscheidung ein, ob alle Städte oder nur eine angezeigt werden sollen:
In der cities.component.ts
muss nun auch RouterLink
importiert werden, da wir in der Tabelle in der linken Spalte durch Klick auf die Buttons direkt die parametrisierten Routen aufrufen können. Hier nochmal die beiden anderen Dateien der CitiesComponent
im Überblick:
cities.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
|
cities.component.css
Die Daten haben wir hier noch als Array in einer Variablen gespeichert. Nun wollen wir die Daten jedoch in einen zentralen Service auslagern.
Services¶
Ein Service ist eine Klasse für einen konkreten Zweck. Services unterscheiden sich von Komponenten dahingehend, dass
- eine Komponente für die Nutzerinteraktion zuständig ist,
- eine Komponente Eigenschaften (Daten) präsentiert,
- eine Komponente Methoden zur Datenbindung (data binding) zur Verfügung stellt, um
- zwischen View und Anwendungslogik zu vermitteln.
Ein Service
- erfüllt eine konkrete Aufgabe, typischerweise mit Daten,
- ohne sich um die Darstellung der Daten zu kümmern.
- Typische Aufgaben eines Services sind: Daten vom Server holen oder auf den Server laden, Nutzereingaben zu validieren.
- Ein Service steht typischerweise allen Komponenten zur Verfügung (aber nicht jede Komponente muss einen Service nutzen).
Ein Service ist eine Klasse mit dem Decorator @Injectable()
. Services enthalten Anwendungslogik, die aus Komponenten ausgelagert werden kann. Ein Service kann mittels CLI so erzeugt werden:
In dem Decorator @Injectable()
wird mittels providedIn: root
angegeben, dass der Service von allen Komponenten innerhalb des Root-Moduls genutzt werden kann. Ist der Service von anderen Services oder Komponenten abhängig, können diese Services oder Komponenten mittels dependency injection als Parameter des Service-Konstruktor eingebunden werden. Hier ein allgemeines Beispiel eines Services MyService
:
Der Service kann dann mittels dependency injection von einer Komponente verwendet werden. Beispiel:
import {Component, OnInit} from '@angular/core';
import {MyService} from './shared/my.service';
@Component({
selector: 'app-example',
templateUrl: './example.component.html',
styleUrls: ['./example.component.css']
})
export class ExampleComponent implements OnInit {
constructor(private myService: MyService) { }
ngOnInit(): void {
this.example.methodOfMyService();
}
}
Für weiterführende Informationen siehe https://angular.dev/tutorials/learn-angular/19-creating-an-injectable-service#.
Service für das Routing-Beispiel¶
Für unser Routing-Beispiel wollen wir Daten über einen Service allen Komponenten zur Verfügung stellen. Wir erstellen dazu einen Service data
und dazu auch noch ein Interface data
, das das Datenmodell für eine Stadt
beschreibt. Beides erstellen wir in einem shared
-Ordner.
Zur Vorbereitung legen wir zunächst die folgende Datei staedte.json
im public/assets
-Ordner ab:
public/assets/staedte,json
[
{
"id":1,
"jahr":1237,
"stadt":"Berlin",
"link":"http://de.wikipedia.org/wiki/Berlin",
"bild":"/assets/images/berlin.png"
},
{
"id":2,
"jahr":1624,
"stadt":"New York",
"link":"http://de.wikipedia.org/wiki/New_York_City",
"bild":"/assets/images/newyork.png"
},
{
"id":3,
"jahr":1252,
"stadt":"Stockholm",
"link":"http://de.wikipedia.org/wiki/Stockholm",
"bild":"/assets/images/stockholm.png"
},
{
"id":4,
"jahr":1827,
"stadt":"Bremerhaven",
"link":"http://de.wikipedia.org/wiki/Bremerhaven",
"bild":"/assets/images/bremerhaven.png"
},
{
"id":5,
"jahr":150,
"stadt":"Bremen",
"link":"http://de.wikipedia.org/wiki/Bremen",
"bild":"/assets/images/bremen.png"
},
{
"id":6,
"jahr":1202,
"stadt":"Bernau",
"link":"http://de.wikipedia.org/wiki/Bernau_bei_Berlin",
"bild":"/assets/images/bernau.png"
},
{
"id":7,
"jahr":929,
"stadt":"Brandenburg",
"link":"http://de.wikipedia.org/wiki/Brandenburg_an_der_Havel",
"bild":"/assets/images/brandenburg.png"
},
{
"id":8,
"jahr":805,
"stadt":"Magdeburg",
"link":"http://de.wikipedia.org/wiki/Magdeburg",
"bild":"/assets/images/magdeburg.png"
},
{
"id":9,
"jahr":1222,
"stadt":"Marburg",
"link":"http://de.wikipedia.org/wiki/Marburg",
"bild":"/assets/images/marburg.png"
},
{
"id":10,
"jahr":766,
"stadt":"Mannheim",
"link":"http://de.wikipedia.org/wiki/Mannheim",
"bild":"/assets/images/mannheim.png"
},
{
"id":11,
"jahr":782,
"stadt":"Mainz",
"link":"http://de.wikipedia.org/wiki/Mainz",
"bild":"/assets/images/mainz.png"
}
]
In diesen Ordner kopieren wir auch den images
-Ordner, den Sie durch Entpacken der images.zip erhalten - falls Sie es nicht bereits vorher (s.o.) gemacht haben.
Mit
lassen wir die CLI den Service erstellen. Im Ordner shared
entstehen zwei Dateien:
data.service.ts
unddata.service.spec.ts
.
Letztere ist für Testzwecke und interessiert uns (derzeit noch) nicht. In diesen Service binden wir gleich unsere Daten ein und stellen eine Funktion zur Verfügung, die uns alle Daten nach außen zur Verfügung stellt. Zunächst erstellen wir noch, zur Gewährleistung der Typsicherheit, ein Interface für das Datenmodell:
Es entsteht eine Datei city.ts
mit folgendem Inhalt:
In dieses Interface tragen wir unser Datenmodell ein:
Dem data
-Service fügen wir nun ein Funktion hinzu, die die staedte.json
einliest und alle Städte als JavaScript-Objekt zurückgibt. Dazu verwenden wir die fetch
-API. Diese gibt ein Promise
zurück- Mit Promises beschäftigen wir uns später nochmal genauer. Die Promise wiederum enthält ein Response
-Objekt. Da es sich bei uns dabei um ein JSON handelt, können wir dieses Response-Objekt mithilfe der statischen Funktion Response.json()
in ein weiteres Response
-Objekt umwandeln, welches die JSON-Daten zurückgibt. Zum Typisieren der Rückgabe der Funktion verwenden wir das Interface City
:
import { Injectable } from '@angular/core';
import { City } from './city';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor() { }
async getAll(): Promise<City[]> {
let response = await fetch('./assets/staedte.json');
let staedte = await response.json();
console.log('staedte', staedte)
return staedte;
}
}
Das Rückgabeobjekt der Funktion getAll()
ist also einePromise
, d.h. den Aufruf von getAll()
können Sie entweder mit then()
-verketten oder await/async
verwenden. Dazu kommen wir gleich:
Verwendung des Services¶
Wir zeigen die Verwendung des Services zunächst am Beispiel der cities
-Komponente. Dort hatten wir bisher die Daten direkt gespeichert. Nun sollen sie dort über den Service eingebunden werden. Dazu ändern wir die cities.component.ts
wie folgt:
Der Service wird per Dependency Injection im Konstruktor eingebunden. Damit ist service
(die Referenz auf den Service - können Sie nennen, wie Sie möchten) eine weitere Objekteigenschaft der cities
-Komponent. Wir rufen die getAll()
-Funktion des Services auf, die alle Daten des staedte
-Arrays als Promise zurückgibt und speichern diese in der staedte
-Variablen (siehe Konstruktor). Diese ist vonm Typ City[]
. Um diesen Typ zu kennen, muss das Interface City
in die Komponente importiert werden (Zeile 3
). Unsere Anwendung funktioniert nun wieder exakt wie zuvor.
Weiter mit parametrisierten Routen¶
Denselben Service wollen wir nun auch in der city
-Komponente verwenden, in der wir eine einzelne Stadt nach ihrer id
auswählen und darstellen wollen. Dazu erweitern wir zunächst den data
-Service um eine Funktion, die uns ein einzelnes Stadt-Objekt für eine gegebene id
zurückgibt:
Diese Funktion gibt ein City
-Array als ein Promise
zurück. Da id
in unserem JSON eindeutig ist, enthält das zurückgegebene Array entweder ein Element (die Stadt mit der passenden id
) oder keins (wenn id
nicht passt). Die Auswertung, ob das Array einen Eintrag enthält oder nicht, überlassen wir aber der aufrufenden Komponente. Im Gegensatz zu oben, wo wir den Parameter der Route als Index des Arrays verwendet haben, vergleichen wir nun mit der id
. Dadurch ergibt sich ein Versatz von 1
(die Stadt mit der id=1
hat den Index 0
im Array). Wir hätten hier natürlich auch stattdessen den ndex verwenden können, wollten nur mal filter()
verwenden, wenn es id
schonmal gibt...
Die Tabelle, die wir in cities.somponent.html
erzeugen, erweitern wir um eine Spalte, in der wir die Links auf die Detailseiten der jeweiligen Stadt hinterlegen:
Wir sehen darin, dass der Wert für routerLink
auch ein Array sein kann, dessen erster Eintrag die Route und dessen zweiter Eintrag eine anschließende /id
sein kann. Der so beschriebene Wert ergibt dann die Routen /cites/1
, /cities/2
usw. Es hätte auch funktioniert, wenn wir <a [routerLink]="'/cities/'+(i+1)">Detail</a>
geschrieben hätten.
Die cities.component.ts
sieht nun so aus:
Neuladen bei neuer Route¶
Angenommen, wir erweitern die city.component.html
um zwei weitere Navigationsbuttons, um zwischen den einzelnen Städten "zu blättern":
Wenn wir nun auf einen solchen Navigationsbutton klicken, dann sehen wir, dass sich im URL-Fenster die Route ändert. Jedoch erscheint keine neue Stadt. Das liegt daran, dass die Komponente nicht automatisch neu geladen wird, wenn sich nur der Routenparameter ändert. Der Parameter wird nur beim Initialisieren der Komponente ausgelesen (in ngOnInit()
). Es lässt sich jedoch "beobachten", ob sich der Parameter ändert. paramMap
von ActivatedRoute
liefert einen sogenannten Observer
. An diesen Observer
kann man sich mithilfe von subscribe()
anmelden. Sobald sich der Observer
ändert, werden alle Subscriber darüber benachrichtigt. Wir ändern entsprechend die city.component.ts
:
Wir haben den Code also nur um eine Zeile ergänzt. Wenn sich jetzt der Parameter der Route ändert, wird die ngOnInit()
-Funktion einfach erneut aufgerufen. Nun funktioniert auch das Blättern zwischen den Städten.
Routen absichern mit Guards¶
Guards sind Funktionen, die entscheiden, ob ein Navigationsschritt ausgeführt werden darf oder nicht. Diese Entscheidung wird durch den Rückgabewert der Funktion ausgedrückt. Es gibt drei verschiedene Varainten für den Rückgabewert:
true
: der Navigationsschritt wird ausgeführt,false
: der Navigationsschritt wird nicht ausgeführt,- Rückgabe vom Typ
URLTree
: die Navigation wird abgebrochen und eine Navigation zu einer anderen Route gestartet.
Guards werden immer als Eigenschaft einer Route definiert, also bereits bei der Definition der Route im routes
-Array in app.routes.ts
. Es gibt vier verschiedene Guard-Typen:
CanAvtivate
: entscheidet, ob eine Route aktiviert werden darf,CanAvtivateChild
: entscheidet, ob die Kind-Routen einer Route aktiviert werden dürfen (Kind-Routen haben wir uns bis jetzt noch nicht angeschaut),CanDeaktivate
: entscheidet, ob eine Route deaktiviert werden darf,CanLoad
: entscheidet, ob ein Module (asynchron) geladen werden darf.
Uns genügt es, CanActivate
zu betrachten. Damit wollen wir regulieren, dass nur eine bestimmte Rolle von Nutzern eine bestimmte Komponente verwenden darf. Wir erstellen uns einen solchen Guard mithilfe des Angular CLI und nennen den Guard authguard
:
Dadurch entsteht eine Datei authguard.guard.ts
im Ordner shared
mit folgendem Inhalt:
Um dieses Beispiel etwas realistischer zu gestalten, erstellen wir noch einen auth
-Service, der später unserer Nutzer- und Rollenverwaltung dient. Wir nennen ihn auth
und erstellen ihn ebenfalls im shared
-Ordner:
In diesen Service fügen wir nur eine dummy-Funktion isAuthenticated()
ein, die ein true
oder false
zurückliefert:
Diesen Service und davon insbesondere die isAuthenticated
-Funktion verwenden wir in unserem auth
-Guard:
Der Rückgabewertes der authguardGuard
-Funktion ist abhängig vom Rückgabewert der isAuthenticated()
-Funktion des AuthServices
. Liefert diese Funktion ein true
zurück, dann gibt auch die authguardGuard()
-Funktion ein true
zurück. Ist der Rückgabewert jedoch false
, dann liefert die authguardGuard()
-Funktion ein UrlTree
in der Form zurück, dass die Navigation auf die Route /login
umgeleitet wird.
Jetzt können wir diesen Guard verwenden und passen dafür die app.routes.ts
an. Wir wollen hier exemplarisch demonstrieren, dass die /cities
- und /cities/:id
-Routen nur dann aktiviert werden können, wenn die authguardGuard()
-Funktion des AuthGuard
s ein true
zurückliefert. Dazu sind folgende Änderungen in der app.routes.ts
notwendig:
Wenn wir nun auf /cities
navigieren wollen, dann werden wir direkt auf die /login
-Route umgeleitet. Die CitiesComponent
und auch die CityComponent
bleiben gesperrt, solange isAuthenticated()
ein false
zurückliefert.
Erweiterung des Guards¶
Wir prüfen im Guard derzeit (nur), ob eine Nutzerin eingeloggt ist. Angenommen, der AuthService
stellt auch eine Funktion isAdmin()
zur Verfügung, die ein true
zurückgibt, wenn die Nutzerin in der Rolle admin
ist und false
sonst. Dann könnten wir unseren Guard wir folgt erweitern:
Wir haben hier übrigens die ursprüngliche authguardGuard()
-Funktion in authguardLogin
umbenannt und haben eine weitere Funktion hinzugefügt, die nun ein true
zurückgibt, wenn es sich um einen admin
handelt und ansonsten auf die /login
-Route navigiert.
Angenommen, wir wollen nun, dass man die Route /cities
wählen kann, wenn man eingeloggt ist, die Route /cities/:id
aber nur, wenn man als admin
eingeloggt ist, dann sehe app.routes.ts
wie folgt aus:
Success
Wir haben die wesentlichsten Konzepte des Routing kennengelernt. Darüber hinaus gibt es noch Themen für Fortgeschrittene, wie z.B. lazy-loading von Modulen (Module erst dann laden, wenn man sie wirklich erst aufruft), Routen für Kindkomponenten, mehrere outlets usw. Aber uns genügen die hier erläuterten wesentlichen Konzepte.