Exercise 2: Functional Guards & Resolvers
Objective
Learn to implement functional guards and resolvers in Angular 15+, replacing class-based guards with modern functional approach.
Prerequisites
- Completed Exercise 1: Standalone Component Basics
- Understanding of Angular routing
- Basic knowledge of dependency injection
What You'll Build
A multi-route application with:
- Authentication guard (CanActivate)
- Admin guard (CanActivate with role check)
- User data resolver
- Unsaved changes guard (CanDeactivate)
Time Estimate: 2-3 hours
Part 1: Authentication Guard
Step 1: Create Auth Service
Create src/app/services/auth.service.ts:
import { Injectable, signal } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class AuthService {
private isAuthenticatedSignal = signal(false);
private currentUserSignal = signal<{ name: string; role: string } | null>(null);
isAuthenticated = this.isAuthenticatedSignal.asReadonly();
currentUser = this.currentUserSignal.asReadonly();
login(username: string, password: string): boolean {
// Simulate authentication
if (username && password === "password") {
this.isAuthenticatedSignal.set(true);
this.currentUserSignal.set({
name: username,
role: username === "admin" ? "admin" : "user",
});
return true;
}
return false;
}
logout() {
this.isAuthenticatedSignal.set(false);
this.currentUserSignal.set(null);
}
hasRole(role: string): boolean {
return this.currentUser()?.role === role;
}
}
Step 2: Create Functional Auth Guard
Create src/app/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;
}
// Store the attempted URL for redirecting
return router.createUrlTree(["/login"], {
queryParams: { returnUrl: state.url },
});
};
Step 3: Create Login Component
ng generate component components/login --standalone
Update login.component.ts:
import { Component, inject } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";
import { FormsModule } from "@angular/forms";
import { CommonModule } from "@angular/common";
import { AuthService } from "../../services/auth.service";
@Component({
selector: "app-login",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="login-container">
<h2>Login</h2>
<form (ngSubmit)="onSubmit()">
<div>
<label>Username:</label>
<input type="text" [(ngModel)]="username" name="username" required />
</div>
<div>
<label>Password:</label>
<input type="password" [(ngModel)]="password" name="password" required />
</div>
<button type="submit">Login</button>
@if (errorMessage) {
<div class="error">{{ errorMessage }}</div>
}
</form>
<p>Hint: Use 'admin'/'password' or 'user'/'password'</p>
</div>
`,
styles: [
`
.login-container {
max-width: 400px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.error {
color: red;
margin-top: 10px;
}
`,
],
})
export class LoginComponent {
private authService = inject(AuthService);
private router = inject(Router);
private route = inject(ActivatedRoute);
username = "";
password = "";
errorMessage = "";
onSubmit() {
if (this.authService.login(this.username, this.password)) {
const returnUrl = this.route.snapshot.queryParams["returnUrl"] || "/dashboard";
this.router.navigateByUrl(returnUrl);
} else {
this.errorMessage = "Invalid credentials";
}
}
}
Part 2: Role-Based Guard
Step 1: Create Admin Guard
Create src/app/guards/admin.guard.ts:
import { inject } from "@angular/core";
import { Router, CanActivateFn } from "@angular/router";
import { AuthService } from "../services/auth.service";
export const adminGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isAuthenticated()) {
return router.createUrlTree(["/login"]);
}
if (authService.hasRole("admin")) {
return true;
}
// Not authorized - redirect to home
return router.createUrlTree(["/"]);
};
Step 2: Generic Role Guard Factory
Create src/app/guards/role.guard.ts:
import { inject } from "@angular/core";
import { Router, CanActivateFn } from "@angular/router";
import { AuthService } from "../services/auth.service";
export function hasRoleGuard(allowedRoles: string[]): CanActivateFn {
return (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isAuthenticated()) {
return router.createUrlTree(["/login"]);
}
const userRole = authService.currentUser()?.role;
if (userRole && allowedRoles.includes(userRole)) {
return true;
}
return router.createUrlTree(["/unauthorized"]);
};
}
Part 3: Data Resolver
Step 1: Create User Service
Create src/app/services/user.service.ts:
import { Injectable } from "@angular/core";
import { Observable, of, delay } from "rxjs";
export interface User {
id: number;
name: string;
email: string;
bio: string;
}
@Injectable({
providedIn: "root",
})
export class UserService {
getUser(id: number): Observable<User> {
// Simulate API call
return of({
id,
name: `User ${id}`,
email: `user${id}@example.com`,
bio: `Biography for user ${id}`,
}).pipe(delay(500));
}
}
Step 2: Create Functional Resolver
Create src/app/resolvers/user.resolver.ts:
import { inject } from "@angular/core";
import { ResolveFn, ActivatedRouteSnapshot } from "@angular/router";
import { Observable } from "rxjs";
import { User, UserService } from "../services/user.service";
export const userResolver: ResolveFn<User> = (route: ActivatedRouteSnapshot): Observable<User> => {
const userService = inject(UserService);
const userId = Number(route.paramMap.get("id"));
return userService.getUser(userId);
};
Step 3: Create User Profile Component
ng generate component components/user-profile --standalone
Update user-profile.component.ts:
import { Component, inject, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { CommonModule } from "@angular/common";
import { User } from "../../services/user.service";
@Component({
selector: "app-user-profile",
standalone: true,
imports: [CommonModule],
template: `
@if (user) {
<div class="profile">
<h2>{{ user.name }}</h2>
<p><strong>Email:</strong> {{ user.email }}</p>
<p><strong>Bio:</strong> {{ user.bio }}</p>
</div>
}
`,
styles: [
`
.profile {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
`,
],
})
export class UserProfileComponent implements OnInit {
private route = inject(ActivatedRoute);
user: User | null = null;
ngOnInit() {
// Data is resolved before component loads
this.user = this.route.snapshot.data["user"];
}
}
Part 4: Can Deactivate Guard
Step 1: Create Form Component with Unsaved Changes
ng generate component components/edit-profile --standalone
Update edit-profile.component.ts:
import { Component, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { CommonModule } from "@angular/common";
export interface CanComponentDeactivate {
canDeactivate: () => boolean;
}
@Component({
selector: "app-edit-profile",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="edit-form">
<h2>Edit Profile</h2>
<form>
<div>
<label>Name:</label>
<input type="text" [(ngModel)]="name" name="name" (ngModelChange)="markDirty()" />
</div>
<div>
<label>Email:</label>
<input type="email" [(ngModel)]="email" name="email" (ngModelChange)="markDirty()" />
</div>
<button type="button" (click)="save()">Save</button>
</form>
@if (isDirty()) {
<p class="warning">You have unsaved changes!</p>
}
</div>
`,
styles: [
`
.edit-form {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.warning {
color: orange;
font-weight: bold;
}
`,
],
})
export class EditProfileComponent implements CanComponentDeactivate {
name = "John Doe";
email = "john@example.com";
isDirty = signal(false);
markDirty() {
this.isDirty.set(true);
}
save() {
// Simulate save
this.isDirty.set(false);
alert("Profile saved!");
}
canDeactivate(): boolean {
if (this.isDirty()) {
return confirm("You have unsaved changes. Do you really want to leave?");
}
return true;
}
}
Step 2: Create Deactivate Guard
Create src/app/guards/can-deactivate.guard.ts:
import { CanDeactivateFn } from "@angular/router";
import { CanComponentDeactivate } from "../components/edit-profile/edit-profile.component";
export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
return component.canDeactivate ? component.canDeactivate() : true;
};
Part 5: Configure Routes
Update src/app/app.routes.ts:
import { Routes } from "@angular/router";
import { authGuard } from "./guards/auth.guard";
import { adminGuard } from "./guards/admin.guard";
import { hasRoleGuard } from "./guards/role.guard";
import { userResolver } from "./resolvers/user.resolver";
import { canDeactivateGuard } from "./guards/can-deactivate.guard";
export const routes: Routes = [
{ path: "", redirectTo: "/home", pathMatch: "full" },
{
path: "home",
loadComponent: () => import("./components/home/home.component").then((m) => m.HomeComponent),
},
{
path: "login",
loadComponent: () => import("./components/login/login.component").then((m) => m.LoginComponent),
},
{
path: "dashboard",
canActivate: [authGuard],
loadComponent: () => import("./components/dashboard/dashboard.component").then((m) => m.DashboardComponent),
},
{
path: "admin",
canActivate: [adminGuard],
loadComponent: () => import("./components/admin/admin.component").then((m) => m.AdminComponent),
},
{
path: "user/:id",
canActivate: [authGuard],
resolve: { user: userResolver },
loadComponent: () => import("./components/user-profile/user-profile.component").then((m) => m.UserProfileComponent),
},
{
path: "edit-profile",
canActivate: [authGuard],
canDeactivate: [canDeactivateGuard],
loadComponent: () => import("./components/edit-profile/edit-profile.component").then((m) => m.EditProfileComponent),
},
{
path: "manager",
canActivate: [hasRoleGuard(["admin", "manager"])],
loadComponent: () => import("./components/manager/manager.component").then((m) => m.ManagerComponent),
},
];
Testing Tasks
Task 1: Test Authentication Flow
- Navigate to
/dashboardwithout logging in - Verify redirect to
/login - Login with 'user'/'password'
- Verify redirect back to
/dashboard
Task 2: Test Role-Based Access
- Login as 'user'
- Try to access
/admin - Verify you're redirected away
- Logout and login as 'admin'
- Access
/adminsuccessfully
Task 3: Test Resolver
- Login
- Navigate to
/user/1 - Observe loading delay (resolver working)
- Verify user data displays
Task 4: Test Can Deactivate
- Navigate to
/edit-profile - Change the name field
- Try to navigate away
- Verify confirmation dialog appears
Challenges
Challenge 1: Multi-Step Guard
Create a guard that checks multiple conditions:
- User is authenticated
- User has verified email
- User accepted terms of service
Challenge 2: Data Prefetching
Create a resolver that prefetches multiple data sources in parallel.
Challenge 3: Dynamic Permission Guard
Create a guard that reads required permissions from route data.
Key Takeaways
✅ Functional guards are simpler than class-based guards
✅ Use inject() to get dependencies in functional guards
✅ Guards can return boolean, UrlTree, or Observable/Promise
✅ Resolvers load data before route activation
✅ Can deactivate guards prevent accidental data loss
Next Steps
Proceed to Exercise 3: Lazy Loading with loadComponent