Exercise 5: Routing and Navigation
Objective
Build a multi-page application with advanced routing features including lazy loading, guards, and route parameters.
Prerequisites
- Understanding of Angular routing
- Knowledge of route guards
- Familiarity with observables
Setup
ng new routing-app --standalone --routing --style=css
cd routing-app
ng serve
Part 1: Basic Routing Setup
Task 1.1: Create Feature Components
# Generate components
ng generate component pages/home --standalone
ng generate component pages/about --standalone
ng generate component pages/contact --standalone
ng generate component pages/not-found --standalone
Task 1.2: Configure Routes
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { AboutComponent } from './pages/about/about.component';
import { ContactComponent } from './pages/contact/contact.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent },
{ path: '**', component: NotFoundComponent },
];
Task 1.3: Create Navigation
// src/app/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 class="navbar">
<div class="nav-brand">
<a routerLink="/">MyApp</a>
</div>
<ul class="nav-links">
<li>
<a
routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
>
Home
</a>
</li>
<li>
<a routerLink="/about" routerLinkActive="active">About</a>
</li>
<li>
<a routerLink="/contact" routerLinkActive="active">Contact</a>
</li>
<li>
<a routerLink="/products" routerLinkActive="active">Products</a>
</li>
</ul>
</nav>
<main class="content">
<router-outlet></router-outlet>
</main>
`,
styles: [
`
.navbar {
background: #333;
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-brand a {
color: white;
text-decoration: none;
font-size: 1.5rem;
font-weight: bold;
}
.nav-links {
list-style: none;
display: flex;
gap: 1rem;
margin: 0;
padding: 0;
}
.nav-links a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background 0.3s;
}
.nav-links a:hover {
background: #555;
}
.nav-links a.active {
background: #007bff;
}
.content {
padding: 2rem;
}
`,
],
})
export class AppComponent {}
Part 2: Route Parameters
Task 2.1: Create Product List and Detail
ng generate component pages/products/product-list --standalone
ng generate component pages/products/product-detail --standalone
// src/app/models/product.model.ts
export interface Product {
id: number;
name: string;
description: string;
price: number;
category: string;
}
// src/app/services/product.service.ts
import { Injectable, signal } from '@angular/core';
import { Product } from '../models/product.model';
@Injectable({
providedIn: 'root',
})
export class ProductService {
private products = signal<Product[]>([
{
id: 1,
name: 'Laptop',
description: 'High-performance laptop',
price: 999,
category: 'Electronics',
},
{
id: 2,
name: 'Mouse',
description: 'Wireless mouse',
price: 29,
category: 'Electronics',
},
{
id: 3,
name: 'Keyboard',
description: 'Mechanical keyboard',
price: 79,
category: 'Electronics',
},
]);
readonly allProducts = this.products.asReadonly();
getProductById(id: number): Product | undefined {
return this.products().find(p => p.id === id);
}
}
// src/app/pages/products/product-list/product-list.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { ProductService } from '../../../services/product.service';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule, RouterLink],
template: `
<div class="product-list">
<h1>Products</h1>
<div class="products-grid">
@for (product of productService.allProducts(); track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">\${{ product.price }}</p>
<p>{{ product.description }}</p>
<a
[routerLink]="['/products', product.id]"
[queryParams]="{ category: product.category }"
class="btn"
>
View Details
</a>
</div>
}
</div>
</div>
`,
styles: [
`
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.product-card {
border: 1px solid #ddd;
padding: 1rem;
border-radius: 8px;
}
.price {
font-size: 1.5rem;
font-weight: bold;
color: #28a745;
}
.btn {
display: inline-block;
padding: 0.5rem 1rem;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
margin-top: 0.5rem;
}
`,
],
})
export class ProductListComponent {
productService = inject(ProductService);
}
// src/app/pages/products/product-detail/product-detail.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ProductService } from '../../../services/product.service';
import { Product } from '../../../models/product.model';
@Component({
selector: 'app-product-detail',
standalone: true,
imports: [CommonModule, RouterLink],
template: `
@if (product) {
<div class="product-detail">
<button (click)="goBack()" class="back-btn">← Back to Products</button>
<h1>{{ product.name }}</h1>
<p class="category">Category: {{ category }}</p>
<p class="price">\${{ product.price }}</p>
<p class="description">{{ product.description }}</p>
<div class="actions">
<button class="btn btn-primary">Add to Cart</button>
<button (click)="navigateToEdit()" class="btn btn-secondary">
Edit
</button>
</div>
</div>
} @else {
<div class="not-found">
<h2>Product not found</h2>
<a routerLink="/products" class="btn">Back to Products</a>
</div>
}
`,
styles: [
`
.product-detail {
max-width: 800px;
}
.back-btn {
background: none;
border: none;
color: #007bff;
cursor: pointer;
font-size: 1rem;
margin-bottom: 1rem;
}
.category {
color: #6c757d;
}
.price {
font-size: 2rem;
font-weight: bold;
color: #28a745;
}
.description {
font-size: 1.1rem;
line-height: 1.6;
margin: 1rem 0;
}
.actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background: #28a745;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
`,
],
})
export class ProductDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private productService = inject(ProductService);
product?: Product;
category = '';
ngOnInit() {
// Get route parameter
const id = Number(this.route.snapshot.paramMap.get('id'));
this.product = this.productService.getProductById(id);
// Get query parameter
this.category = this.route.snapshot.queryParamMap.get('category') || '';
// Alternative: Subscribe to params (for reactive updates)
this.route.paramMap.subscribe(params => {
const productId = Number(params.get('id'));
this.product = this.productService.getProductById(productId);
});
}
goBack() {
this.router.navigate(['/products']);
}
navigateToEdit() {
this.router.navigate(['/products', this.product?.id, 'edit']);
}
}
Task 2.2: Update Routes
// src/app/app.routes.ts
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent },
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductDetailComponent },
{ path: '**', component: NotFoundComponent },
];
Part 3: Route Guards
Task 3.1: Create Auth Service
// src/app/services/auth.service.ts
import { Injectable, signal } from '@angular/core';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private isAuthenticatedSignal = signal(false);
readonly isAuthenticated = this.isAuthenticatedSignal.asReadonly();
constructor(private router: Router) {
// Check localStorage for existing session
const savedAuth = localStorage.getItem('isAuthenticated');
this.isAuthenticatedSignal.set(savedAuth === 'true');
}
login(username: string, password: string): boolean {
// Mock authentication
if (username === 'admin' && password === 'admin') {
this.isAuthenticatedSignal.set(true);
localStorage.setItem('isAuthenticated', 'true');
return true;
}
return false;
}
logout() {
this.isAuthenticatedSignal.set(false);
localStorage.removeItem('isAuthenticated');
this.router.navigate(['/login']);
}
}
Task 3.2: Create Guards
// src/app/guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
router.navigate(['/login']);
return false;
};
// src/app/guards/unsaved-changes.guard.ts
export interface CanComponentDeactivate {
canDeactivate: () => boolean | Promise<boolean>;
}
export const unsavedChangesGuard = () => {
return (component: CanComponentDeactivate) => {
return component.canDeactivate ? component.canDeactivate() : true;
};
};
Task 3.3: Create Login and Dashboard
ng generate component pages/login --standalone
ng generate component pages/dashboard --standalone
// src/app/pages/login/login.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="login-container">
<div class="login-card">
<h2>Login</h2>
@if (errorMessage) {
<div class="error">{{ errorMessage }}</div>
}
<form (ngSubmit)="onSubmit()">
<div class="form-group">
<label>Username</label>
<input
type="text"
[(ngModel)]="username"
name="username"
required
/>
</div>
<div class="form-group">
<label>Password</label>
<input
type="password"
[(ngModel)]="password"
name="password"
required
/>
</div>
<button type="submit" class="btn-login">Login</button>
</form>
<p class="hint">Hint: admin / admin</p>
</div>
</div>
`,
styles: [
`
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.login-card {
width: 100%;
max-width: 400px;
padding: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.btn-login {
width: 100%;
padding: 0.75rem;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 0.75rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.hint {
text-align: center;
color: #6c757d;
margin-top: 1rem;
}
`,
],
})
export class LoginComponent {
private authService = inject(AuthService);
private router = inject(Router);
username = '';
password = '';
errorMessage = '';
onSubmit() {
if (this.authService.login(this.username, this.password)) {
this.router.navigate(['/dashboard']);
} else {
this.errorMessage = 'Invalid credentials';
}
}
}
// src/app/pages/dashboard/dashboard.component.ts
import { Component, inject } from '@angular/core';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `
<div class="dashboard">
<h1>Dashboard</h1>
<p>Welcome to your protected dashboard!</p>
<button (click)="logout()" class="btn">Logout</button>
</div>
`,
styles: [
`
.dashboard {
padding: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}
`,
],
})
export class DashboardComponent {
private authService = inject(AuthService);
logout() {
this.authService.logout();
}
}
Task 3.4: Apply Guards to Routes
// src/app/app.routes.ts
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard],
},
// ... other routes
];
Part 4: Lazy Loading
Task 4.1: Create Admin Module
ng generate component features/admin/admin-dashboard --standalone
ng generate component features/admin/user-management --standalone
// src/app/features/admin/admin.routes.ts
import { Routes } from '@angular/router';
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./admin-dashboard/admin-dashboard.component').then(
m => m.AdminDashboardComponent
),
},
{
path: 'users',
loadComponent: () =>
import('./user-management/user-management.component').then(
m => m.UserManagementComponent
),
},
];
// src/app/app.routes.ts
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{
path: 'admin',
loadChildren: () =>
import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
canActivate: [authGuard],
},
// ... other routes
];
Part 5: Resolvers
Task 5.1: Create Product Resolver
// src/app/resolvers/product.resolver.ts
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { ProductService } from '../services/product.service';
export const productResolver = (route: ActivatedRouteSnapshot) => {
const productService = inject(ProductService);
const id = Number(route.paramMap.get('id'));
return productService.getProductById(id);
};
// Update route
{
path: 'products/:id',
component: ProductDetailComponent,
resolve: {
product: productResolver
}
}
// Update component to use resolved data
ngOnInit() {
this.product = this.route.snapshot.data['product'];
// Or reactive
this.route.data.subscribe(data => {
this.product = data['product'];
});
}
Key Concepts Practiced
✅ Basic routing and navigation ✅ Route parameters (path and query) ✅ Programmatic navigation ✅ Route guards (canActivate, canDeactivate) ✅ Lazy loading ✅ Route resolvers ✅ Child routes ✅ RouterLink and RouterLinkActive
Time Estimate
- Basic routing: 2-3 hours
- Guards and auth: 2-3 hours
- Lazy loading: 1-2 hours
- Complete application: 5-8 hours
Next Steps
- Add breadcrumbs navigation
- Implement route animations
- Add preloading strategies
- Create route data management