You already know routes and parameters. This lesson fills the gaps: moving around from code, reading everything the URL can carry, highlighting the active link and reacting to navigation.
What you will learn
RouterLinkandRouter.navigateActivatedRoute: params, query params, fragment and staticdataRouterLinkActivefor the active menu item- Router events and how to get the current route
Navigating with links and code
In a template use routerLink. In code inject Router:
// src/app/nav/nav.routes.ts
import { Routes } from '@angular/router';
import { ProductsPage } from './products-page';
import { ProductDetail } from './product-detail';
export const NAV_ROUTES: Routes = [
{ path: 'shop', component: ProductsPage, title: 'Shop', data: { section: 'catalog' } },
{ path: 'shop/:id', component: ProductDetail, title: 'Product', data: { section: 'catalog' } },
];
// src/app/nav/products-page.ts
import { Component, inject } from '@angular/core';
import { Router, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-products-page',
imports: [RouterLink, RouterLinkActive],
template: `
<nav>
<a routerLink="/shop" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">All</a>
<a [routerLink]="['/shop', 7]" routerLinkActive="active">Product 7</a>
</nav>
<button type="button" (click)="open(9)">Open 9 (from code)</button>
<button type="button" (click)="search('shoes')">Search shoes</button>
`,
})
export class ProductsPage {
private readonly router = inject(Router);
open(id: number) {
// same as /shop/9
this.router.navigate(['/shop', id]);
}
search(term: string) {
// /shop?q=shoes#results (query params and a fragment)
this.router.navigate(['/shop'], { queryParams: { q: term }, fragment: 'results' });
}
}
Points to remember:
- The array is path segments:
['/shop', id]becomes/shop/9. Do not build strings by hand. navigatereturns a Promise<boolean>. UsenavigateByUrl('/shop/9')when you already have a full URL string.routerLinkActiveadds a CSS class when the link matches the current URL. Addexact: trueto the home link, otherwise/is active for every page.- Relative navigation:
this.router.navigate(['edit'], { relativeTo: this.route }).
ActivatedRoute: reading the URL
ActivatedRoute describes the route that created your component. Its properties are Observables, because the same component can be reused when only the parameter changes (/shop/1 to /shop/2).
// src/app/nav/product-detail.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs';
@Component({
selector: 'app-product-detail',
template: `
<h2>Product {{ id() }}</h2>
<p>Section: {{ section }}</p>
<p>Search: {{ q() ?? 'none' }}</p>
`,
})
export class ProductDetail {
private readonly route = inject(ActivatedRoute);
id = toSignal(this.route.paramMap.pipe(map((p) => p.get('id'))));
q = toSignal(this.route.queryParamMap.pipe(map((p) => p.get('q'))));
section = this.route.snapshot.data['section']; // static data never changes
}
Which one do I use?
paramMap: the:idparts of the path.queryParamMap: the part after?.fragment: the part after#.data: fixed values you attach in the route config (a role, a layout name, a page title).snapshot: the value right now. Good for data that cannot change while the component lives.
### The simplest way: component input binding
If you enable withComponentInputBinding() in provideRouter, params, query params and data become normal signal inputs with the same name. No ActivatedRoute needed:
// File: with withComponentInputBinding()
id = input<string>(); // from :id
q = input<string>(); // from ?q=
section = input<string>(); // from data
Router events and the current route
The router tells you what is happening through router.events:
// src/app/nav/route-logger.ts
import { Component, DestroyRef, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { filter } from 'rxjs';
@Component({
selector: 'app-route-logger',
template: `
@if (loading()) { <p>Loading page...</p> }
<p>Current URL: {{ url() }}</p>
`,
})
export class RouteLogger {
private readonly router = inject(Router);
loading = signal(false);
url = signal(this.router.url);
constructor() {
this.router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
if (event instanceof NavigationStart) this.loading.set(true);
});
this.router.events
.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd), takeUntilDestroyed())
.subscribe((e) => {
this.loading.set(false);
this.url.set(e.urlAfterRedirects);
});
}
}
Useful events in order: NavigationStart, RoutesRecognized, NavigationEnd (success), NavigationCancel (a guard said no) and NavigationError.
How to get the current route? Use router.url for the URL string, route.snapshot for the params, or router.events filtered by NavigationEnd when you need to react to changes.
Test it
// src/app/nav/nav.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { NAV_ROUTES } from './nav.routes';
describe('navigation', () => {
beforeEach(() => TestBed.configureTestingModule({ providers: [provideRouter(NAV_ROUTES)] }));
it('reads the param, query param and route data', async () => {
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/shop/42?q=shoes');
const el = harness.routeNativeElement as HTMLElement;
expect(el.textContent).toContain('Product 42');
expect(el.textContent).toContain('Search: shoes');
expect(el.textContent).toContain('Section: catalog');
});
it('navigates from code', async () => {
const harness = await RouterTestingHarness.create('/shop');
const router = TestBed.inject((await import('@angular/router')).Router);
await router.navigate(['/shop', 9]);
expect(router.url).toBe('/shop/9');
});
});
Common mistakes
- Reading
snapshot.paramMaponce in a component that is reused for different ids. - Forgetting to unsubscribe from
router.events.takeUntilDestroyed()does it for you. - Missing
exact: trueon the home link. - Building URLs with string concatenation instead of the array form.
Try it yourself
- Add a link that keeps the current query params with
queryParamsHandling: 'merge'. - Show a small loading bar using
NavigationStartandNavigationEnd. - Move
sectiontowithComponentInputBinding()and deleteActivatedRoute.
Quick check
- What is the difference between
paramMapandqueryParamMap? - When is
snapshotenough? - Which event fires when a guard blocks navigation?
Next, continue with Angular Route Guards: Protect Pages with CanActivateFn.