8.9 KiB
8.9 KiB
Angular Routing
Routes Configuration
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
component: HomeComponent,
title: 'Home'
},
{
path: 'users',
loadComponent: () => import('./users/users.component').then(m => m.UsersComponent),
title: 'Users'
},
{
path: 'users/:id',
loadComponent: () => import('./users/user-detail.component').then(m => m.UserDetailComponent),
canActivate: [authGuard],
resolve: { user: userResolver }
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
canActivate: [authGuard, adminGuard]
},
{
path: '**',
loadComponent: () => import('./not-found/not-found.component').then(m => m.NotFoundComponent),
title: '404 Not Found'
}
];
// app.config.ts
import { provideRouter, withComponentInputBinding } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(), // Bind route params to @Input()
withViewTransitions(), // Enable view transitions
withPreloading(PreloadAllModules)
)
]
};
Lazy Loading
// Feature routes
// admin/admin.routes.ts
import { Routes } from '@angular/router';
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () => import('./admin-dashboard.component').then(m => m.AdminDashboardComponent)
},
{
path: 'users',
loadComponent: () => import('./admin-users.component').then(m => m.AdminUsersComponent)
},
{
path: 'settings',
loadComponent: () => import('./admin-settings.component').then(m => m.AdminSettingsComponent)
}
];
Functional Guards
// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
// Redirect to login with return URL
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// Admin guard
export const adminGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.hasRole('admin')) {
return true;
}
return router.createUrlTree(['/unauthorized']);
};
// Can deactivate (unsaved changes)
export const canDeactivateGuard: CanDeactivateFn<FormComponent> = (component) => {
if (component.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Are you sure you want to leave?');
}
return true;
};
Resolvers
// resolvers/user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { catchError, of } from 'rxjs';
import { User } from '../models/user.model';
import { UsersService } from '../services/users.service';
export const userResolver: ResolveFn<User | null> = (route, state) => {
const usersService = inject(UsersService);
const id = route.paramMap.get('id')!;
return usersService.getById(id).pipe(
catchError(() => of(null))
);
};
// Component receives resolved data
@Component({
selector: 'app-user-detail',
standalone: true,
template: `
@if (user) {
<h1>{{ user.name }}</h1>
} @else {
<p>User not found</p>
}
`
})
export class UserDetailComponent {
user = input<User | null>(null); // Resolved data bound as input
}
Route Parameters
import { Component, inject, input } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-product-detail',
standalone: true
})
export class ProductDetailComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
// Modern approach: route params as inputs
id = input.required<string>();
// Legacy approach: subscribe to params
ngOnInit() {
this.route.paramMap.subscribe(params => {
const id = params.get('id');
this.loadProduct(id);
});
// Query params
this.route.queryParamMap.subscribe(params => {
const filter = params.get('filter');
const sort = params.get('sort');
});
}
// Navigate programmatically
goToEdit() {
this.router.navigate(['/products', this.id(), 'edit']);
}
// Navigate with query params
applyFilter(filter: string) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { filter },
queryParamsHandling: 'merge' // Preserve other params
});
}
}
Router Events
import { Component, inject } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationError } from '@angular/router';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-root',
standalone: true
})
export class AppComponent {
private router = inject(Router);
loading = signal(false);
constructor() {
// Show loading on navigation start
this.router.events.pipe(
filter(event => event instanceof NavigationStart)
).subscribe(() => {
this.loading.set(true);
});
// Hide loading on navigation end
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(() => {
this.loading.set(false);
});
// Handle navigation errors
this.router.events.pipe(
filter(event => event instanceof NavigationError)
).subscribe((event: NavigationError) => {
console.error('Navigation error:', event.error);
this.loading.set(false);
});
}
}
Child Routes & Outlets
// Parent route with child routes
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
children: [
{
path: 'stats',
component: StatsComponent,
outlet: 'panel' // Named outlet
},
{
path: 'charts',
component: ChartsComponent,
outlet: 'panel'
}
]
}
];
// Dashboard component template
@Component({
template: `
<div class="dashboard">
<div class="main">
<router-outlet></router-outlet> <!-- Primary outlet -->
</div>
<div class="panel">
<router-outlet name="panel"></router-outlet> <!-- Named outlet -->
</div>
</div>
`
})
export class DashboardComponent {}
// Navigate to named outlet
this.router.navigate(['/dashboard', { outlets: { panel: ['stats'] } }]);
Preloading Strategies
// Custom preloading strategy
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of, timer } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Only preload routes with data.preload = true
if (route.data?.['preload']) {
const delay = route.data?.['preloadDelay'] || 0;
return timer(delay).pipe(
mergeMap(() => load())
);
}
return of(null);
}
}
// Route config with preload data
const routes: Routes = [
{
path: 'important',
loadChildren: () => import('./important/important.routes'),
data: { preload: true, preloadDelay: 2000 }
}
];
// Register in app config
provideRouter(routes, withPreloading(CustomPreloadingStrategy))
Route Guards with Observables
export const dataGuard: CanActivateFn = (route, state) => {
const dataService = inject(DataService);
const router = inject(Router);
return dataService.checkAccess(route.params['id']).pipe(
map(hasAccess => {
if (hasAccess) {
return true;
}
return router.createUrlTree(['/no-access']);
}),
catchError(() => {
return of(router.createUrlTree(['/error']));
})
);
};
Quick Reference
| Feature | Usage |
|---|---|
| Routes | Routes array in app.routes.ts |
| Lazy load | loadComponent(), loadChildren() |
| Guards | CanActivateFn, CanDeactivateFn |
| Resolvers | ResolveFn<T> |
| Params | route.paramMap, input<T>() |
| Query | route.queryParamMap |
| Navigate | router.navigate(), routerLink |
| Events | router.events |
| Outlets | <router-outlet name="..."> |
| Preload | withPreloading() |