Skip to content

Routing and Navigation in Angular

Course Objective

Master Angular's routing system to build sophisticated single-page applications with navigation, route guards, lazy loading, and advanced routing patterns.

1. Introduction to Angular Routing

Angular's Router enables navigation between different views in a single-page application. It manages the browser's URL and maps it to components, providing a seamless user experience.

Key Concepts

  • Route: A mapping between a URL path and a component
  • Router Outlet: A placeholder for routed components
  • Router: The service that manages navigation
  • ActivatedRoute: Information about the active route
  • Route Guards: Control access to routes
  • Lazy Loading: Load modules on demand

Benefits

  • SEO-friendly URLs
  • Browser history support
  • Bookmarkable pages
  • Code splitting with lazy loading
  • Navigation guards for security

2. Setting Up Routing

Modern Setup (Standalone Components)

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    // other providers
  ]
});
// app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './components/home.component';
import { AboutComponent } from './components/about.component';
import { ContactComponent } from './components/contact.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: '**', redirectTo: '' } // Wildcard route - must be last
];

Traditional Module Setup

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './components/home.component';
import { AboutComponent } from './components/about.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: '**', redirectTo: '' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

App Component Template

// 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="/" class="brand-link">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>
      </ul>
    </nav>

    <main class="main-content">
      <router-outlet></router-outlet>
    </main>
  `,
  styles: [`
    .navbar {
      display: flex;
      justify-content: space-between;
      padding: 1rem;
      background-color: #333;
      color: white;
    }

    .nav-links {
      display: flex;
      list-style: none;
      gap: 1rem;
      margin: 0;
      padding: 0;
    }

    .nav-links a {
      color: white;
      text-decoration: none;
      padding: 0.5rem 1rem;
      border-radius: 4px;
      transition: background-color 0.3s;
    }

    .nav-links a:hover,
    .nav-links a.active {
      background-color: #555;
    }

    .main-content {
      padding: 2rem;
    }
  `]
})
export class AppComponent {
  title = 'my-app';
}
// components/navigation.component.ts
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';

@Component({
  selector: 'app-navigation',
  standalone: true,
  imports: [RouterLink, RouterLinkActive],
  template: `
    <nav class="navigation">
      <!-- Simple navigation -->
      <a routerLink="/home">Home</a>
      <a routerLink="/products">Products</a>
      <a routerLink="/about">About</a>

      <!-- Navigation with parameters -->
      <a [routerLink]="['/user', userId]">My Profile</a>
      <a [routerLink]="['/product', productId]">Product Details</a>

      <!-- Navigation with query parameters -->
      <a [routerLink]="['/search']"
         [queryParams]="{q: 'angular', category: 'tech'}">
        Search Angular
      </a>

      <!-- Navigation with fragments -->
      <a [routerLink]="['/docs']"
         fragment="routing">
        Routing Section
      </a>

      <!-- Conditional navigation -->
      <a *ngIf="isLoggedIn"
         routerLink="/dashboard"
         routerLinkActive="active">
        Dashboard
      </a>

      <!-- Preserve query parameters -->
      <a [routerLink]="['/profile']"
         queryParamsHandling="preserve">
        Profile (preserve params)
      </a>
    </nav>
  `
})
export class NavigationComponent {
  userId = 123;
  productId = 456;
  isLoggedIn = true;
}

Programmatic Navigation with Router

// components/search.component.ts
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormControl, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <div class="search-container">
      <input
        [formControl]="searchControl"
        placeholder="Search..."
        (keyup.enter)="search()"
      >
      <button (click)="search()">Search</button>
      <button (click)="goBack()">Back</button>
      <button (click)="goToAdvancedSearch()">Advanced Search</button>
    </div>
  `
})
export class SearchComponent {
  searchControl = new FormControl('');

  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) {}

  search(): void {
    const query = this.searchControl.value;
    if (query) {
      // Navigate with query parameters
      this.router.navigate(['/search-results'], {
        queryParams: { q: query, page: 1 }
      });
    }
  }

  goBack(): void {
    // Navigate back in history
    window.history.back();
    // Or use Location service:
    // this.location.back();
  }

  goToAdvancedSearch(): void {
    // Navigate relative to current route
    this.router.navigate(['../advanced'], {
      relativeTo: this.route,
      queryParams: { mode: 'advanced' }
    });
  }

  navigateWithOptions(): void {
    this.router.navigate(['/results'], {
      queryParams: { search: 'angular' },
      fragment: 'top',
      queryParamsHandling: 'merge', // merge with existing params
      skipLocationChange: false, // update URL
      replaceUrl: false // don't replace current URL in history
    });
  }
}

4. Route Parameters

Defining Routes with Parameters

// app.routes.ts
export const routes: Routes = [
  { path: 'user/:id', component: UserComponent },
  { path: 'user/:id/edit', component: EditUserComponent },
  { path: 'product/:categoryId/:productId', component: ProductComponent },
  { path: 'search', component: SearchComponent }, // Query params
  { path: '**', redirectTo: '' }
];

Accessing Route Parameters

// components/user.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil, switchMap } from 'rxjs/operators';
import { UserService } from '../services/user.service';

@Component({
  selector: 'app-user',
  template: `
    <div class="user-profile" *ngIf="user">
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
      <button (click)="editUser()">Edit</button>
      <button (click)="goToNextUser()">Next User</button>
    </div>

    <div *ngIf="loading">Loading user...</div>
    <div *ngIf="error">{{ error }}</div>
  `
})
export class UserComponent implements OnInit, OnDestroy {
  user: User | null = null;
  loading = false;
  error: string | null = null;
  private destroy$ = new Subject<void>();

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private userService: UserService
  ) {}

  ngOnInit(): void {
    // Method 1: Using paramMap observable (recommended)
    this.route.paramMap
      .pipe(
        takeUntil(this.destroy$),
        switchMap((params: ParamMap) => {
          const id = Number(params.get('id'));
          this.loading = true;
          this.error = null;
          return this.userService.getUser(id);
        })
      )
      .subscribe({
        next: (user) => {
          this.user = user;
          this.loading = false;
        },
        error: (error) => {
          this.error = 'Failed to load user';
          this.loading = false;
          console.error('Error loading user:', error);
        }
      });

    // Method 2: Snapshot (for one-time access)
    // const id = Number(this.route.snapshot.paramMap.get('id'));
    // this.loadUser(id);

    // Access query parameters
    this.route.queryParamMap
      .pipe(takeUntil(this.destroy$))
      .subscribe(queryParams => {
        const tab = queryParams.get('tab');
        const edit = queryParams.get('edit') === 'true';
        // Handle query parameters
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  editUser(): void {
    if (this.user) {
      this.router.navigate(['/user', this.user.id, 'edit']);
    }
  }

  goToNextUser(): void {
    if (this.user) {
      const nextId = this.user.id + 1;
      this.router.navigate(['/user', nextId]);
    }
  }
}

Working with Query Parameters

// components/search-results.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject, combineLatest } from 'rxjs';
import { takeUntil, map, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'app-search-results',
  template: `
    <div class="search-results">
      <div class="search-header">
        <h2>Search Results for "{{ searchQuery }}"</h2>
        <div class="filters">
          <select (change)="updateCategory($event)">
            <option value="">All Categories</option>
            <option value="tech">Technology</option>
            <option value="business">Business</option>
          </select>

          <input
            type="number"
            [value]="currentPage"
            (change)="updatePage($event)"
            min="1"
          >
        </div>
      </div>

      <div class="results">
        <div *ngFor="let result of results" class="result-item">
          {{ result.title }}
        </div>
      </div>

      <div class="pagination">
        <button
          [disabled]="currentPage <= 1"
          (click)="previousPage()">
          Previous
        </button>
        <span>Page {{ currentPage }} of {{ totalPages }}</span>
        <button
          [disabled]="currentPage >= totalPages"
          (click)="nextPage()">
          Next
        </button>
      </div>
    </div>
  `
})
export class SearchResultsComponent implements OnInit, OnDestroy {
  searchQuery = '';
  category = '';
  currentPage = 1;
  totalPages = 1;
  results: any[] = [];
  private destroy$ = new Subject<void>();

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private searchService: SearchService
  ) {}

  ngOnInit(): void {
    // Listen to both route params and query params
    combineLatest([
      this.route.paramMap,
      this.route.queryParamMap
    ]).pipe(
      takeUntil(this.destroy$),
      map(([params, queryParams]) => ({
        query: queryParams.get('q') || '',
        category: queryParams.get('category') || '',
        page: Number(queryParams.get('page')) || 1
      })),
      debounceTime(100), // Prevent rapid updates
      distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
    ).subscribe(({ query, category, page }) => {
      this.searchQuery = query;
      this.category = category;
      this.currentPage = page;
      this.performSearch();
    });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  private performSearch(): void {
    this.searchService.search({
      query: this.searchQuery,
      category: this.category,
      page: this.currentPage
    }).subscribe(response => {
      this.results = response.results;
      this.totalPages = response.totalPages;
    });
  }

  updateCategory(event: any): void {
    const category = event.target.value;
    this.updateQueryParams({ category, page: 1 });
  }

  updatePage(event: any): void {
    const page = Number(event.target.value);
    this.updateQueryParams({ page });
  }

  previousPage(): void {
    if (this.currentPage > 1) {
      this.updateQueryParams({ page: this.currentPage - 1 });
    }
  }

  nextPage(): void {
    if (this.currentPage < this.totalPages) {
      this.updateQueryParams({ page: this.currentPage + 1 });
    }
  }

  private updateQueryParams(params: any): void {
    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: params,
      queryParamsHandling: 'merge'
    });
  }
}

5. Nested Routes

Defining Nested Routes

// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    children: [
      { path: '', component: DashboardHomeComponent },
      { path: 'analytics', component: AnalyticsComponent },
      { path: 'reports', component: ReportsComponent },
      { path: 'settings', component: SettingsComponent }
    ]
  },
  {
    path: 'admin',
    component: AdminLayoutComponent,
    children: [
      { path: '', redirectTo: 'overview', pathMatch: 'full' },
      { path: 'overview', component: AdminOverviewComponent },
      {
        path: 'users',
        component: UserManagementComponent,
        children: [
          { path: '', component: UserListComponent },
          { path: ':id', component: UserDetailComponent },
          { path: ':id/edit', component: EditUserComponent }
        ]
      }
    ]
  }
];

Parent Component with Router Outlet

// components/dashboard.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <div class="dashboard-layout">
      <aside class="sidebar">
        <nav class="sidebar-nav">
          <a routerLink="/dashboard"
             routerLinkActive="active"
             [routerLinkActiveOptions]="{exact: true}">
            Home
          </a>
          <a routerLink="/dashboard/analytics"
             routerLinkActive="active">
            Analytics
          </a>
          <a routerLink="/dashboard/reports"
             routerLinkActive="active">
            Reports
          </a>
          <a routerLink="/dashboard/settings"
             routerLinkActive="active">
            Settings
          </a>
        </nav>
      </aside>

      <main class="main-content">
        <header class="content-header">
          <h1>Dashboard</h1>
          <div class="user-info">
            Welcome, {{ userName }}
          </div>
        </header>

        <!-- Nested routes rendered here -->
        <router-outlet></router-outlet>
      </main>
    </div>
  `,
  styles: [`
    .dashboard-layout {
      display: flex;
      height: 100vh;
    }

    .sidebar {
      width: 250px;
      background-color: #2c3e50;
      color: white;
    }

    .sidebar-nav {
      display: flex;
      flex-direction: column;
      padding: 1rem 0;
    }

    .sidebar-nav a {
      color: white;
      text-decoration: none;
      padding: 1rem 1.5rem;
      transition: background-color 0.3s;
    }

    .sidebar-nav a:hover,
    .sidebar-nav a.active {
      background-color: #34495e;
    }

    .main-content {
      flex: 1;
      display: flex;
      flex-direction: column;
    }

    .content-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem 2rem;
      border-bottom: 1px solid #eee;
    }
  `]
})
export class DashboardComponent {
  userName = 'John Doe';
}

Accessing Parent Route Data

// components/user-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map, switchMap } from 'rxjs/operators';

@Component({
  selector: 'app-user-detail',
  template: `
    <div class="user-detail">
      <div class="breadcrumb">
        <a [routerLink]="['/admin']">Admin</a> >
        <a [routerLink]="['/admin/users']">Users</a> >
        <span>{{ user?.name }}</span>
      </div>

      <div *ngIf="user" class="user-info">
        <h2>{{ user.name }}</h2>
        <p>{{ user.email }}</p>
        <button [routerLink]="['edit']">Edit User</button>
      </div>
    </div>
  `
})
export class UserDetailComponent implements OnInit {
  user: User | null = null;

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    // Access nested route parameters
    this.route.paramMap.pipe(
      map(params => Number(params.get('id'))),
      switchMap(id => this.userService.getUser(id))
    ).subscribe(user => {
      this.user = user;
    });

    // Access parent route data
    this.route.parent?.data.subscribe(parentData => {
      console.log('Parent route data:', parentData);
    });

    // Access root route data
    this.route.root.data.subscribe(rootData => {
      console.log('Root route data:', rootData);
    });
  }
}

6. Route Guards

CanActivate Guard

// guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivate(): Observable<boolean | UrlTree> {
    return this.authService.isAuthenticated$.pipe(
      take(1),
      map(isAuthenticated => {
        if (isAuthenticated) {
          return true;
        } else {
          // Redirect to login page
          return this.router.createUrlTree(['/login']);
        }
      })
    );
  }
}

CanActivateChild Guard

// guards/admin.guard.ts
import { Injectable } from '@angular/core';
import { CanActivateChild, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AdminGuard implements CanActivateChild {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivateChild(): Observable<boolean | UrlTree> {
    return this.authService.user$.pipe(
      map(user => {
        if (user && user.role === 'admin') {
          return true;
        } else {
          return this.router.createUrlTree(['/unauthorized']);
        }
      })
    );
  }
}

CanDeactivate Guard

// guards/can-deactivate.guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';

export interface CanComponentDeactivate {
  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable({
  providedIn: 'root'
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(
    component: CanComponentDeactivate
  ): Observable<boolean> | Promise<boolean> | boolean {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

// Using in a component
@Component({...})
export class EditFormComponent implements CanComponentDeactivate {
  @HostListener('window:beforeunload', ['$event'])
  unloadNotification($event: BeforeUnloadEvent): void {
    if (this.hasUnsavedChanges) {
      $event.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
    }
  }

  canDeactivate(): boolean {
    if (this.hasUnsavedChanges) {
      return confirm('You have unsaved changes. Are you sure you want to leave?');
    }
    return true;
  }

  private get hasUnsavedChanges(): boolean {
    return this.form.dirty && !this.form.submitted;
  }
}

Resolve Guard

// guards/user-resolver.guard.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { UserService } from '../services/user.service';

@Injectable({
  providedIn: 'root'
})
export class UserResolver implements Resolve<User | null> {
  constructor(private userService: UserService) {}

  resolve(route: ActivatedRouteSnapshot): Observable<User | null> {
    const id = Number(route.paramMap.get('id'));

    return this.userService.getUser(id).pipe(
      catchError(error => {
        console.error('Error loading user:', error);
        return of(null);
      })
    );
  }
}

Applying Guards to Routes

// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [AuthGuard],
    children: [
      { path: '', component: DashboardHomeComponent },
      { path: 'analytics', component: AnalyticsComponent }
    ]
  },
  {
    path: 'admin',
    component: AdminLayoutComponent,
    canActivate: [AuthGuard],
    canActivateChild: [AdminGuard],
    children: [
      {
        path: 'user/:id',
        component: UserDetailComponent,
        resolve: { user: UserResolver }
      }
    ]
  },
  {
    path: 'edit-profile',
    component: EditProfileComponent,
    canActivate: [AuthGuard],
    canDeactivate: [CanDeactivateGuard]
  }
];

7. Lazy Loading

Feature Module Setup

// features/products/products.routes.ts
import { Routes } from '@angular/router';
import { ProductListComponent } from './components/product-list.component';
import { ProductDetailComponent } from './components/product-detail.component';

export const PRODUCTS_ROUTES: Routes = [
  { path: '', component: ProductListComponent },
  { path: ':id', component: ProductDetailComponent }
];

Main Routes with Lazy Loading

// app.routes.ts
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'products',
    loadChildren: () => import('./features/products/products.routes').then(m => m.PRODUCTS_ROUTES)
  },
  {
    path: 'admin',
    loadChildren: () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
    canActivate: [AuthGuard]
  },
  {
    path: 'user',
    loadComponent: () => import('./features/user/user.component').then(m => m.UserComponent)
  },
  { path: '**', redirectTo: '' }
];

Preloading Strategies

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules) // Preload all lazy modules
    )
  ]
});

Custom Preloading Strategy

// services/custom-preloading.service.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // Only preload routes marked with preload: true
    if (route.data && route.data['preload']) {
      console.log('Preloading: ' + route.path);
      return load();
    } else {
      return of(null);
    }
  }
}

// Using custom preloading strategy
export const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => import('./features/products/products.routes').then(m => m.PRODUCTS_ROUTES),
    data: { preload: true } // This route will be preloaded
  },
  {
    path: 'admin',
    loadChildren: () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
    data: { preload: false } // This route will not be preloaded
  }
];

8. Advanced Routing Patterns

Route Data and Custom Properties

// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    data: {
      title: 'Dashboard',
      breadcrumb: 'Dashboard',
      roles: ['user', 'admin'],
      animation: 'slide'
    }
  },
  {
    path: 'admin',
    component: AdminComponent,
    data: {
      title: 'Administration',
      breadcrumb: 'Admin',
      roles: ['admin'],
      hideNavigation: true
    }
  }
];

// Accessing route data
@Component({...})
export class BaseComponent implements OnInit {
  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    this.route.data.subscribe(data => {
      document.title = data['title'] || 'Default Title';
      this.showNavigation = !data['hideNavigation'];
    });
  }
}

Dynamic Route Configuration

// services/dynamic-routes.service.ts
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { ComponentType } from '@angular/cdk/portal';

@Injectable({
  providedIn: 'root'
})
export class DynamicRoutesService {
  constructor(private router: Router) {}

  addRoute(path: string, component: ComponentType<any>): void {
    const config = this.router.config;
    config.unshift({ path, component });
    this.router.resetConfig(config);
  }

  removeRoute(path: string): void {
    const config = this.router.config.filter(route => route.path !== path);
    this.router.resetConfig(config);
  }

  addPluginRoutes(pluginRoutes: any[]): void {
    const config = [...pluginRoutes, ...this.router.config];
    this.router.resetConfig(config);
  }
}

Route Animations

// animations/route-animations.ts
import { trigger, transition, style, query, animateChild, group, animate } from '@angular/animations';

export const routeAnimations = trigger('routeAnimations', [
  transition('* <=> *', [
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%'
      })
    ], { optional: true }),

    query(':enter', [
      style({ opacity: 0, transform: 'translateX(100%)' })
    ], { optional: true }),

    group([
      query(':leave', [
        animate('300ms ease-out', style({ opacity: 0, transform: 'translateX(-100%)' }))
      ], { optional: true }),

      query(':enter', [
        animate('300ms ease-out', style({ opacity: 1, transform: 'translateX(0%)' }))
      ], { optional: true })
    ])
  ])
]);

// app.component.ts
@Component({
  selector: 'app-root',
  template: `
    <div [@routeAnimations]="prepareRoute(outlet)">
      <router-outlet #outlet="outlet"></router-outlet>
    </div>
  `,
  animations: [routeAnimations]
})
export class AppComponent {
  prepareRoute(outlet: RouterOutlet) {
    return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
  }
}

9. Router Events and Navigation

Listening to Router Events

// services/navigation.service.ts
import { Injectable } from '@angular/core';
import {
  Router,
  NavigationStart,
  NavigationEnd,
  NavigationCancel,
  NavigationError,
  RoutesRecognized
} from '@angular/router';
import { filter } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class NavigationService {
  constructor(private router: Router) {
    this.setupRouterEventListeners();
  }

  private setupRouterEventListeners(): void {
    // Listen to navigation start
    this.router.events.pipe(
      filter(event => event instanceof NavigationStart)
    ).subscribe((event: NavigationStart) => {
      console.log('Navigation started to:', event.url);
      this.showLoadingIndicator();
    });

    // Listen to navigation end
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      console.log('Navigation ended:', event.url);
      this.hideLoadingIndicator();
      this.trackPageView(event.url);
    });

    // Listen to navigation errors
    this.router.events.pipe(
      filter(event => event instanceof NavigationError)
    ).subscribe((event: NavigationError) => {
      console.error('Navigation error:', event.error);
      this.handleNavigationError(event);
    });

    // Listen to route recognition
    this.router.events.pipe(
      filter(event => event instanceof RoutesRecognized)
    ).subscribe((event: RoutesRecognized) => {
      console.log('Routes recognized:', event.state.root);
    });
  }

  private showLoadingIndicator(): void {
    // Show global loading indicator
  }

  private hideLoadingIndicator(): void {
    // Hide global loading indicator
  }

  private trackPageView(url: string): void {
    // Analytics tracking
    // gtag('config', 'GA_TRACKING_ID', { page_path: url });
  }

  private handleNavigationError(event: NavigationError): void {
    // Log error to monitoring service
    // Redirect to error page if needed
    this.router.navigate(['/error'], { queryParams: { error: 'navigation' } });
  }
}

Loading Indicator Component

// components/loading-indicator.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';

@Component({
  selector: 'app-loading-indicator',
  template: `
    <div class="loading-bar" [class.active]="loading">
      <div class="loading-progress"></div>
    </div>
  `,
  styles: [`
    .loading-bar {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 3px;
      background-color: #f0f0f0;
      z-index: 9999;
      opacity: 0;
      transition: opacity 0.3s;
    }

    .loading-bar.active {
      opacity: 1;
    }

    .loading-progress {
      height: 100%;
      background: linear-gradient(90deg, #007bff, #0056b3);
      width: 0%;
      animation: loading 2s linear infinite;
    }

    @keyframes loading {
      0% { width: 0%; }
      50% { width: 70%; }
      100% { width: 100%; }
    }
  `]
})
export class LoadingIndicatorComponent implements OnInit, OnDestroy {
  loading = false;
  private destroy$ = new Subject<void>();

  constructor(private router: Router) {}

  ngOnInit(): void {
    this.router.events
      .pipe(
        takeUntil(this.destroy$),
        filter(event =>
          event instanceof NavigationStart ||
          event instanceof NavigationEnd ||
          event instanceof NavigationCancel ||
          event instanceof NavigationError
        )
      )
      .subscribe(event => {
        if (event instanceof NavigationStart) {
          this.loading = true;
        } else {
          this.loading = false;
        }
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

10. Testing Routing

Testing Router Navigation

// components/navigation.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { NavigationComponent } from './navigation.component';

@Component({ template: '' })
class DummyComponent { }

describe('NavigationComponent', () => {
  let component: NavigationComponent;
  let fixture: ComponentFixture<NavigationComponent>;
  let router: Router;
  let location: Location;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [NavigationComponent],
      imports: [
        RouterTestingModule.withRoutes([
          { path: 'home', component: DummyComponent },
          { path: 'about', component: DummyComponent },
          { path: 'contact', component: DummyComponent }
        ])
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(NavigationComponent);
    component = fixture.componentInstance;
    router = TestBed.inject(Router);
    location = TestBed.inject(Location);

    fixture.detectChanges();
  });

  it('should navigate to home', async () => {
    await router.navigate(['/home']);
    expect(location.path()).toBe('/home');
  });

  it('should navigate to about page when button clicked', async () => {
    const aboutButton = fixture.debugElement.nativeElement.querySelector('[data-test="about-button"]');
    aboutButton.click();

    await fixture.whenStable();
    expect(location.path()).toBe('/about');
  });
});

Testing Route Guards

// guards/auth.guard.spec.ts
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { AuthService } from '../services/auth.service';
import { of } from 'rxjs';

describe('AuthGuard', () => {
  let guard: AuthGuard;
  let authService: jasmine.SpyObj<AuthService>;
  let router: jasmine.SpyObj<Router>;

  beforeEach(() => {
    const authServiceSpy = jasmine.createSpyObj('AuthService', [], {
      isAuthenticated$: of(false)
    });
    const routerSpy = jasmine.createSpyObj('Router', ['createUrlTree']);

    TestBed.configureTestingModule({
      providers: [
        AuthGuard,
        { provide: AuthService, useValue: authServiceSpy },
        { provide: Router, useValue: routerSpy }
      ]
    });

    guard = TestBed.inject(AuthGuard);
    authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
    router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
  });

  it('should allow access when user is authenticated', (done) => {
    (Object.getOwnPropertyDescriptor(authService, 'isAuthenticated$')!.get as jasmine.Spy).and.returnValue(of(true));

    guard.canActivate().subscribe(result => {
      expect(result).toBe(true);
      done();
    });
  });

  it('should redirect to login when user is not authenticated', (done) => {
    const urlTree = router.createUrlTree(['/login']);
    router.createUrlTree.and.returnValue(urlTree);

    guard.canActivate().subscribe(result => {
      expect(result).toBe(urlTree);
      expect(router.createUrlTree).toHaveBeenCalledWith(['/login']);
      done();
    });
  });
});

Testing Route Parameters

// components/user.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { UserComponent } from './user.component';
import { UserService } from '../services/user.service';

describe('UserComponent', () => {
  let component: UserComponent;
  let fixture: ComponentFixture<UserComponent>;
  let userService: jasmine.SpyObj<UserService>;

  beforeEach(async () => {
    const userServiceSpy = jasmine.createSpyObj('UserService', ['getUser']);

    await TestBed.configureTestingModule({
      declarations: [UserComponent],
      providers: [
        { provide: UserService, useValue: userServiceSpy },
        {
          provide: ActivatedRoute,
          useValue: {
            paramMap: of(new Map([['id', '123']])),
            queryParamMap: of(new Map([['tab', 'profile']]))
          }
        }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(UserComponent);
    component = fixture.componentInstance;
    userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;

    const mockUser = { id: 123, name: 'John Doe', email: 'john@example.com' };
    userService.getUser.and.returnValue(of(mockUser));

    fixture.detectChanges();
  });

  it('should load user based on route parameter', () => {
    expect(userService.getUser).toHaveBeenCalledWith(123);
    expect(component.user).toEqual({ id: 123, name: 'John Doe', email: 'john@example.com' });
  });
});

11. Best Practices

Route Organization

// ✅ Good: Organized route structure
export const routes: Routes = [
  // Public routes
  { path: '', component: HomeComponent },
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },

  // Protected routes
  {
    path: 'app',
    canActivate: [AuthGuard],
    children: [
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard', component: DashboardComponent },
      { path: 'profile', component: ProfileComponent }
    ]
  },

  // Feature modules (lazy loaded)
  {
    path: 'products',
    loadChildren: () => import('./features/products/products.module').then(m => m.ProductsModule)
  },

  // Error routes
  { path: '404', component: NotFoundComponent },
  { path: 'error', component: ErrorComponent },

  // Wildcard route (must be last)
  { path: '**', redirectTo: '404' }
];

Memory Management

// ✅ Good: Proper subscription management
@Component({...})
export class RouteAwareComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    // Use takeUntil for automatic unsubscription
    this.route.paramMap
      .pipe(takeUntil(this.destroy$))
      .subscribe(params => {
        // Handle parameters
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Performance Optimization

// ✅ Use OnPush change detection with async pipe
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div *ngIf="user$ | async as user">
      {{ user.name }}
    </div>
  `
})
export class OptimizedUserComponent {
  user$ = this.route.paramMap.pipe(
    map(params => Number(params.get('id'))),
    switchMap(id => this.userService.getUser(id)),
    shareReplay(1)
  );

  constructor(
    private route: ActivatedRoute,
    private userService: UserService
  ) {}
}

Common Pitfalls to Avoid

  1. Not handling route parameters reactively - Use paramMap observable, not snapshot
  2. Memory leaks from subscriptions - Always unsubscribe in ngOnDestroy
  3. Wildcard route placement - Always place wildcard routes last
  4. Not implementing proper loading states - Show loading indicators during navigation
  5. Ignoring error handling - Handle navigation errors gracefully
  6. Not using route guards - Protect routes that require authentication
  7. Poor route organization - Group related routes and use feature modules

Next Steps

After mastering Angular routing, you'll be ready to explore:

  • Forms: Build complex forms with validation
  • State Management: Manage application state with NgRx or Akita
  • Performance Optimization: Advanced optimization techniques
  • Testing: Comprehensive testing strategies

Routing is essential for creating professional Angular applications. Practice these patterns to build robust, user-friendly navigation systems!