Sie sind hier: Weblog

Mit Guards ins Routing des Angular-2-Routers eingreifen

Foto ,
17.06.2016 01:00:00

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);