401 lines
8.6 KiB
Markdown
401 lines
8.6 KiB
Markdown
---
|
|
name: angular-routing
|
|
description: Implement routing in Angular v20+ applications with lazy loading, functional guards, resolvers, and route parameters. Use for navigation setup, protected routes, route-based data loading, and nested routing. Triggers on route configuration, adding authentication guards, implementing lazy loading, or reading route parameters with signals.
|
|
---
|
|
|
|
# Angular Routing
|
|
|
|
Configure routing in Angular v20+ with lazy loading, functional guards, and signal-based route parameters.
|
|
|
|
## Basic Setup
|
|
|
|
```typescript
|
|
// app.routes.ts
|
|
import { Routes } from '@angular/router';
|
|
|
|
export const routes: Routes = [
|
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
|
{ path: 'home', component: Home },
|
|
{ path: 'about', component: About },
|
|
{ path: '**', component: NotFound },
|
|
];
|
|
|
|
// app.config.ts
|
|
import { ApplicationConfig } from '@angular/core';
|
|
import { provideRouter } from '@angular/router';
|
|
import { routes } from './app.routes';
|
|
|
|
export const appConfig: ApplicationConfig = {
|
|
providers: [provideRouter(routes)],
|
|
};
|
|
|
|
// app.component.ts
|
|
import { Component } from '@angular/core';
|
|
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
|
|
|
|
@Component({
|
|
selector: 'app-root',
|
|
imports: [RouterOutlet, RouterLink, RouterLinkActive],
|
|
template: `
|
|
<nav>
|
|
<a
|
|
routerLink="/home"
|
|
routerLinkActive="active"
|
|
>Home</a
|
|
>
|
|
<a
|
|
routerLink="/about"
|
|
routerLinkActive="active"
|
|
>About</a
|
|
>
|
|
</nav>
|
|
<router-outlet />
|
|
`,
|
|
})
|
|
export class App {}
|
|
```
|
|
|
|
## Lazy Loading
|
|
|
|
Load feature modules on demand:
|
|
|
|
```typescript
|
|
// app.routes.ts
|
|
export const routes: Routes = [
|
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
|
{ path: 'home', component: Home },
|
|
|
|
// Lazy load entire feature
|
|
{
|
|
path: 'admin',
|
|
loadChildren: () => import('./admin/admin.routes').then((m) => m.adminRoutes),
|
|
},
|
|
|
|
// Lazy load single component
|
|
{
|
|
path: 'settings',
|
|
loadComponent: () => import('./settings/settings.component').then((m) => m.Settings),
|
|
},
|
|
];
|
|
|
|
// admin/admin.routes.ts
|
|
export const adminRoutes: Routes = [
|
|
{ path: '', component: AdminDashboard },
|
|
{ path: 'users', component: AdminUsers },
|
|
{ path: 'settings', component: AdminSettings },
|
|
];
|
|
```
|
|
|
|
## Route Parameters
|
|
|
|
### With Signal Inputs (Recommended)
|
|
|
|
```typescript
|
|
// Route config
|
|
{ path: 'users/:id', component: UserDetail }
|
|
|
|
// Component - use input() for route params
|
|
import { Component, input, computed } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'app-user-detail',
|
|
template: `
|
|
<h1>User {{ id() }}</h1>
|
|
`,
|
|
})
|
|
export class UserDetail {
|
|
// Route param as signal input
|
|
id = input.required<string>();
|
|
|
|
// Computed based on route param
|
|
userId = computed(() => parseInt(this.id(), 10));
|
|
}
|
|
```
|
|
|
|
Enable with `withComponentInputBinding()`:
|
|
|
|
```typescript
|
|
// app.config.ts
|
|
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
|
|
|
export const appConfig: ApplicationConfig = {
|
|
providers: [provideRouter(routes, withComponentInputBinding())],
|
|
};
|
|
```
|
|
|
|
### Query Parameters
|
|
|
|
```typescript
|
|
// Route: /search?q=angular&page=1
|
|
|
|
@Component({...})
|
|
export class Search {
|
|
// Query params as inputs
|
|
q = input<string>('');
|
|
page = input<string>('1');
|
|
|
|
currentPage = computed(() => parseInt(this.page(), 10));
|
|
}
|
|
```
|
|
|
|
### With ActivatedRoute (Alternative)
|
|
|
|
```typescript
|
|
import { Component, inject } from '@angular/core';
|
|
import { ActivatedRoute } from '@angular/router';
|
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
import { map } from 'rxjs';
|
|
|
|
@Component({...})
|
|
export class UserDetail {
|
|
private route = inject(ActivatedRoute);
|
|
|
|
// Convert route params to signal
|
|
id = toSignal(
|
|
this.route.paramMap.pipe(map(params => params.get('id'))),
|
|
{ initialValue: null }
|
|
);
|
|
|
|
// Query params
|
|
query = toSignal(
|
|
this.route.queryParamMap.pipe(map(params => params.get('q'))),
|
|
{ initialValue: '' }
|
|
);
|
|
}
|
|
```
|
|
|
|
## Functional Guards
|
|
|
|
### Auth Guard
|
|
|
|
```typescript
|
|
// guards/auth.guard.ts
|
|
import { inject } from '@angular/core';
|
|
import { CanActivateFn, Router } from '@angular/router';
|
|
|
|
export const authGuard: CanActivateFn = (route, state) => {
|
|
const authService = inject(Auth);
|
|
const router = inject(Router);
|
|
|
|
if (authService.isAuthenticated()) {
|
|
return true;
|
|
}
|
|
|
|
// Redirect to login with return URL
|
|
return router.createUrlTree(['/login'], {
|
|
queryParams: { returnUrl: state.url },
|
|
});
|
|
};
|
|
|
|
// Usage in routes
|
|
{
|
|
path: 'dashboard',
|
|
component: Dashboard,
|
|
canActivate: [authGuard],
|
|
}
|
|
```
|
|
|
|
### Role Guard
|
|
|
|
```typescript
|
|
export const roleGuard = (allowedRoles: string[]): CanActivateFn => {
|
|
return (route, state) => {
|
|
const authService = inject(Auth);
|
|
const router = inject(Router);
|
|
|
|
const userRole = authService.currentUser()?.role;
|
|
|
|
if (userRole && allowedRoles.includes(userRole)) {
|
|
return true;
|
|
}
|
|
|
|
return router.createUrlTree(['/unauthorized']);
|
|
};
|
|
};
|
|
|
|
// Usage
|
|
{
|
|
path: 'admin',
|
|
component: Admin,
|
|
canActivate: [authGuard, roleGuard(['admin', 'superadmin'])],
|
|
}
|
|
```
|
|
|
|
### Can Deactivate Guard
|
|
|
|
```typescript
|
|
export interface CanDeactivate {
|
|
canDeactivate: () => boolean | Promise<boolean>;
|
|
}
|
|
|
|
export const unsavedChangesGuard: CanDeactivateFn<CanDeactivate> = (component) => {
|
|
if (component.canDeactivate()) {
|
|
return true;
|
|
}
|
|
|
|
return confirm('You have unsaved changes. Leave anyway?');
|
|
};
|
|
|
|
// Component implementation
|
|
@Component({...})
|
|
export class Edit implements CanDeactivate {
|
|
form = inject(FormBuilder).group({...});
|
|
|
|
canDeactivate(): boolean {
|
|
return !this.form.dirty;
|
|
}
|
|
}
|
|
|
|
// Route
|
|
{
|
|
path: 'edit/:id',
|
|
component: Edit,
|
|
canDeactivate: [unsavedChangesGuard],
|
|
}
|
|
```
|
|
|
|
## Resolvers
|
|
|
|
Pre-fetch data before route activation:
|
|
|
|
```typescript
|
|
// resolvers/user.resolver.ts
|
|
import { inject } from '@angular/core';
|
|
import { ResolveFn } from '@angular/router';
|
|
|
|
export const userResolver: ResolveFn<User> = (route) => {
|
|
const userService = inject(User);
|
|
const id = route.paramMap.get('id')!;
|
|
return userService.getById(id);
|
|
};
|
|
|
|
// Route config
|
|
{
|
|
path: 'users/:id',
|
|
component: UserDetail,
|
|
resolve: { user: userResolver },
|
|
}
|
|
|
|
// Component - access resolved data via input
|
|
@Component({...})
|
|
export class UserDetail {
|
|
user = input.required<User>();
|
|
}
|
|
```
|
|
|
|
## Nested Routes
|
|
|
|
```typescript
|
|
// Parent route with children
|
|
export const routes: Routes = [
|
|
{
|
|
path: 'products',
|
|
component: ProductsLayout,
|
|
children: [
|
|
{ path: '', component: ProductList },
|
|
{ path: ':id', component: ProductDetail },
|
|
{ path: ':id/edit', component: ProductEdit },
|
|
],
|
|
},
|
|
];
|
|
|
|
// ProductsLayout
|
|
@Component({
|
|
imports: [RouterOutlet],
|
|
template: `
|
|
<h1>Products</h1>
|
|
<router-outlet />
|
|
<!-- Child routes render here -->
|
|
`,
|
|
})
|
|
export class ProductsLayout {}
|
|
```
|
|
|
|
## Programmatic Navigation
|
|
|
|
```typescript
|
|
import { Component, inject } from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
|
|
@Component({...})
|
|
export class Product {
|
|
private router = inject(Router);
|
|
|
|
// Navigate to route
|
|
goToProducts() {
|
|
this.router.navigate(['/products']);
|
|
}
|
|
|
|
// Navigate with params
|
|
goToProduct(id: string) {
|
|
this.router.navigate(['/products', id]);
|
|
}
|
|
|
|
// Navigate with query params
|
|
search(query: string) {
|
|
this.router.navigate(['/search'], {
|
|
queryParams: { q: query, page: 1 },
|
|
});
|
|
}
|
|
|
|
// Navigate relative to current route
|
|
goToEdit() {
|
|
this.router.navigate(['edit'], { relativeTo: this.route });
|
|
}
|
|
|
|
// Replace current history entry
|
|
replaceUrl() {
|
|
this.router.navigate(['/new-page'], { replaceUrl: true });
|
|
}
|
|
}
|
|
```
|
|
|
|
## Route Data
|
|
|
|
```typescript
|
|
// Static route data
|
|
{
|
|
path: 'admin',
|
|
component: Admin,
|
|
data: {
|
|
title: 'Admin Dashboard',
|
|
roles: ['admin'],
|
|
},
|
|
}
|
|
|
|
// Access in component
|
|
@Component({...})
|
|
export class AdminCmpt {
|
|
title = input<string>(); // From route data
|
|
roles = input<string[]>(); // From route data
|
|
}
|
|
|
|
// Or via ActivatedRoute
|
|
private route = inject(ActivatedRoute);
|
|
data = toSignal(this.route.data);
|
|
```
|
|
|
|
## Router Events
|
|
|
|
```typescript
|
|
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
|
|
import { filter } from 'rxjs';
|
|
|
|
@Component({...})
|
|
export class AppMain {
|
|
private router = inject(Router);
|
|
|
|
isNavigating = signal(false);
|
|
|
|
constructor() {
|
|
this.router.events.pipe(
|
|
filter(e => e instanceof NavigationStart || e instanceof NavigationEnd)
|
|
).subscribe(event => {
|
|
this.isNavigating.set(event instanceof NavigationStart);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
For advanced patterns, see [references/routing-patterns.md](references/routing-patterns.md).
|