Routing and Navigation

This section documents modern, standalone Angular — signals, the built-in @if / @for / @switch control flow, @defer, typed reactive forms, provideHttpClient, functional guards and interceptors, and server-side rendering with hydration — as described by the official documentation at angular.dev, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted.

This section’s bibliography lists the reference material consulted while preparing these pages.

The Angular router maps the browser URL to a tree of components, swapping views without a full page reload. It is configured functionally with provideRouter and a Routes array.

Configuration

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

export const routes: Routes = [
  { path: '', component: Home, title: 'Home' },
  { path: 'books', loadComponent: () => import('./books/book-list').then((m) => m.BookList) },
  { path: 'books/:id', loadComponent: () => import('./books/book-detail').then((m) => m.BookDetail) },
  { path: 'legacy', redirectTo: 'books', pathMatch: 'full' },
  { path: '**', component: NotFound, title: 'Not found' }, // wildcard: keep last
];
// app.config.ts
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(), // bind route params/data/query to component input() properties
      withViewTransitions(),       // animate route changes with the View Transitions API
    ),
  ],
};

Place a <router-outlet> where the matched component should render, and set a <base href="/"> in index.html so the router can resolve absolute paths (the CLI adds it):

<nav><a routerLink="/books">Books</a></nav>
<router-outlet />

Routes are matched top to bottom; the first match wins, so put specific paths before :param paths and the ** wildcard last. See Routing overview and Define routes.

Linking and navigating

routerLink builds URLs without reloading; routerLinkActive toggles a class when the link’s route is active. In code, inject Router and call navigate (a link-parameters array) or navigateByUrl (a string).

<a routerLink="/books">All books</a>
<a [routerLink]="['/books', book.id]" routerLinkActive="active">{{ book.title }}</a>
<a [routerLink]="['/books', book.id]" [queryParams]="{ tab: 'reviews' }" fragment="top">Reviews</a>

<!-- named (auxiliary) outlet -->
<router-outlet name="aside" />
<a [routerLink]="[{ outlets: { aside: ['help'] } }]">Open help</a>
import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

@Component({ /* ... */ })
export class BookActions {
  private readonly router = inject(Router);
  private readonly route = inject(ActivatedRoute);

  open(id: string) {
    this.router.navigate(['/books', id], { queryParams: { tab: 'reviews' } });
  }

  next() {
    // relative navigation: resolve against the current route
    this.router.navigate(['..', '43'], { relativeTo: this.route });
  }

  home() {
    this.router.navigateByUrl('/');
  }
}

Reading route state

Route data comes from path segments (:id), the static data object, the resolved title, and the URL’s ?query and #fragment. Read it either from ActivatedRoute observables or — with withComponentInputBinding() — straight from input() properties whose names match.

// route: { path: 'books/:id', component: BookDetail, data: { mode: 'view' } }

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

@Component({ selector: 'app-book-detail', template: '{{ id() }} / {{ mode() }} / {{ tab() }}' })
export class BookDetail {
  // component input binding: matched from path param, data, query param, or resolver key
  readonly id = input.required<string>();
  readonly mode = input<'view' | 'edit'>('view');
  readonly tab = input<string>();

  // or, the observable form
  private readonly route = inject(ActivatedRoute);
  readonly params = toSignal(this.route.paramMap);
  readonly query = toSignal(this.route.queryParamMap);
}

Set a dynamic title with a ResolveFn on the route’s title property. See Read route state.

Child routes and lazy loading

A route with children renders them into a nested <router-outlet> inside its component. Splitting code by route keeps the initial bundle small: loadComponent lazily loads one standalone component, loadChildren lazily loads a child Routes array.

export const routes: Routes = [
  {
    path: 'books',
    component: BooksShell,          // has its own <router-outlet />
    children: [
      { path: '', component: BookList },
      { path: ':id', component: BookDetail },
    ],
  },
  {
    path: 'admin',
    canMatch: [isAdmin],
    loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
  },
];

Each lazily loaded area gets its own environment injector, so providers on a lazy route are scoped to it (see Dependency injection). See Lazy loading.

Guards and resolvers

Guards are functions returning boolean, UrlTree (a redirect), or a Promise/Observable of either. They run in the injection context, so they inject() services directly — no class needed.

import { inject } from '@angular/core';
import {
  CanActivateFn, CanActivateChildFn, CanDeactivateFn, CanMatchFn, ResolveFn, Router,
} from '@angular/router';

// CanMatch: decide whether the route matches at all (skips to the next route if false)
export const isAdmin: CanMatchFn = () =>
  inject(AuthStore).role() === 'admin' || inject(Router).createUrlTree(['/forbidden']);

// CanActivate: guard entry to a route
export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthStore);
  return auth.isLoggedIn()
    ? true
    : inject(Router).createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};

// CanActivateChild: guard entry to any child route
export const sectionGuard: CanActivateChildFn = (route, state) => authGuard(route, state);

// CanDeactivate: guard leaving (e.g. unsaved changes); typed to the component
export const confirmLeave: CanDeactivateFn<EditForm> = (component) =>
  component.saved() || confirm('Discard unsaved changes?');

// ResolveFn: pre-fetch data before activation; the value lands in the route's data
export const bookResolver: ResolveFn<Book> = (route) =>
  inject(BookApi).get(route.paramMap.get('id')!);

Wire them onto routes, alongside any route-scoped providers:

{
  path: 'books/:id',
  component: BookDetail,
  canActivate: [authGuard],
  canDeactivate: [confirmLeave],
  resolve: { book: bookResolver },
  providers: [BookApi],
  title: (route) => `Book ${route.paramMap.get('id')}`,
}

The activation sequence

flowchart TD A["URL change"] --> B["Match a route (top to bottom)"] B --> C{"CanMatch?"} C -- "false / UrlTree" --> B2["Try the next route
or redirect"] C -- "true" --> D{"CanActivate /
CanActivateChild?"} D -- "false" --> R["Redirect to UrlTree
(e.g. /login)"] D -- "true" --> E["Run resolvers
(wait for data)"] E --> F["CanDeactivate the
outgoing component?"] F -- "false" --> X["Cancel navigation"] F -- "true" --> G["Instantiate component,
render into router-outlet"]

If any guard returns a UrlTree, the router cancels the current navigation and starts a new one to that URL — the redirect branch above.