The router maps URLs to components. You give it a table of routes, put an outlet in a template, and it fills the outlet with whatever matches.
provideRouter(
routes,<main class="flex-grow-1">
<router-outlet />
</main>The route table
{
path: 'menu',
title: 'Menu — PizzaHub',
loadComponent: () => import('./pages/menu/menu-page').then((m) => m.MenuPage),
},path has no leading slash. title sets the document title on
navigation — worth filling in, because otherwise every page in the browser's history is called the
same thing.
Order matters
Routes are matched top to bottom, first match wins. The wildcard therefore goes last:
{
path: '**',
title: 'Page not found — PizzaHub',
loadComponent: () => import('./pages/not-found/not-found').then((m) => m.NotFound),
},Put it anywhere else and it swallows everything below it.
Route parameters
{
path: 'order-confirmation/:orderId',The interesting part is how the component receives it. This is configured once at bootstrap:
withComponentInputBinding(),…and then a matching input() on the routed component is filled in
automatically:
readonly type = input<Filter | undefined>(undefined);That is the menu page receiving ?type=PIZZA as a signal, with no
ActivatedRoute injected and no subscription to clean up. It is the tidiest form of
React Router's useParams and useSearchParams, and it works for path
params, query params and route data alike.
Without it you inject ActivatedRoute and read paramMap, which is
still what you will see in most existing code. ⚠️ If you do, remember the route can change
without the component being recreated — navigating from
/orders/1 to /orders/2 reuses the instance. A value read once in
ngOnInit will be stale; the observable form is what handles it.
Links
<a routerLink="/menu" class="btn btn-primary btn-lg">Order now</a>
<a routerLink="/menu" [queryParams]="{ type: 'DRINK' }" class="btn btn-outline-light btn-lg">
Add a drink
</a>routerLink rather than href: it navigates within the application
instead of reloading the whole page. routerLinkActive adds a class when the route
matches, which is how the navbar highlights the current tab.
Navigating from code
void this.router.navigate(['/menu'], {
queryParams: filter === 'ALL' ? {} : { type: filter },
});The menu's filter tabs put the filter in the URL rather than in component state, deliberately:
/menu?type=PIZZA is then shareable, bookmarkable, and survives a refresh. Treating the
URL as state is usually the right call for anything a user might want to link to.
replaceUrl
await this.router.navigateByUrl(this.returnUrl(), { replaceUrl: true });After signing in, this swaps the history entry instead of adding one — so pressing Back does
not return the user to the login form they just completed. The same reasoning applies to a guard
returning a UrlTree rather than calling navigate().
Scroll position
withInMemoryScrolling({ scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled' }),Land at the top on a new page; restore the old position on Back. Browsers do this for a document navigation and cannot for a client-side one, so the router has to. It is one line and it is the difference between an app that feels right and one that does not.
Child routes
A route can have children, rendered into an outlet in the parent's template. The admin area is
built that way: AdminLayout draws the sidebar and contains its own
<router-outlet />, and the six admin screens render inside it. The layout is
therefore not repeated on six pages, and — more usefully — a guard on the parent protects all of
them.
What is next
That guard, its counterpart on the way out, and the lazy loading in every
loadComponent above.