Many pages depend on the URL: /products/3 shows product 3, and /products?sort=price sorts a list. In this lesson you build a list page and a detail page and read values from the URL.
What you will learn
- How to define a route with a parameter such as
:id - How to read it with component input binding
- How to use query parameters and navigate from code
A small product service
// src/app/products/product-data.ts
export interface Product {
id: number;
name: string;
price: number;
}
export const PRODUCTS: Product[] = [
{ id: 1, name: 'Notebook', price: 5 },
{ id: 2, name: 'Backpack', price: 40 },
{ id: 3, name: 'Desk lamp', price: 25 },
];
The list and detail pages
// src/app/products/product-list.ts
import { Component, computed, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { PRODUCTS } from './product-data';
@Component({
selector: 'app-product-list',
imports: [RouterLink],
template: `
<h2>Products</h2>
<p>
Sort:
<a routerLink="/products" [queryParams]="{ sort: 'name' }">by name</a> |
<a routerLink="/products" [queryParams]="{ sort: 'price' }">by price</a>
</p>
<ul>
@for (p of items(); track p.id) {
<li><a [routerLink]="['/products', p.id]">{{ p.name }}</a> ({{ p.price }} USD)</li>
}
</ul>
`,
})
export class ProductList {
// Filled from the query string: /products?sort=price
readonly sort = input<string>();
protected readonly items = computed(() => {
const list = [...PRODUCTS];
return this.sort() === 'price'
? list.sort((a, b) => a.price - b.price)
: list.sort((a, b) => a.name.localeCompare(b.name));
});
}
// src/app/products/product-detail.ts
import { Component, computed, inject, input } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { PRODUCTS } from './product-data';
@Component({
selector: 'app-product-detail',
imports: [RouterLink],
template: `
@if (product(); as p) {
<h2>{{ p.name }}</h2>
<p>Price: {{ p.price }} USD</p>
<button type="button" (click)="next()">Next product</button>
} @else {
<h2>Product not found</h2>
}
<p><a routerLink="/products">Back to the list</a></p>
`,
})
export class ProductDetail {
private readonly router = inject(Router);
// Filled from the path: /products/:id
readonly id = input.required<string>();
protected readonly product = computed(() =>
PRODUCTS.find((p) => p.id === Number(this.id())),
);
protected next(): void {
const current = Number(this.id());
const nextId = current >= PRODUCTS.length ? 1 : current + 1;
this.router.navigate(['/products', nextId]);
}
}
Routes and router setup
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { Home } from './pages/home';
import { About } from './pages/about';
import { NotFound } from './pages/not-found';
import { ProductList } from './products/product-list';
import { ProductDetail } from './products/product-detail';
export const routes: Routes = [
{ path: '', component: Home, title: 'Home' },
{ path: 'about', component: About, title: 'About' },
{ path: 'products', component: ProductList, title: 'Products' },
{ path: 'products/:id', component: ProductDetail, title: 'Product details' },
{ path: '**', component: NotFound, title: 'Not found' },
];
// src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withComponentInputBinding()),
],
};
<!-- src/app/app.html -->
<header>
<nav aria-label="Main">
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
<a routerLink="/products" routerLinkActive="active">Products</a>
<a routerLink="/about" routerLinkActive="active">About</a>
</nav>
</header>
<main>
<router-outlet />
</main>
The key parts
path: 'products/:id'defines a parameter. The colon marksidas a variable part of the URL.withComponentInputBinding()sends route parameters and query parameters into component inputs with the same name. NoActivatedRoutecode is needed for simple cases.- Route parameters are always strings, so convert with
Number(...)when you need a number. [routerLink]="['/products', p.id]"builds the URL/products/1from an array of segments.[queryParams]="{ sort: 'price' }"adds?sort=price. Query parameters are optional, so useinput<string>()and handleundefined.router.navigate([...])changes the page from code.
Try it yourself
- Add a
categoryquery parameter and filter the list. - Show a friendly message when the id in the URL does not match any product.
- Add a "Previous product" button next to "Next product".
Quick check
- How do you declare a route parameter?
- What type is a route parameter value?
- What does
withComponentInputBinding()do?
Next, continue with Angular Router Navigation: ActivatedRoute, Query Params, Route Data and Router Events.