Rezepte für Routing

In der Regel brauchen, vor allem größere Anwendungen verschiedene Views, die abhängig von eine Kondition angezeigt werden. Eine Möglichkeit so etwas zu implementieren, ist mittels Client-Seitigen Routing. Je nach Browser-URL, wird die Anwendung dem Nutzer eine andere View bzw. andere Inhalte anzeigen. Da wir beim Client-Seitigen Routing die Browser-URL als einer Art Zustand nutzen, kann ein Nutzer z. B. die URL in ein anderes Browser-Fenster kopieren und einfach dort weiter machen wo er war.

In diesem Kapitel, werden wir uns mit dem Router, den Angular uns zur Verfügung stellt beschäftigen. Wir werden uns nicht alle mögliche Funktionen des Routers anschauen. Dafür bräuchte man ein eigenes Buch. Ziel des Kapitels ist es mit ein paar wenige Rezepte, eine Basis zu schaffen, die man später je nach konkreten Anwendungsfall erweitern kann.

Einfaches Routing implementieren

Problem

Ich möchte meine Anwendung mit Hilfe des Routers in drei Teilbereichen aufspalten.

Zutaten

  • Angular 2 Anwendung
  • Eine Komponente für jeden Teilbereich
  • Das Router-Modul von Angular
  • Router-Konfiguration mit Pfaden für die drei Teilbereiche (app.routes.ts)
  • Anpassungen an der app.component.ts- und der app.module.ts-Datei
  • Die RouterLink-Direktive von Angular-Router
  • Die RouterOutlet-Direktive von Angular-Router
  • Anpassungen an der package.json-Datei

Lösung

Wie schon erwähnt, wollen wir die Anwendung in drei Teilbereichen aufspalten. Diese sind “Home”, “Products” und “Admin” und für jeden diese Teilbereiche werden wir eine Komponente implementieren.

home.component.ts
1 import { Component } from '@angular/core';
2 
3 @Component({
4   template: '<h1>Home</h1>'
5 })
6 export class HomeComponent {}

Erklärung:

Wie wir sehen, haben wir jetzt keine selector-Eigenschaft für den @Component-Decorator definiert. Wir brauchen diese nicht, weil unsere Komponente nicht in einem Template referenziert wird, sondern vom Router abhängig vom Pfad geladen wird. Die Restlichen zwei Komponenten zeigen wir hier nicht, diese werden analog zu der “HomeComponent” definiert.

Damit der Router weiß welcher Pfad bzw. URL zu welcher Komponente gehört, müssen wir diesen jetzt konfigurieren. Wir tun dies in eine eigene Datei, so dass es später einfacher ist die Konfiguration zu finden und zu erweitern bzw. anpassen.

app.routes.ts
 1 import { RouterModule, Routes } from '@angular/router';
 2 
 3 import { AdminComponent } from './admin.component';
 4 import { HomeComponent } from './home.component';
 5 import { ProductsComponent } from './products.component';
 6 
 7 const routes: Routes = [
 8   { path: '', component: HomeComponent },
 9   { path: 'admin', component: AdminComponent },
10   { path: 'products', component: ProductsComponent },
11 ];
12 
13 export const routing = RouterModule.forRoot(routes);

Erklärung:

  • Zeile 1: Als Erstes importieren wir die nötigen Abhängigkeiten aus dem @angular/router-Paket. Dieses kann mittels npm installiert werden
  • Zeilen 7-11: Unsere Router-Konfiguration. Die path-Eigenschaft ist der Teil nach der Domain der URL. Z. B. in der URL “http://localhost:4200/foo” ist “foo” der Pfad
    • Zeile 8: Wenn kein Pfad angegeben wird, wird die “HomeComponent” angezeigt
    • Zeile 9: Wenn der Pfad “admin” ist, wird die “AdminComponent” angezeigt
    • Zeile 10: Wenn der Pfad “products” ist, wird die “ProductsComponent” angezeigt
  • Zeile 13: Hier wird die Konfiguration dem Router übergeben. Die forRoot-Methode gibt dann ein Angular-Modul zurück mit der Konfiguration und Services, die uns die Arbeit mit dem Router erleichtern

Wie jedes andere Angular-Modul, müssen wir auch das Routing-Modul in unser “AppModule” importieren.

app.module.ts
 1 import { NgModule }      from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 
 4 import { routing } from './app.routes';
 5 import { AppComponent }  from './app.component';
 6 import { AdminComponent } from './admin.component';
 7 import { HomeComponent } from './home.component';
 8 import { ProductsComponent } from './products.component';
 9 
10 @NgModule({
11   imports: [ BrowserModule, routing ],
12   declarations: [
13     AppComponent, AdminComponent,
14     HomeComponent, ProductsComponent
15   ],
16   bootstrap: [ AppComponent ]
17 })
18 export class AppModule { }

Bis jetzt haben wir die Komponenten für unsere Teilbereiche definiert und den Router konfiguriert. Jetzt müssen wir die Teilbereiche noch anzeigen und ein einfaches Menü für die Navigation definiert. Wir tun dies in der app.component.ts-Datei.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <nav>
 7       <ul>
 8         <li><a routerLink="">Home</a></li>
 9         <li><a routerLink="products">Products</a></li>
10         <li><a routerLink="admin">Admin</a></li>
11       </ul>
12     </nav>
13     <router-outlet></router-outlet>
14   `
15 })
16 export class AppComponent {}

Erklärung:

  • Zeilen 6-12: Die Navigation für unsere Anwendung. Mit der RouterLink-Direktive definieren wir, zu welchem Teilbereich der Router hin navigieren soll, wenn der Nutzer auf das Element klickt. Die Pfade, die wir der RouterLink-Direktive übergeben, müssen zu den path-Eigenschaften in der Router-Konfiguration passen
  • Zeile 13: Mit der RouterOutlet-Direktive, sagen wir dem Router wo er die Teilbereiche anzeigen soll. Je nach Pfad bzw. Routerzustand, entscheidet der Router welcher Teilbereich angezeigt werden muss

Da sich das “RouterModule” in einem eigenen npm-Paket befindet, müssen wir dieses auch in der package.json deklarieren.

package.json
1 {
2   ...
3   "dependencies": {
4     ...
5     "@angular/router": "3.1.2"
6     ...
7   }
8   ...
9 }

Wenn eine Angular-Anwendung mit angular-cli initialisiert wird, wird das “RouterModule” automatisch von angular-cli importiert und das entsprechende npm-Paket in der package.json-Datei deklariert.

Diskussion

Standardmäßig nutzt der Angular-Router HTML5-URLs. Allerdings brauchen die meisten Webserver eine spezielle Konfiguration, damit diese mit HTML5-URLs umgehen können. Aus diesem Grund bietet uns Angular auch die Möglichkeit Hash-Basierte (#) URLs zu nutzen. Wie das geht wird im Rezept “Hash-Basiert URLs für das Routing” gezeigt

Code

Code auf Github

Weitere Ressourcen

Hash-Basierte URLs für das Routing

Problem

Ich möchte Hash-Basierte URLs für das Routing nutzen, da mein Webserver mit HTML5-URLs nicht umgehen kann.

Zutaten

  • Einfaches Routing implementieren
  • Den LocationStrategy-Service von Angular-Common
  • Den HashLocationStrategy-Service von Angular-Common
  • Anpassungen an der app.module.ts-Datei

Lösung

app.module.ts
 1 import { NgModule } from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 import {
 4   LocationStrategy,
 5   HashLocationStrategy
 6 } from '@angular/common';
 7 
 8 import { routing } from './app.routes';
 9 import { AppComponent }  from './app.component';
10 import { AdminComponent } from './admin.component';
11 import { HomeComponent } from './home.component';
12 import { ProductsComponent } from './products.component';
13 
14 @NgModule({
15   imports: [ BrowserModule, routing ],
16   declarations: [
17     AppComponent, AdminComponent,
18     HomeComponent, ProductsComponent
19   ],
20   providers: [ { provide: LocationStrategy, useClass: HashLocationStrategy } ],
21   bootstrap: [ AppComponent ]
22 })
23 export class AppModule { }

Erklärung:

  • Zeile 4: Hier importieren wir den LocationStrategy-Service. Dieser sagt Angular wie die URLs auszusehen haben (mit Hash oder HTML5)
  • Zeile 5: Hier importieren wir den HashLocationStrategy-Service. Dieser wird für Hash-Basierte URLs benutzt
  • Zeile 20: Hier sagen wir Angular, dass zur Laufzeit der LocationStrategy-Service, den HashLocationStrategy-Service als Implementierung nutzen soll

Diskussion

Standardmäßig wird eine Instanz der PathLocationStrategy-Klasse (HTML5-URLs) zurückgegeben, wenn mittels Dependency Injection eine Instanz des LocationStrategy-Services gefragt ist. “LocationStrategy” ist ein sogenanntes “Token” und es definiert den Namen eines Services. Die useClass-Eigenschaft definiert welche Klasse für das Token benutzt werden soll. Hier (Zeile 20) sagen wir dem Injector: “Wenn jemand das Token “LocationStrategy” nutzt, um einen Service als Abhängigkeit zu definieren, sollst du eine Instanz der Klasse “HashLocationStrategy” zurückgeben”. Im Rezept Einen Service definieren, haben wir für das providers-Array nur “DataService” angegeben. Die Schreibweise, die wir dort benutzt haben ist äquivalent zu { provide: DataService, useClass: DataService }. Wenn also das Token und die Klasse den gleichen Namen haben, brauchen wir nicht die Objekt-Schreibweise, wie wir diese hier benutzt haben.

Eine vollständige Erklärung, wie Dependency Injection funktioniert und was wir alles damit machen können, gibt es auf der Angular 2 Webseite: Dependency Injection und Hierarchical Dependency Injectors.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Die aktuelle Route hervorheben

Problem

Ich möchte die aktuelle Route in der Navigation farblich hervorheben, damit der Nutzer weiß wo er/sie sich aktuell befindet.

Zutaten

Lösung

Der einfachste Weg, die aktuelle Route farblich hervorzuheben ist mit Hilfe einer CSS-Klasse und der RouterLinkActive-Direktive. Dafür brauchen wir nur die app.component.ts-Datei anzupassen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styles: ['.active-route { color: red; }'],
 6   template: `
 7     <nav>
 8       <ul>
 9         <li><a routerLink=""
10           routerLinkActive="active-route"
11           [routerLinkActiveOptions]="{exact: true}">Home</a></li>
12         <li><a routerLink="products"
13           routerLinkActive="active-route">Products</a></li>
14         <li><a routerLink="admin"
15           routerLinkActive="active-route">Admin</a></li>
16       </ul>
17     </nav>
18     <router-outlet></router-outlet>
19   `
20 })
21 export class AppComponent {}

Erklärung:

  • Zeile 5: Hier definieren wir die “active-route”-CSS-Klasse. Siehe auch Das Template der Komponente von dem CSS trennen
  • Zeilen 10, 13 und 15: Mittels der RouterLinkActive-Direktive, setzen wir die “active-route”-CSS-Klasse auf das Element, das auf die aktuelle Route zeigt

Diskussion

Die RouterLinkActive-Direktive nimmt den Wert der RouterLink-Direktive und ermittelt, ob der Pfad der RouterLink-Direktive der gleiche ist wie der Pfad der aktuellen Route. Falls ja wird der Wert nach dem Gleichheitszeichen als CSS-Klasse für das Element gesetzt. In unserem Fall wird die “active-route”-CSS-Klasse auf das a-Tag gesetzt. Wenn z. B. der Pfad “admin” ist, wird die CSS-Klasse auf das a-Tag mit routerLink=”admin” gesetzt. Für die “Home”-Route nutzen wir einen leeren String als Pfad. Da der leerer String Teil jedes Pfades ist, wird der a-Tag für die “Home”-Route immer als aktuell/aktiv gesetzt. Um das zu verhindern, sagen wir dem Router mittels [routerLinkActiveOptions]=”{exact: true}”, dass das a-Tag mit routerLink=”“ nur dann die aktuelle Route ist, wenn der Pfad der aktuellen Route ein leerer String ist.

Code

Code auf Github

Live Demo auf angular2kochbuch.de. Nutzt Hash-Basierte URLs: “Hash-Basierte URLs für das Routing

Weitere Ressourcen

Umleitung für unbekannte Pfade

Problem

Ich möchte, dass unbekannte Pfade zu der Hauptkomponente umgeleitet (redirect) werden.

Zutaten

Lösung

app.routes.ts
 1 import { RouterModule, Routes } from '@angular/router';
 2 
 3 import { AdminComponent } from './admin.component';
 4 import { HomeComponent } from './home.component';
 5 import { ProductsComponent } from './products.component';
 6 
 7 const routes: Routes = [
 8   { path: '', component: HomeComponent },
 9   { path: 'admin', component: AdminComponent },
10   { path: 'products', component: ProductsComponent },
11   { path: '**', redirectTo: '' },
12 ];
13 
14 export const routing = RouterModule.forRoot(routes);

Erklärung:

  • Zeile 11: Hier definieren wir eine Route mit Pfad “**”, die immer dann greift, wenn keine andere Route zu der Browser-URL passt. Die redirectTo-Eigenschaft bekommt als Wert den Pfad zu den wir umleiten möchten

Code

Code auf Github

Live Demo auf angular2kochbuch.de. Nutzt Hash-Basierte URLs: “Hash-Basierte URLs für das Routing

Navigation in der Klasse der Komponente

Problem

Ich möchte zu eine andere Komponente navigieren, indem ich auf einen Button klicke.

Zutaten

  • Einfaches Routing implementieren
  • Den ActivatedRoute-Service vom Angular-Router
  • Den Router-Service vom Angular-Router
  • Anpassungen an der app.component.ts-Datei

Lösung

Wir wollen jetzt nicht mit Hilfe der RouterLink-Direktive navigieren, sondern mit der navigate-Methode des Routers. Diese können wir nutzen, um zu navigieren als Reaktion z. B. auf ein Event nach dem Speichern von Daten.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import { ActivatedRoute, Router } from '@angular/router';
 3 
 4 @Component({
 5   selector: 'app-root',
 6   template: `
 7     <nav>
 8       <ul>
 9         <li>
10           <button type="button" (click)="navigate('')">
11             Home
12           </button>
13         </li>
14         <li>
15           <button type="button" (click)="navigate('products')">
16             Products
17           </button>
18         </li>
19         <li>
20           <button type="button" (click)="navigate('admin')">
21             Admin
22           </button>
23         </li>
24       </ul>
25     </nav>
26     <router-outlet></router-outlet>
27   `
28 })
29 export class AppComponent {
30   route: ActivatedRoute;
31   router: Router;
32   constructor(route: ActivatedRoute, router: Router) {
33     this.route = route;
34     this.router = router;
35   }
36 
37   navigate(path) {
38     this.router.navigate([ path ], { relativeTo: this.route });
39   }
40 }

Erklärung:

Die a-Tags mit der RouterLink-Direktive, die wir im Rezept “Einfaches Routing implementieren” benutzt haben, haben wir hier mit einem button-Tag ersetzt. Jeder Button ruft bei einem Klick die navigate-Methode der Komponenten-Klasse und übergibt den Pfad.

  • Zeile 32: Hier injizieren wir den ActivatedRoute- und den Router-Service. Der ActivatedRoute-Service repräsentiert die aktuelle Route
  • Zeilen 37-39: navigate-Methode, die von den Buttons aufgerufen wird
    • Zeile 38: Hier wird die navigate-Methode des Routers aufgerufen. Dieser Aufruf ist äquivalent zu der RouterLink-Direktive, die wir schon gesehen haben

Diskussion

Wenn wir mit einem Pfad der ohne Slash (/) beginnt navigieren z. B. “admin”, führen wir eine relative Navigation aus. Dem entsprächen heißen Pfade, die ohne Slash beginnen “relative Pfade”. Auch Pfade, die mit “./” (die Pfade “admin” und “./admin” sind äquivalent) oder “../” beginnen sind relativ. Pfade, die mit einem Slash beginnen z. B. “/admin” heißen “absolute Pfade”. Immer wenn wir relative Pfade nutzen, müssen wir dem Router sagen relativ zu welcher Route wir navigieren möchten. Im Falle der RouterLink-Direktive wird immer relativ zu der aktuellen Route navigiert. Mit der navigate-Methode müssen wir explizit die Route angeben zu der wir relativ Navigieren wollen, indem wir die relativeTo-Eigenschaft setzen.

Die Auswirkungen von relativen Pfaden sind vor allem sichtbar, wenn wir innerhalb einer Komponente navigieren, die einen eigenen nicht leeren Pfad hat z. B. AdminComponent mit “admin” als Pfad. In unserem Beispiel hätten wir problemlos auch absolute Pfade nutzen können z. B. in Zeile 15 “/products” statt “products”. Wenn wir absolute Pfade nutzen, brauchen wir den zweiten Parameter ({relativeTo: this.route}) der navigate-Methode nicht.

Code

Code auf Github

Live Demo auf angular2kochbuch.de. Nutzt Hash-Basierte URLs: “Hash-Basierte URLs für das Routing

Weitere Ressourcen

Routing-Parameter

Problem

Ich möchte eine Komponente implementieren, die unterschiedliche Inhalte in der View anzeigt abhängig von einem Parameter in der URL.

Zutaten

  • Einfaches Routing implementieren
  • Anpassungen an der products.component.ts-Datei
  • Eine Komponente. Der Inhalt der View der Komponente wird sich abhängig von der Route-Parameter ändern. Diese Komponente (ProductComponent) zeigt das jeweilige Produkt an
  • ActivatedRoute-Service von Angular-Router
  • Anpassungen an der app.routes.ts-Datei

Lösung

Als Erstes definieren wir eine parametrisierte Route. Diese bekommt die id-Eigenschaft eines Produktes als Parameter und die “ProductComponent” als Komponente.

app.routes.ts
 1 import { RouterModule, Routes } from '@angular/router';
 2 
 3 import { AdminComponent } from './admin.component';
 4 import { HomeComponent } from './home.component';
 5 import { ProductsComponent } from './products.component';
 6 import { ProductComponent } from './product.component';
 7 
 8 const routes: Routes = [
 9   { path: '', component: HomeComponent },
10   { path: 'admin', component: AdminComponent },
11   { path: 'products', component: ProductsComponent },
12   { path: 'products/:id', component: ProductComponent },
13 ];
14 
15 export const routing = RouterModule.forRoot(routes);

Erklärung:

  • Zeile 12: Unsere neue Route. Mit dem Doppelpunkt und einen Namen (hier id) definieren wir einen Route-Parameter

Jetzt wollen wir eine Liste von Produkten für “ProductsComponent” definieren. Die id-Eigenschaft eines Produktes wird als Route-Parameter benutzt :id wird also zur Laufzeit durch die ID des Produktes ersetzt.

products.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   template: `
 5     <h1>Products</h1>
 6     <ul>
 7       <li *ngFor="let prod of products">
 8         <a [routerLink]="prod.id">{{prod.name}}</a>
 9       </li>
10     </ul>
11   `
12 })
13 export class ProductsComponent {
14   products = [{
15     id: 1,
16     name: 'Product 1'
17   }, {
18     id: 2,
19     name: 'Product 2'
20   }, {
21     id: 3,
22     name: 'Product 3'
23   }];
24 }

Erklärung:

  • Zeile 8: Hier nutzen wir die RouterLink-Direktive und definieren damit zu welchem Produkt wir hin navigieren möchten. Wir nutzen eine Eigenschaft-Bindung (eckige Klammern) weil der Pfad, im Gegensatz zu den Pfaden in der app.component.ts-Datei, nicht konstant ist

Als letztes definieren wir die “ProductComponent”. Diese wird die id-Eigenschaft aus der Route lesen und sie in der View anzeigen.

product.component.ts
 1 import { Component } from '@angular/core';
 2 import { ActivatedRoute } from '@angular/router';
 3 
 4 @Component({
 5   template: `<h1>Product {{id}}</h1>`
 6 })
 7 export class ProductComponent {
 8   id: string;
 9 
10   constructor(route: ActivatedRoute) {
11     this.id = route.snapshot.params['id'];
12   }
13 }

Erklärung:

  • Zeile 11: Hier lesen wir den id-Parameter aus der aktuellen Route. Der String, hier ‘id’ muss der gleiche sein wie der String nach dem Doppelpunkt in der Pfaddefinition (app.routes.ts-Datei). Alternativ können wir die id-Eigenschaft in der ngOnInit-Methode lesen. Mehr Informationen über diese Methode gibt es im Rezept “Code ausführen bei der Initialisierung einer Komponente

Diskussion

In der app.routes.ts-Datei, haben wir den Pfad “products/:id” definiert aber in der “ProductsComponent” haben wir der RouterLink-Direktive (Zeile 8) nur die ID (prod.id) übergeben und trotzdem funktioniert das Routing. Wir wollen jetzt kurz verstehen warum das so ist.

Wir nutzen hier einen relativen Pfad (den Wert von prod.id) und die RouterLink-Direktive befindet sich in einer Komponente die einen nicht leeren Pfad (“products”) hat. In diesem Fall werden die zwei Pfade konkateniert. Das heißt, wenn wir zur Laufzeit auf den Link für das erste Produkt klicken, navigiert der Router zu der Komponente mit Pfad “products/1” (das erste Produkt hat die ID 1). Ausführlichere Informationen zu relativen bzw. absoluten Pfaden gibt es in der Diskussion des Rezepts “Navigation in der Klasse der Komponente

Code

Code auf Github

Live Demo auf angular2kochbuch.de. Nutzt Hash-Basierte URLs: “Hash-Basierte URLs für das Routing

Weitere Ressourcen