Guards entscheiden, ob du eintreten darfst (canActivate), ob die Route für dich existiert (canMatch) oder ob du sie verlassen darfst (canDeactivate). Resolver laden Daten, bevor die Seite erscheint. Im modernen Angular sind sie alle Funktionen, die inject() verwenden — keine Klassen, keine Interfaces zu implementieren.
Ein funktionaler Guard
import { inject } from '@angular/core';
import { CanActivateFn, RedirectCommand, Router } from '@angular/router';
export const adminGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthStore);
const router = inject(Router);
if (auth.isAdmin()) return true;
// UrlTree: einfacher Redirect
// return router.parseUrl('/login');
// RedirectCommand: Redirect + Navigationsoptionen
return new RedirectCommand(router.parseUrl(`/login?returnUrl=${state.url}`), { replaceUrl: true });
};Öffentlicher Bereich. Jeder kann diese Seite sehen.
canDeactivate: ungespeicherte Änderungen
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) =>
!component.hasUnsavedChanges() || confirm('Du hast ungespeicherte Änderungen. Trotzdem verlassen?');
@Component({ /* ... */ })
export class EditProfilePage implements HasUnsavedChanges {
readonly form = form(this.model);
hasUnsavedChanges() {
return this.form().dirty();
}
}Resolver
export const reportResolver: ResolveFn<Report> = (route) => {
const api = inject(ReportsApi);
return api.get(Number(route.paramMap.get('id'))); // Observable / Promise / einfacher Wert
};
@Component({ /* ... */ })
export default class ReportPage {
readonly report = input.required<Report>(); // kommt aus resolve, bereits geladen
}Resolver httpResource in der Komponente
─────────────────────────────────────────────────────────────────────────────
Seite erscheint mit fertigen Daten Seite erscheint sofort, mit Ladezustand
Navigation „wartet“, bis Daten da sind Navigation ist sofort
Fehler = Navigation abgebrochen Fehler = Zustand in der UI (error())
gut für: notwendige, kleine Daten gut für: Listen, Dashboards, schnelle UXcanMatch vs. canActivate
// canMatch vs canActivate
// canMatch: entscheidet, ob die Route für den Nutzer „existiert“. Wenn nicht → Angular probiert die nächste Route
// mit demselben Pfad, und der Lazy Chunk wird NICHT heruntergeladen.
// canActivate: die Route existiert, aber der Zutritt ist blockiert (der Chunk ist womöglich schon heruntergeladen).
const routes: Routes = [
{ path: 'dashboard', canMatch: [isAdmin], loadComponent: () => import('./admin-dashboard') },
{ path: 'dashboard', loadComponent: () => import('./user-dashboard') }, // Fallback für alle anderen
];
// v22: currentSnapshot ist jetzt ein Pflichtparameter in CanMatchFn / CanMatch.canMatch (route, segments, currentSnapshot)
// v22.2: Du kannst aus Guards/Resolvern einen RedirectCommand WERFEN, um umzuleitenIn welcher Reihenfolge sie laufen
Bei einer Navigation durchläuft der Router feste Schritte. Sagt ein Schritt „nein“, laufen die folgenden nicht mehr:
canMatch, während er die Route sucht. Ein „nein“ hier bedeutet, dass die Route für diesen Nutzer nicht existiert, und der Router probiert die nächste.canDeactivateauf den Routen, die du verlässt.canActivateChildundcanActivate, vom Parent zum Kind.- Die Resolver. Erst wenn alle fertig sind, erscheint die Komponente.
Ein asynchroner Guard
Ein Guard kann ein Promise oder ein Observable zurückgeben, und die Navigation wartet. Der typische Fall: Der Nutzer ist beim Start der App noch nicht geladen. Warte auf den ersten echten Wert, nicht auf den leeren Anfangszustand.
export const authGuard: CanActivateFn = (_route, state) => {
const auth = inject(AuthStore);
const router = inject(Router);
// warten, bis es nicht mehr „loading“ ist, dann einmal entscheiden
return toObservable(auth.status).pipe(
filter((s) => s !== 'loading'),
take(1),
map((s) =>
s === 'signed-in'
? true
: router.createUrlTree(['/login'], { queryParams: { next: state.url } }),
),
);
};Resolver-Daten als Input
Mit withComponentInputBinding() landen die Schlüssel aus resolve direkt in den Inputs der Komponente, genau wie URL-Parameter. Die Komponente braucht kein ActivatedRoute mehr.
// app.config.ts
provideRouter(routes, withComponentInputBinding());
// routes.ts
{ path: 'reports/:id', component: ReportPage, resolve: { report: reportResolver } }
// report-page.ts — ohne ActivatedRoute
export class ReportPage {
readonly id = input.required<string>(); // aus der URL
readonly report = input.required<Report>(); // aus resolve
}Wann ja, wann nein
canMatchbei eingeschränkten Features (Admin) — du lädst den Code nicht umsonst herunter.canActivatefür Authentifizierung / Berechtigungen auf Seiten.canDeactivatefür Formulare mit ungespeicherten Änderungen.- Ein Resolver für kleine, notwendige Daten, ohne die die Seite keinen Sinn ergibt.
- Guards als einzige Sicherheit — der Server muss ohnehin prüfen.
- Resolver für große Listen → die Seite wirkt „eingefroren“. Nutze
httpResource+ ein Skeleton. - Business-Logik in einem Guard (Berechnungen, Transformationen) → verschiebe sie in einen Service.
Häufige Stolperfallen
- Ein Guard, der
falseohne Redirect zurückgibt → der Nutzer bleibt stehen, ohne zu verstehen, warum. Gib einenUrlTree/RedirectCommandzurück. - Ein Guard am Parent, aber Navigationen zwischen Kindern:
canActivateam Parent läuft beim Wechsel des Kindes nicht erneut — nutzecanActivateChildoderrunGuardsAndResolvers. - Ein Resolver, der fehlschlägt, bricht die Navigation ab — behandle den Fehler (Redirect auf eine Fehlerseite).
Spickzettel
// Reihenfolge: canMatch → canDeactivate → canActivateChild / canActivate → resolve
canMatch: [() => inject(AuthStore).isAdmin()]
canActivate: [authGuard]
canActivateChild: [authGuard]
canDeactivate: [(c: HasUnsavedChanges) => !c.hasUnsavedChanges() || confirm('Ohne Speichern verlassen?')]
resolve: { report: reportResolver }
// was ein Guard zurückgeben kann
return true | false | router.createUrlTree(['/login']) | new RedirectCommand(tree)
return Promise<...> | Observable<...> // die Navigation wartet
// Guards auch bei geänderten Query-Parametern erneut ausführen
runGuardsAndResolvers: 'paramsOrQueryParamsChange'