Docs
/
Angular
Chapter 6
06 — Routing & Navigation
Setting Up Routes
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'login', loadComponent: () => import('./features/auth/login.component').then(m => m.LoginComponent) },
{ path: 'dashboard', loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent) },
// Route with params
{ path: 'users/:id', loadComponent: () => import('./features/users/user-detail.component').then(m => m.UserDetailComponent) },
// Lazy-loaded child routes
{
path: 'admin',
loadChildren: () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
canActivate: [authGuard],
},
// Wildcard — 404
{ path: '**', loadComponent: () => import('./features/not-found.component').then(m => m.NotFoundComponent) },
];
// app.config.ts
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(), // Auto-bind route params to @Input()
withViewTransitions(), // Smooth page transitions
),
],
};
Navigation
Template
<nav>
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
<a [routerLink]="['/users', user.id]" routerLinkActive="active">Profile</a>
<a routerLink="/admin" [routerLinkActiveOptions]="{ exact: true }">Admin</a>
</nav>
<router-outlet /> <!-- Renders matched component here -->
Programmatic
private router = inject(Router);
// Simple navigation
this.router.navigate(['/dashboard']);
// With params
this.router.navigate(['/users', userId]);
// With query params
this.router.navigate(['/products'], {
queryParams: { page: 2, sort: 'price' },
});
// Relative navigation
this.router.navigate(['edit'], { relativeTo: this.route });
// Replace history (no back button)
this.router.navigate(['/login'], { replaceUrl: true });
Reading Route Params
With @Input() (Angular 16+ — Recommended)
Enable withComponentInputBinding() in router config.
@Component({ ... })
export class UserDetailComponent {
@Input() id!: string; // Matches route param ':id'
@Input() search?: string; // Matches query param 'search'
private userService = inject(UserService);
user$ = computed(() => this.userService.getById(this.id));
}
With ActivatedRoute
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
ngOnInit() {
// Snapshot (one-time read)
const id = this.route.snapshot.paramMap.get('id')!;
// Observable (reacts to param changes — same component reused)
this.route.paramMap
.pipe(map(params => params.get('id')!))
.subscribe(id => this.loadUser(id));
// Query params
this.route.queryParamMap
.pipe(map(params => params.get('search') ?? ''))
.subscribe(search => this.search = search);
// Route data
const resolvedUser = this.route.snapshot.data['user'];
}
}
Nested (Child) Routes
// admin.routes.ts
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () => import('./admin-layout.component').then(m => m.AdminLayoutComponent),
children: [
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
{ path: 'overview', loadComponent: () => import('./overview.component').then(m => m.OverviewComponent) },
{ path: 'users', loadComponent: () => import('./user-mgmt.component').then(m => m.UserMgmtComponent) },
{ path: 'settings', loadComponent: () => import('./settings.component').then(m => m.SettingsComponent) },
],
},
];
<!-- admin-layout.component.html -->
<div class="admin-layout">
<aside>
<a routerLink="overview" routerLinkActive="active">Overview</a>
<a routerLink="users" routerLinkActive="active">Users</a>
<a routerLink="settings" routerLinkActive="active">Settings</a>
</aside>
<main>
<router-outlet /> <!-- Child routes render here -->
</main>
</div>
Route Guards (Functional — Angular 15+)
canActivate — Protect Routes
import { CanActivateFn, Router } from '@angular/router';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};
export const roleGuard: CanActivateFn = (route) => {
const authService = inject(AuthService);
const requiredRoles = route.data['roles'] as string[];
return requiredRoles.some(role => authService.hasRole(role));
};
// Usage
{
path: 'admin',
canActivate: [authGuard, roleGuard],
data: { roles: ['admin', 'superadmin'] },
loadChildren: () => import('./admin.routes').then(m => m.ADMIN_ROUTES),
}
canDeactivate — Unsaved Changes Warning
export const unsavedChangesGuard: CanDeactivateFn<{ hasUnsavedChanges: () => boolean }> = (component) => {
if (component.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Leave anyway?');
}
return true;
};
canMatch — Conditionally Load Routes
export const featureFlagGuard: CanMatchFn = () => {
const features = inject(FeatureFlagService);
return features.isEnabled('new-dashboard');
};
Route Resolvers
Fetch data before navigating to the route.
import { ResolveFn } from '@angular/router';
export const userResolver: ResolveFn<User> = (route) => {
const userService = inject(UserService);
const id = route.paramMap.get('id')!;
return userService.getById(id); // Observable or Promise
};
// Route config
{
path: 'users/:id',
loadComponent: () => import('./user-detail.component'),
resolve: { user: userResolver },
}
// Access in component
@Input() user!: User; // Auto-bound from resolver via withComponentInputBinding()
// OR
const user = this.route.snapshot.data['user'];
Lazy Loading Strategies
// 1. Lazy-load a single component
{ path: 'about', loadComponent: () => import('./about.component').then(m => m.AboutComponent) }
// 2. Lazy-load a group of routes
{ path: 'admin', loadChildren: () => import('./admin.routes').then(m => m.ADMIN_ROUTES) }
// 3. Preloading strategies
provideRouter(
routes,
withPreloading(PreloadAllModules), // Preload all lazy routes after initial load
)
Named Outlets (Advanced)
// Route config
{ path: 'chat', component: ChatComponent, outlet: 'sidebar' }
<router-outlet /> <!-- Primary -->
<router-outlet name="sidebar" /> <!-- Named -->
<a [routerLink]="[{ outlets: { sidebar: 'chat' } }]">Open Chat</a>
Key Takeaways
- Use
loadComponentandloadChildrenfor lazy loading — reduces initial bundle size - Enable
withComponentInputBinding()to auto-bind route/query params to@Input() - Functional guards (
CanActivateFn) are simpler than class-based guards - Use resolvers to pre-fetch data before route activation
- Always add a
**wildcard route for 404 handling - Use
withViewTransitions()for smooth page transitions (View Transitions API) - Nested routes with
<router-outlet>enable layout inheritance (sidebar + content)