Guards decid dacă poți intra (canActivate), dacă ruta există pentru tine (canMatch) sau dacă poți pleca (canDeactivate). Resolvers încarcă date înainte ca pagina să apară. În Angular modern toate sunt funcții care folosesc inject() — fără clase, fără interfețe de implementat.
Un guard funcțional
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: redirect simplu
// return router.parseUrl('/login');
// RedirectCommand: redirect + opțiuni de navigare
return new RedirectCommand(router.parseUrl(`/login?returnUrl=${state.url}`), { replaceUrl: true });
};Zonă publică. Oricine vede pagina asta.
canDeactivate: modificări nesalvate
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) =>
!component.hasUnsavedChanges() || confirm('Ai modificări nesalvate. Pleci oricum?');
@Component({ /* ... */ })
export class EditProfilePage implements HasUnsavedChanges {
readonly form = form(this.model);
hasUnsavedChanges() {
return this.form().dirty();
}
}Resolvers
export const reportResolver: ResolveFn<Report> = (route) => {
const api = inject(ReportsApi);
return api.get(Number(route.paramMap.get('id'))); // Observable / Promise / valoare
};
@Component({ /* ... */ })
export default class ReportPage {
readonly report = input.required<Report>(); // vine din resolve, gata încărcat
}Resolver httpResource în componentă
───────────────────────────────────────────────────────────────────────────
pagina apare cu datele gata pagina apare imediat, cu loading
navigarea „stă” până vin datele navigarea e instant
eroare = navigare anulată eroare = stare în UI (error())
bun pentru: date obligatorii, mici bun pentru: liste, dashboard-uri, UX rapidcanMatch vs canActivate
// canMatch vs canActivate
// canMatch: decide dacă ruta „există” pentru utilizator. Dacă nu → Angular încearcă următoarea rută
// cu același path, iar chunk-ul lazy NU e descărcat.
// canActivate: ruta există, dar intrarea e blocată (chunk-ul poate fi deja descărcat).
const routes: Routes = [
{ path: 'dashboard', canMatch: [isAdmin], loadComponent: () => import('./admin-dashboard') },
{ path: 'dashboard', loadComponent: () => import('./user-dashboard') }, // fallback pentru restul
];
// v22: currentSnapshot e acum parametru obligatoriu în CanMatchFn / CanMatch.canMatch (route, segments, currentSnapshot)
// v22.2: poți ARUNCA un RedirectCommand din guards/resolvers pentru redirectÎn ce ordine rulează
La o navigare, router-ul trece prin pași ficși. Dacă un pas spune „nu”, următorii nu mai rulează:
canMatch, în timp ce caută ruta. Un „nu” aici înseamnă că ruta nu există pentru utilizator, iar router-ul încearcă următoarea.canDeactivatepe rutele de pe care pleci.canActivateChildșicanActivate, de la părinte spre copil.- Resolver-ele. Abia după ce toate s-au terminat apare componenta.
Guard asincron
Un guard poate întoarce un Promise sau un Observable, iar navigarea așteaptă. Cazul tipic: utilizatorul nu e încă încărcat când aplicația pornește. Așteaptă prima valoare reală, nu starea goală de la început.
export const authGuard: CanActivateFn = (_route, state) => {
const auth = inject(AuthStore);
const router = inject(Router);
// așteaptă până nu mai e „loading”, apoi decide o singură dată
return toObservable(auth.status).pipe(
filter((s) => s !== 'loading'),
take(1),
map((s) =>
s === 'signed-in'
? true
: router.createUrlTree(['/login'], { queryParams: { next: state.url } }),
),
);
};Datele din resolver ca input
Cu withComponentInputBinding(), cheile din resolve ajung direct în input-urile componentei, la fel ca parametrii din URL. Componenta nu mai are nevoie de ActivatedRoute.
// app.config.ts
provideRouter(routes, withComponentInputBinding());
// routes.ts
{ path: 'reports/:id', component: ReportPage, resolve: { report: reportResolver } }
// report-page.ts — fără ActivatedRoute
export class ReportPage {
readonly id = input.required<string>(); // din URL
readonly report = input.required<Report>(); // din resolve
}Când DA, când NU
canMatchpe feature-uri restricționate (admin) — nu descarci codul degeaba.canActivatepentru autentificare / permisiuni pe pagini.canDeactivatepentru formulare cu modificări nesalvate.- Resolver pentru date mici, obligatorii, fără de care pagina nu are sens.
- Guards ca singură securitate — serverul trebuie să verifice oricum.
- Resolvere pentru liste mari → pagina pare „înghețată”. Folosește
httpResource+ skeleton. - Logică de business în guard (calcule, transformări) → mut-o într-un serviciu.
Capcane frecvente
- Guard care întoarce
falsefără redirect → utilizatorul rămâne pe loc fără să înțeleagă de ce. Întoarce unUrlTree/RedirectCommand. - Guard pe părinte, dar navigări între copii:
canActivatepe părinte nu re-rulează la schimbarea copilului — foloseștecanActivateChildsaurunGuardsAndResolvers. - Resolver care eșuează anulează navigarea — tratează eroarea (redirect la o pagină de eroare).
Fișă rapidă
// ordinea: canMatch → canDeactivate → canActivateChild / canActivate → resolve
canMatch: [() => inject(AuthStore).isAdmin()]
canActivate: [authGuard]
canActivateChild: [authGuard]
canDeactivate: [(c: HasUnsavedChanges) => !c.hasUnsavedChanges() || confirm('Pleci fără să salvezi?')]
resolve: { report: reportResolver }
// ce poate întoarce un guard
return true | false | router.createUrlTree(['/login']) | new RedirectCommand(tree)
return Promise<...> | Observable<...> // navigarea așteaptă
// re-rulează guard-urile și la schimbarea query params
runGuardsAndResolvers: 'paramsOrQueryParamsChange'