angular-routing

Installation
SKILL.md

Angular Routing

Modern routing patterns with standalone components, functional guards, and signal-based parameter access.

Basic Route Configuration

// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./home/home.component')
      .then(m => m.HomeComponent)
  },
  {
    path: 'about',
    loadComponent: () => import('./about/about.component')
      .then(m => m.AboutComponent)
  },
  {
    path: 'users/:id',
    loadComponent: () => import('./user/user-detail.component')
      .then(m => m.UserDetailComponent)
  },
  {
    path: '**',
    redirectTo: ''
  }
];

App Configuration

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes)
  ]
};

Lazy Loading

Feature Routes

// feature.routes.ts
import { Routes } from '@angular/router';

export const FEATURE_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () => import('./feature-list.component')
      .then(m => m.FeatureListComponent)
  },
  {
    path: ':id',
    loadComponent: () => import('./feature-detail.component')
      .then(m => m.FeatureDetailComponent)
  }
];

// app.routes.ts
export const routes: Routes = [
  {
    path: 'features',
    loadChildren: () => import('./features/feature.routes')
      .then(m => m.FEATURE_ROUTES)
  }
];

Accessing Route Parameters

Using ActivatedRoute with Signals

import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-user-detail',
  template: `
    <h1>User ID: {{ userId() }}</h1>
    <h2>Tab: {{ tab() }}</h2>
  `
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  
  // Route params as signal
  userId = toSignal(
    this.route.params.pipe(map(params => params['id'])),
    { initialValue: '' }
  );
  
  // Query params as signal
  tab = toSignal(
    this.route.queryParams.pipe(map(params => params['tab'] || 'overview')),
    { initialValue: 'overview' }
  );
}

Using input() with Route Data (Angular 16+)

import { Component, input } from '@angular/core';

// Route configuration
{
  path: 'users/:id',
  loadComponent: () => import('./user-detail.component')
    .then(m => m.UserDetailComponent)
}

@Component({
  selector: 'app-user-detail',
  template: `
    <h1>User ID: {{ id() }}</h1>
  `
})
export class UserDetailComponent {
  // Automatically binds to route param 'id'
  id = input.required<string>();
}

Functional Guards

canActivate Guard

// auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.isAuthenticated()) {
    return true;
  }
  
  // Redirect to login
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// Usage in routes
{
  path: 'profile',
  loadComponent: () => import('./profile.component')
    .then(m => m.ProfileComponent),
  canActivate: [authGuard]
}

canActivateChild Guard

export const adminGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  return authService.hasRole('admin');
};

{
  path: 'admin',
  canActivateChild: [adminGuard],
  children: [
    {
      path: 'users',
      loadComponent: () => import('./admin-users.component')
        .then(m => m.AdminUsersComponent)
    },
    {
      path: 'settings',
      loadComponent: () => import('./admin-settings.component')
        .then(m => m.AdminSettingsComponent)
    }
  ]
}

canDeactivate Guard

import { CanDeactivateFn } from '@angular/router';

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

export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (
  component
) => {
  return component.canDeactivate 
    ? component.canDeactivate() 
    : true;
};

// Component
@Component({
  selector: 'app-form',
  template: `...`
})
export class FormComponent implements CanComponentDeactivate {
  hasUnsavedChanges = signal(false);
  
  canDeactivate(): boolean {
    if (this.hasUnsavedChanges()) {
      return confirm('You have unsaved changes. Do you want to leave?');
    }
    return true;
  }
}

// Route
{
  path: 'edit',
  loadComponent: () => import('./form.component')
    .then(m => m.FormComponent),
  canDeactivate: [unsavedChangesGuard]
}

Functional Resolvers

import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { UserService } from './user.service';

export const userResolver: ResolveFn<User> = (route, state) => {
  const userService = inject(UserService);
  const id = route.paramMap.get('id')!;
  return userService.getUser(id);
};

// Route configuration
{
  path: 'users/:id',
  loadComponent: () => import('./user-detail.component')
    .then(m => m.UserDetailComponent),
  resolve: {
    user: userResolver
  }
}

// Component
@Component({
  selector: 'app-user-detail',
  template: `
    @if (user(); as userData) {
      <h1>{{ userData.name }}</h1>
    }
  `
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  
  user = toSignal(
    this.route.data.pipe(map(data => data['user'])),
    { initialValue: null }
  );
}

Navigation

Programmatic Navigation

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-navigation',
  template: `
    <button (click)="goToUser(123)">Go to User</button>
    <button (click)="goBack()">Go Back</button>
  `
})
export class NavigationComponent {
  private router = inject(Router);
  
  goToUser(id: number) {
    // Navigate to route
    this.router.navigate(['/users', id]);
    
    // With query params
    this.router.navigate(['/users', id], {
      queryParams: { tab: 'profile' }
    });
    
    // Navigate by URL
    this.router.navigateByUrl(`/users/${id}`);
  }
  
  goBack() {
    // Go back in history
    window.history.back();
  }
}

RouterLink Directive

@Component({
  selector: 'app-nav',
  standalone: true,
  imports: [RouterLink, RouterLinkActive],
  template: `
    <nav>
      <a routerLink="/" routerLinkActive="active" 
         [routerLinkActiveOptions]="{exact: true}">
        Home
      </a>
      <a [routerLink]="['/users', userId]" routerLinkActive="active">
        User Profile
      </a>
      <a [routerLink]="['/settings']" 
         [queryParams]="{tab: 'notifications'}"
         routerLinkActive="active">
        Settings
      </a>
    </nav>
  `
})
export class NavComponent {
  userId = 123;
}

Child Routes

// Parent component
@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [RouterOutlet],
  template: `
    <div class="dashboard">
      <nav><!-- navigation --></nav>
      <router-outlet />
    </div>
  `
})
export class DashboardComponent {}

// Routes
{
  path: 'dashboard',
  loadComponent: () => import('./dashboard.component')
    .then(m => m.DashboardComponent),
  children: [
    {
      path: '',
      redirectTo: 'overview',
      pathMatch: 'full'
    },
    {
      path: 'overview',
      loadComponent: () => import('./overview.component')
        .then(m => m.OverviewComponent)
    },
    {
      path: 'analytics',
      loadComponent: () => import('./analytics.component')
        .then(m => m.AnalyticsComponent)
    }
  ]
}

Route Data

// Static route data
{
  path: 'admin',
  loadComponent: () => import('./admin.component')
    .then(m => m.AdminComponent),
  data: {
    title: 'Admin Panel',
    requiredRole: 'admin',
    breadcrumb: 'Administration'
  }
}

// Access route data
@Component({
  selector: 'app-admin',
  template: `<h1>{{ title() }}</h1>`
})
export class AdminComponent {
  private route = inject(ActivatedRoute);
  
  title = toSignal(
    this.route.data.pipe(map(data => data['title'])),
    { initialValue: '' }
  );
}

Router Events

import { Component, inject } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

@Component({
  selector: 'app-root',
  template: `<router-outlet />`
})
export class AppComponent {
  private router = inject(Router);
  
  constructor() {
    // Listen to navigation events
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      console.log('Navigated to:', event.url);
      // Track page views, update breadcrumbs, etc.
    });
  }
}

Advanced Patterns

Preloading Strategy

import { ApplicationConfig } from '@angular/core';
import { 
  provideRouter, 
  PreloadAllModules,
  withPreloading
} from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules)
    )
  ]
};

Custom Preloading Strategy

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 with data.preload = true
    return route.data?.['preload'] ? load() : of(null);
  }
}

// Usage
{
  path: 'important',
  loadComponent: () => import('./important.component')
    .then(m => m.ImportantComponent),
  data: { preload: true }
}

Best Practices

  1. Use lazy loading for better initial load performance
  2. Prefer functional guards over class-based guards
  3. Use signal-based parameter access with toSignal()
  4. Implement canDeactivate for unsaved changes
  5. Use resolvers to pre-fetch data before navigation
  6. Keep route configuration flat when possible
  7. Use route data for static configuration
  8. Implement proper error handling in guards/resolvers

Resources

Weekly Installs
2
First Seen
Jan 26, 2026
Installed on
claude-code2
codex1
github-copilot1
gemini-cli1