1. Route Configuration & the Router Service
You've had provideRouter(routes) in your app.config.ts since
Week 3, sitting there unused. This week it earns its place — routes is just
an array mapping URL paths to components.
import { Routes } from '@angular/router';
import { DashboardOverview } from './dashboard/overview/overview';
import { ProjectDetail } from './dashboard/project-detail/project-detail';
export const routes: Routes = [
{ path: '', component: DashboardOverview },
{ path: 'projects/:projectId', component: ProjectDetail },
];
And in your template, a single <router-outlet> is where the matched
component actually renders:
<app-nav-bar />
<router-outlet />
Navigate to /projects/42 and Angular matches the second route, renders
ProjectDetail into that outlet, and makes 42 available as a
route parameter — which is exactly what Section 2 covers.
2. Route Parameters, Query Parameters & Fragments
A segment prefixed with : in a path — like :projectId above —
is a route parameter. The modern way to read it is to bind it straight to a component
input, no injected service required:
import { provideRouter, withComponentInputBinding } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
],
};
import { Component, input } from '@angular/core';
@Component({
selector: 'app-project-detail',
templateUrl: './project-detail.html',
})
export class ProjectDetail {
// Automatically bound from the :projectId route parameter
projectId = input.required<string>();
}
Query parameters (?sort=name) and the fragment (#reviews)
aren't bound this way — they're read from ActivatedRoute directly, since
they're optional and don't map to a fixed input the way a required path segment does:
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
export class ProductList {
private route = inject(ActivatedRoute);
sortBy = toSignal(
this.route.queryParamMap.pipe(map((params) => params.get('sort') ?? 'name')),
{ initialValue: 'name' }
);
}
3. Router Outlets & Nested/Child Routes
A route can have children — each with its own path segment, rendered into
a <router-outlet> that lives inside the parent route's own
component template. This is how a URL like
/projects/42/tasks/7 maps to three components nested inside each other.
export const routes: Routes = [
{
path: 'projects/:projectId',
component: ProjectDetail,
children: [
{ path: '', component: ProjectOverview },
{ path: 'tasks/:taskId', component: TaskDetail },
],
},
];
<h1>Project {{ projectId() }}</h1>
<nav> ...project-level tabs... </nav>
<!-- Child route content renders here -->
<router-outlet />
Notice ProjectDetail needs its own <router-outlet> for
its children to render at all — the app's top-level outlet only handles the top-level
route match.
5. Route Data & Title Strategies
A route can carry static data — useful for things like required
permissions, or flags a guard (Week 11) will check — and Angular can set the browser tab
title per route automatically, without a component needing to do it manually.
export const routes: Routes = [
{
path: 'projects/:projectId',
component: ProjectDetail,
title: 'Project Details', // sets document.title on navigation
data: { requiresRole: 'member' },
},
];
title can also be a function (a ResolveFn) when the title
needs to be dynamic — for example, including the project's actual name once it's
loaded, rather than a static string.
6. Handling 404s and Wildcard Routes
A route with path ** matches anything not matched by an earlier route —
always place it last in your routes array, since the router matches
top to bottom and stops at the first match.
export const routes: Routes = [
{ path: '', component: DashboardOverview },
{ path: 'projects/:projectId', component: ProjectDetail },
{ path: '**', component: NotFound }, // catches everything else
];
If { path: '**' } were placed first, it would swallow every navigation before the router ever reached your real routes — nothing after it would ever match. This is the single most common routing bug for anyone new to Angular's router.
7. Hands-on Exercise
Build a three-level nested routing dashboard
Wire up overview → project → task-detail routing, with real navigation between all three.
Requirements:
- A
DashboardOverviewcomponent at/, listing several fake projects withrouterLinks to each. - A
ProjectDetailcomponent at/projects/:projectId, with its own<router-outlet>and child routes for a default overview andtasks/:taskId. - A
TaskDetailcomponent that readstaskIdvia component-input binding, and a "Back to project" link using a relativerouterLink. - Set a per-route
titlefor all three levels, and confirm the browser tab updates correctly as you navigate. - Add a
**wildcard route rendering a simple "Page not found" component, positioned last, and confirm an invalid URL hits it.
If the child routes never render, double-check ProjectDetail's own template actually has a <router-outlet /> in it — the app's top-level outlet only ever renders the top-level match.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What does withComponentInputBinding() actually change about how you read route parameters?
What does withComponentInputBinding() actually change about how you read route parameters?
Without it, a component has to inject ActivatedRoute and read parameters out of an Observable map. With it enabled, a route parameter named :projectId is bound directly to a component input named projectId automatically — no injected service or subscription needed for that specific value.
Q2
Why are query parameters read via ActivatedRoute rather than component-input binding, unlike path parameters?
Why are query parameters read via ActivatedRoute rather than component-input binding, unlike path parameters?
Query parameters are optional and not declared anywhere in the route configuration the way :projectId is — there's no fixed list of "the query params this route has" for Angular to bind automatically. They're read reactively from ActivatedRoute.queryParamMap instead, which naturally handles params that may or may not be present.
Q3
Why does a parent route with children need its own <router-outlet>?
Why does a parent route with children need its own <router-outlet>?
Each level of nested routing needs a place to render its own matched child — the app's top-level outlet only renders the top-level route's component. A child route's component renders into whatever outlet exists inside its parent's template, so without one there, the child simply has nowhere to appear.
Q4
You move the { path: '**' } route to the top of the routes array. What breaks?
You move the { path: '**' } route to the top of the routes array. What breaks?
Every navigation, because the router checks routes in array order and stops at the first match. ** matches literally any path, so placed first it would immediately catch every URL and render the not-found component — none of the real routes below it would ever be reached.