Mit Guards können sich Angular-2-Anwendungen über Routenwechsel des neuen, seit Juni 2016 zur Verfügung stehenden Angular-2-Routers (Version 3) informieren lassen. Dabei handelt es sich lediglich um Services, welche die Lifecycle-Hooks des "BETA-Routers" ablösen. Ihre über Interfaces vorgegebenen Methoden ruft der Router beim Deaktivieren einer Komponente bzw. beim Aktivieren einer neuen Komponente auf. Der zurückgelieferte Wert bestimmt, ob der Router den angeforderten Routenwechsel tatsächlich durchführen darf. Hierzu kommen true
, false
oder ein Observable<boolean>
zum Einsatz. Letzteres erlaubt das Hinauszögern der Entscheidung, um zum Beispiel eine Web API zu konsultieren oder Rücksprache mit dem Benutzer zu halten.
Der Router bietet für die Implementierung von Guards zwei Interfaces: CanActivate
definiert die Methode canActivate
, welche der Router vor dem Aktivieren einer Komponente anstößt. Das Interface CanDeactivate
kommt analog dazu mit der Methode canDeactivate
, die der Router vor dem Deaktivieren einer Komponente zur Ausführung bringt. Dieser Beitrag zeigt, wie eine Anwendung mit canDeactivate
vor dem Verlassen einer Route eine Bestätigung vom Benutzer einholen kann. Der gesamte Quellcode findet sich hier.
Installation des neuen Routers
Um den neuen Router zu beziehen, ist die Version 3 des Paketes @angular/router zu installieren. Als dieser Text geschrieben wurde, verwies das QuickStart-Sample auf angular.io noch auf Version 2. In diesem Fall ist der nachfolgende Eintrag in der Datei package.json
zu aktualisieren. Ein Aufruf von npm install
installiert Version 3 des Routers anschließend.
"dependencies": {
[...]
"@angular/router": "3.0.0-alpha.6"
}
Implementierung des Guards
Beim hier verwendeten Guard handelt es sich um einen einfachen Angular-2-Service, der das Interface CanDeactivate
implementiert. Dieses Interface ist mit dem Typ der Komponente, um welche sich der Guard kümmert, zu parametrisieren:
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot} from @angular/router;
import { FlightEditComponent} from ./flight-edit.component;
export class FlightEditGuard implements CanDeactivate<FlightEditComponent> {
canDeactivate(
component: FlightEditComponent,
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot) {
return component.canDeactivate();
}
}
Implementierung der Komponente
Die Methode canActivate
nimmt die betroffene Instanz dieser Komponente sowie Parameter, welche über den aktuellen Zustand sowie künftigen Zustand des Routers informieren, entgegen. Die hier beschriebene Implementierung delegiert an die Komponente, damit diese für den Benutzer eine Warnmeldung einblendet.
Die Methode canDeactivate
der Komponente, welche auf diesem Weg zur Ausführung kommt, setzt die Eigenschaft exitWarning.show
auf true
. Damit blendet sie die Warnmeldung ein. Diese fragt den Benutzer, ob er tatsächlich die aktuelle Route verlassen möchte. Da canActivate
die Antwort des Benutzers abwarten muss, liefert sie ein Observable<boolean>
zurück und verstaut den dazu passenden Observer<boolean>
in der Eigenschaft exitWarning.observer
.
Die Methode decide
nimmt diese Entscheidung entgegen und blendet die Warnmeldung wieder aus, indem sie die Eigenschaft exitWarning.show
auf false
zurücksetzt. Danach gibt sie über den vorhin erhaltenen Observer
die Entscheidung an das Observable
und somit an den Router weiter.
import { Component} from @angular/core;
import { ActivatedRoute} from @angular/router;
import { Observable, Observer} from rxjs;
@Component({
template: require(./flight-edit.component.html)
})
export class FlightEditComponent {
[...]
exitWarning = {
observer: null,
show: false
}
decide(d: boolean) {
this.exitWarning.show = false;
this.exitWarning.observer.next(d);
this.exitWarning.observer.complete();
}
canDeactivate() {
this.exitWarning.show = true;
return new Observable<boolean>((sender: Observer<boolean>) => {
this.exitWarning.observer = sender;
});
}
}
Das Template für die Warnmeldung findet sich im nächsten Listing. Es blendet die Warnmeldung in Abhängigkeit von exitWarning.show
mittels *ngIf
ein. Die Entscheidung des Benutzers, welche die Warnung über einen der beiden Links entgegennimmt, delegiert das Template an die Methode decide
weiter.
<div *ngIf="exitWarning.show" class="alert alert-warning">
<div>
Daten wurden nicht gespeichert! Trotzdem Maske verlassen?
</div>
<div>
<a href="javascript:void(0)" (click)="decide(true)" class="btn btn-danger">Ja</a>
<a href="javascript:void(0)" (click)="decide(false)" class="btn btn-default">Nein</a>
</div>
</div>
Guard in Router-Konfiguration registrieren
Um den Guard für eine Komponente zu registrieren, verweist der betroffene Eintrag der Router-Konfiguration über das Array canDeactivate
darauf:
const APP_ROUTES: RouterConfig = [
{
path: /home,
component: HomeComponent,
index: true
},
{
path: /flight-booking,
component: FlightBookingComponent,
children: [
{
path: /flight-search,
component: FlightSearchComponent
},
{
path: /passenger-search,
component: PassengerSearchComponent
},
{
path: /flight-edit/:id,
component: FlightEditComponent,
canDeactivate: [FlightEditGuard]
}
]
}
];
Genaugenommen verweist canDeactivate
lediglich auf ein Token, welches im Rahmen eines Provider-Arrays an einen Service zu binden ist. Das nachfolgende Listing nutzt hierzu die übliche Kurzschreibweise, um den Service FlightEditGuard
an das gleichnamige Token zu binden:
export const APP_ROUTER_PROVIDERS = [
FlightEditGuard,
// entspricht:
// provide(FlightEditGuard, {useClass: FlightEditGuard})
provideRouter(APP_ROUTES),
];
Damit Angular 2 dieses Provider-Array berücksichtigt, nimmt es das hier beschriebene Beispiel in ein weiteres Provider-Array auf. Anschließend übergibt es dieses Provider-Array an bootstrap
. Auf diese Weise nutzt das Beispiel die Tatsache, dass eine Angular-Anwendung Provider-Arrays beliebig verschachteln kann:
var providers = [
APP_ROUTER_PROVIDERS,
HTTP_PROVIDERS
];
bootstrap(AppComponent, providers);