Module Overview
Estimated Time: 3-4 hours | Difficulty: Intermediate | Prerequisites: Module 5
- Setting up routing configuration
- Router outlet and navigation
- Route parameters and query params
- Lazy loading for performance
- Route guards and resolvers
- Nested routes and layouts
Router Architecture
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ Angular Router Architecture │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ URL: /products/123/details?sort=price#reviews │
│ ├───────┬───┬───────┬─────────────┬──────── │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ path param child query fragment │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Router │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│ │
│ │ │ Guards │──│ Resolver │──│ Outlet │──│ Component ││ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘│ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ Can navigate? Fetch data Render here │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Lifecycle: URL Change → Match Route → Run Guards → Resolve Data │
│ → Activate Route → Render Component │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Basic Routing Setup
Configure Routes
Copy
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'products', component: ProductsComponent },
{ path: 'products/:id', component: ProductDetailComponent },
{ path: '**', component: NotFoundComponent } // Wildcard for 404
];
Copy
// 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)
]
};
Router Outlet
Copy
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<nav>
<a routerLink="/home" routerLinkActive="active">Home</a>
<a routerLink="/about" routerLinkActive="active">About</a>
<a routerLink="/products" routerLinkActive="active">Products</a>
</nav>
<!-- Route content renders here -->
<main>
<router-outlet />
</main>
<footer>© 2024 My App</footer>
`,
styles: [`
.active {
font-weight: bold;
color: #DD0031;
}
`]
})
export class AppComponent {}
Route Parameters
Path Parameters
Copy
// products/:id route
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-detail',
standalone: true,
template: `
<h1>Product {{ productId }}</h1>
<!-- Product details -->
`
})
export class ProductDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
productId!: string;
ngOnInit() {
// Snapshot (for non-changing params)
this.productId = this.route.snapshot.params['id'];
// Observable (for params that might change)
this.route.params.subscribe(params => {
this.productId = params['id'];
this.loadProduct();
});
}
}
Modern: Input Binding for Route Parameters
Copy
// app.config.ts
import { provideRouter, withComponentInputBinding } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()) // Enable input binding
]
};
Copy
// product-detail.component.ts
@Component({
selector: 'app-product-detail',
standalone: true,
template: `<h1>Product {{ id }}</h1>`
})
export class ProductDetailComponent {
// Route param automatically bound as input!
id = input.required<string>();
// Also works with query params
sort = input<string>(); // ?sort=price
}
Query Parameters
Copy
@Component({
selector: 'app-products',
standalone: true,
imports: [RouterLink],
template: `
<!-- Navigate with query params -->
<a [routerLink]="['/products']" [queryParams]="{ category: 'electronics', sort: 'price' }">
Electronics
</a>
<button (click)="applyFilters()">Apply Filters</button>
`
})
export class ProductsComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
ngOnInit() {
// Read query params
this.route.queryParams.subscribe(params => {
console.log(params['category'], params['sort']);
});
}
applyFilters() {
// Navigate with query params programmatically
this.router.navigate(['/products'], {
queryParams: { category: 'books', sort: 'rating' },
queryParamsHandling: 'merge' // Preserve existing params
});
}
}
Programmatic Navigation
Copy
import { Router } from '@angular/router';
@Component({...})
export class ProductComponent {
private router = inject(Router);
// Simple navigation
goHome() {
this.router.navigate(['/home']);
}
// With route params
viewProduct(id: number) {
this.router.navigate(['/products', id]);
}
// With query params
searchProducts(term: string) {
this.router.navigate(['/products'], {
queryParams: { search: term }
});
}
// Relative navigation
goToDetails() {
this.router.navigate(['details'], { relativeTo: this.route });
}
// Replace current URL (no back button entry)
login() {
this.router.navigate(['/dashboard'], { replaceUrl: true });
}
// Navigate with state
editProduct(product: Product) {
this.router.navigate(['/products', product.id, 'edit'], {
state: { product } // Pass data without URL
});
}
}
Lazy Loading
Load feature modules only when needed:Copy
// app.routes.ts
export const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
// Lazy load entire feature
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES)
},
// Lazy load single component
{
path: 'settings',
loadComponent: () => import('./settings/settings.component')
.then(m => m.SettingsComponent)
}
];
Copy
// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
{ path: '', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent },
{ path: 'settings', component: AdminSettingsComponent }
];
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ Lazy Loading Benefits │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ WITHOUT Lazy Loading WITH Lazy Loading │
│ ───────────────────── ────────────────── │
│ │
│ Initial Bundle: 2MB Initial Bundle: 500KB │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ App + Admin + │ │ App │ │
│ │ Products + │ │ (core only) │ │
│ │ Settings │ └──────────────────┘ │
│ └─────────────────┘ + │
│ ┌─────────┐ ┌─────────┐ │
│ Load time: 3-5 seconds │ Admin │ │Products │ │
│ │ (on nav)│ │(on nav) │ │
│ └─────────┘ └─────────┘ │
│ │
│ Initial load: 1-2 seconds │
│ Feature loads: ~200ms each │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Route Guards
Protect routes with functional guards:canActivate - Protect Route Access
Copy
// 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 }
});
};
Copy
// app.routes.ts
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard]
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
canActivate: [authGuard, adminGuard] // Multiple guards
}
];
canDeactivate - Confirm Before Leaving
Copy
// guards/unsaved-changes.guard.ts
export interface CanDeactivateComponent {
canDeactivate: () => boolean | Observable<boolean>;
}
export const unsavedChangesGuard: CanDeactivateFn<CanDeactivateComponent> =
(component, currentRoute, currentState, nextState) => {
if (component.canDeactivate && !component.canDeactivate()) {
return confirm('You have unsaved changes. Leave anyway?');
}
return true;
};
Copy
// edit-profile.component.ts
@Component({...})
export class EditProfileComponent implements CanDeactivateComponent {
hasUnsavedChanges = signal(false);
canDeactivate(): boolean {
return !this.hasUnsavedChanges();
}
}
canMatch - Control Route Matching
Copy
// Role-based route matching
export const roleGuard = (requiredRole: string): CanMatchFn => {
return (route, segments) => {
const authService = inject(AuthService);
return authService.hasRole(requiredRole);
};
};
// Different components based on role
export const routes: Routes = [
{
path: 'dashboard',
component: AdminDashboardComponent,
canMatch: [roleGuard('admin')]
},
{
path: 'dashboard',
component: UserDashboardComponent,
canMatch: [roleGuard('user')]
}
];
Route Resolvers
Pre-fetch data before activating route:Copy
// resolvers/product.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product.model';
export const productResolver: ResolveFn<Product> = (route, state) => {
const productService = inject(ProductService);
const productId = route.params['id'];
return productService.getProduct(productId);
};
Copy
// app.routes.ts
export const routes: Routes = [
{
path: 'products/:id',
component: ProductDetailComponent,
resolve: {
product: productResolver
}
}
];
Copy
// product-detail.component.ts
@Component({
selector: 'app-product-detail',
template: `
<h1>{{ product().name }}</h1>
<p>{{ product().description }}</p>
`
})
export class ProductDetailComponent {
private route = inject(ActivatedRoute);
// Data is already loaded!
product = toSignal(
this.route.data.pipe(map(data => data['product'])),
{ requireSync: true }
);
}
Nested Routes & Layouts
Copy
// app.routes.ts
export const routes: Routes = [
{
path: 'admin',
component: AdminLayoutComponent, // Layout with its own router-outlet
canActivate: [authGuard, adminGuard],
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent },
{
path: 'users/:id',
component: AdminUserDetailComponent,
children: [
{ path: 'profile', component: UserProfileComponent },
{ path: 'permissions', component: UserPermissionsComponent }
]
},
{ path: 'settings', component: AdminSettingsComponent }
]
}
];
Copy
// admin-layout.component.ts
@Component({
selector: 'app-admin-layout',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="admin-layout">
<aside class="sidebar">
<nav>
<a routerLink="dashboard" routerLinkActive="active">Dashboard</a>
<a routerLink="users" routerLinkActive="active">Users</a>
<a routerLink="settings" routerLinkActive="active">Settings</a>
</nav>
</aside>
<main class="content">
<!-- Nested routes render here -->
<router-outlet />
</main>
</div>
`
})
export class AdminLayoutComponent {}
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ URL: /admin/users/123/permissions │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ AppComponent (router-outlet) │ │
│ │ ┌───────────────────────────────────────────────────────────────┐│ │
│ │ │ AdminLayoutComponent (router-outlet) ││ │
│ │ │ ┌───────────────────────────────────────────────────────────┐││ │
│ │ │ │ AdminUserDetailComponent (router-outlet) │││ │
│ │ │ │ ┌───────────────────────────────────────────────────────┐│││ │
│ │ │ │ │ UserPermissionsComponent ││││ │
│ │ │ │ └───────────────────────────────────────────────────────┘│││ │
│ │ │ └───────────────────────────────────────────────────────────┘││ │
│ │ └───────────────────────────────────────────────────────────────┘│ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Router Events
Monitor navigation lifecycle:Copy
import { Router, NavigationStart, NavigationEnd, NavigationError } from '@angular/router';
@Component({...})
export class AppComponent {
private router = inject(Router);
isLoading = signal(false);
constructor() {
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
this.isLoading.set(true);
}
if (event instanceof NavigationEnd) {
this.isLoading.set(false);
// Analytics tracking
this.analytics.trackPageView(event.urlAfterRedirects);
}
if (event instanceof NavigationError) {
this.isLoading.set(false);
console.error('Navigation error:', event.error);
}
});
}
}
Router Configuration Options
Copy
// app.config.ts
import {
provideRouter,
withComponentInputBinding,
withViewTransitions,
withPreloading,
PreloadAllModules
} from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(), // Bind route params to inputs
withViewTransitions(), // Animate route transitions
withPreloading(PreloadAllModules), // Preload lazy modules
withRouterConfig({
onSameUrlNavigation: 'reload', // Reload on same URL navigation
paramsInheritanceStrategy: 'always' // Inherit params from parent
})
)
]
};
Practice Exercise
Exercise: Build a Product Catalog with Routing
Create a multi-page app with:
- Product list page with category filters via query params
- Product detail page with tabs (info, reviews, specs)
- Auth guard protecting checkout route
- Lazy loaded admin section
- 404 page for unknown routes
Solution
Solution
Copy
// app.routes.ts
export const routes: Routes = [
{ path: '', redirectTo: 'products', pathMatch: 'full' },
{ path: 'products', component: ProductListComponent },
{
path: 'products/:id',
component: ProductDetailComponent,
children: [
{ path: '', redirectTo: 'info', pathMatch: 'full' },
{ path: 'info', component: ProductInfoComponent },
{ path: 'reviews', component: ProductReviewsComponent },
{ path: 'specs', component: ProductSpecsComponent }
]
},
{
path: 'checkout',
loadComponent: () => import('./checkout/checkout.component')
.then(m => m.CheckoutComponent),
canActivate: [authGuard]
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES),
canActivate: [authGuard, adminGuard]
},
{ path: '**', component: NotFoundComponent }
];
// product-list.component.ts
@Component({
selector: 'app-product-list',
standalone: true,
imports: [RouterLink],
template: `
<div class="filters">
@for (category of categories; track category) {
<button
(click)="filterByCategory(category)"
[class.active]="selectedCategory() === category"
>
{{ category }}
</button>
}
</div>
<div class="products">
@for (product of filteredProducts(); track product.id) {
<a [routerLink]="['/products', product.id]" class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency }}</p>
</a>
}
</div>
`
})
export class ProductListComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private productService = inject(ProductService);
categories = ['All', 'Electronics', 'Clothing', 'Books'];
products = toSignal(this.productService.getProducts(), { initialValue: [] });
selectedCategory = toSignal(
this.route.queryParams.pipe(map(p => p['category'] || 'All')),
{ initialValue: 'All' }
);
filteredProducts = computed(() => {
const category = this.selectedCategory();
if (category === 'All') return this.products();
return this.products().filter(p => p.category === category);
});
filterByCategory(category: string) {
this.router.navigate([], {
queryParams: { category: category === 'All' ? null : category },
queryParamsHandling: 'merge'
});
}
}
// product-detail.component.ts
@Component({
selector: 'app-product-detail',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="product-detail">
<h1>{{ product()?.name }}</h1>
<nav class="tabs">
<a routerLink="info" routerLinkActive="active">Info</a>
<a routerLink="reviews" routerLinkActive="active">Reviews</a>
<a routerLink="specs" routerLinkActive="active">Specs</a>
</nav>
<div class="tab-content">
<router-outlet />
</div>
</div>
`
})
export class ProductDetailComponent {
id = input.required<string>();
private productService = inject(ProductService);
product = computed(() =>
this.productService.getProductById(+this.id())
);
}
Summary
1
Route Configuration
Define routes with path, component, and optional guards/resolvers
2
Navigation
Use RouterLink in templates or Router.navigate() programmatically
3
Lazy Loading
Use loadChildren/loadComponent for better initial load performance
4
Guards
Protect routes with canActivate, canDeactivate, canMatch
5
Resolvers
Pre-fetch data before route activation
Next Steps
Next: Angular Forms
Master template-driven and reactive forms for user input