Let me tell you about the time I built a multi-level admin dashboard for my blog platform. I had /admin (basic access) with nested routes like /admin/settings and /admin/users that needed extra permissions.
Using CanActivate on every child route felt repetitive and messy. Then I discovered that CanActivateChild applies once to the parent and protects ALL children automatically. Game changer!
The Nested Route Challenge I Faced
Imagine this structure:
admin/ ← Basic login requirement
├── settings/ ← Admin role needed
└── users/ ← Super admin only
Without CanActivateChild, I'd need guards on every child route. With it? One guard on the parent route handles all children elegantly.
My CanActivateChild Implementation
Step 1: Enhanced Auth Service with Roles
// auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
getUserRole(): 'guest' | 'user' | 'admin' | 'super-admin' {
const token = localStorage.getItem('token');
if (!token) return 'guest';
if (token.includes('admin')) return 'admin';
if (token.includes('super')) return 'super-admin';
return 'user';
}
isLoggedIn(): boolean {
return !!localStorage.getItem('token');
}
}
Step 2: Create CanActivateChild Guard
// admin-child.guard.ts
import { Injectable } from '@angular/core';
import { CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AdminChildGuard implements CanActivateChild {
constructor(private authService1: AuthService, private _router2: Router) {}
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
const role = this.authService1.getUserRole();
// Check child route specific permissions
if (childRoute.routeConfig?.path === 'users') {
if (role !== 'super-admin') {
alert('Super Admin required for user management!');
this._router2.navigateByUrl('/admin');
return Promise.resolve(Boolean(false));
}
} else if (childRoute.routeConfig?.path === 'settings') {
if (role !== 'admin') {
alert('Admin role required!');
this._router2.navigateByUrl('/admin/dashboard');
return Promise.resolve(Boolean(false));
}
}
console.log(`Child route ${state.url} approved for ${role}`);
return of(true);
}
}
Step 3: Route Configuration (One Guard Rules Them All)
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
component: AdminLayoutComponent,
canActivate: [AdminGuard], // Basic admin access
canActivateChild: [AdminChildGuard], // Protects ALL children!
children: [
{ path: 'dash-board', component: AdminDashboardComponent },
{ path: 'admin-settings', component: AdminSettingsComponent },
{ path: 'users', component: AdminUsersComponent },
{ path: '', redirectTo: 'dash-board', pathMatch: 'full' }
]
}
];
Step 4: Admin Layout Component (Bonus)
// admin-layout.component.ts
@Component({
template: `
<nav>Admin Dashboard</nav>
<router-outlet></router-outlet>
`
})
export class AdminLayoutComponent {}
Key insight: CanActivateChild runs every time you navigate between children, even if the parent is already loaded!
Testing Results From My App
- User role: 'user' → /admin/settings → BLOCKED
- User role: 'admin' → /admin/users → BLOCKED
- User role: 'super-admin' → All children → APPROVED
- Parent → Child navigation → Guard runs every time
Why This Pattern Rocks
- DRY Principle: One guard vs. multiple child guards
- Route-Specific Logic: childRoute param lets you check he exact child
- Smart Redirects: Send to safe parent routes instead of login
- Stackable: Combine with CanActivate on the same parent
- Performance: Runs only on child navigation
