Routing and Navigation
|
This section documents modern, standalone Angular — signals, the built-in 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('/');
}
}
See Navigate to routes.
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')}`,
}
See Route guards and Data resolvers.
The activation sequence
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.