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 loadComponent and loadChildren for 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)