Le Routing moderne d’Angular est fonctionnel et lazy par défaut : les routes sont de simples objets, les pages se chargent avec loadComponent/loadChildren, et tout ce qui vient de l’URL (params, query, data, résultats des resolvers) va directement dans les input() du composant. Pas d’ActivatedRoute, pas de subscribe.
Configuration
export const routes: Routes = [
{ path: '', loadComponent: () => import('./features/home/home-page') }, // une page
{ path: 'products', loadChildren: () => import('./features/products/products.routes') }, // une fonctionnalité
{ path: 'old-products', redirectTo: 'products' },
{ path: '**', loadComponent: () => import('./core/layout/not-found'), title: 'Page introuvable' },
];
// app.config.ts
provideRouter(
routes,
withComponentInputBinding(), // params / query / data / resolve → input()
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }),
withViewTransitions(), // animations entre les pages (View Transitions API)
// withPreloading(PreloadAllModules),
);Comment le routeur choisit une route
Le routeur lit les routes de haut en bas et prend la première qui correspond à toute l’URL. Un segment :id correspond à n’importe quoi, ** à tout, et redirectTo relance la recherche avec la nouvelle URL. C’est pourquoi l’ordre des routes compte.
/books/new → NewBook
- ✗ '' pathMatch 'full', mais il reste 'books/new'
- ✗ 'books' le début correspond, mais il reste 'new'
- ✓ 'books/new' correspond à toute l’URL
Avec books/:id au-dessus de books/new, la page d’un nouveau livre ne peut jamais être atteinte : « new » devient un id. Les chemins précis d’abord, les paramètres ensuite, ** en dernier. Les deux erreurs de configuration ne vont même pas jusque-là : le routeur refuse les routes au démarrage de l’application, avec les mêmes messages NG04014 qu’ici.
// les chemins précis d’abord, les paramètres ensuite, le joker en dernier
export const routes: Routes = [
{ path: '', redirectTo: 'books', pathMatch: 'full' }, // '' + redirection : toujours pathMatch: 'full'
{ path: 'books', loadComponent: () => import('./books/book-list') },
{ path: 'books/new', loadComponent: () => import('./books/new-book') }, // avant :id, sinon 'new' devient un id
{ path: 'books/:id', loadComponent: () => import('./books/book-detail') },
{ path: 'books/:id/edit', loadComponent: () => import('./books/edit-book') },
{ path: 'about', loadComponent: () => import('./about') }, // pas de barre oblique au début de path
{ path: '**', loadComponent: () => import('./not-found') }, // en dernier : tout ce que personne n’a pris
];Paramètres en inputs (exemple interactif)
La démo ci-dessous utilise de vraies routes enfants de cette page : /routing/user/:id. Le fichier de routes du topic (routing.routes.ts) est chargé en lazy, et le composant enfant reçoit tout via input().
Choisissez un utilisateur à gauche. L’URL change, et le composant enfant reçoit :id en tant qu’input.
@Component({ /* ... */ })
export class UserDetail {
readonly id = input.required({ transform: numberAttribute }); // :id (path param)
readonly tab = input<string>(); // ?tab= (query param)
readonly section = input<string>(); // data.section
readonly area = input<string>(); // data.area, hérité du PARENT
readonly user = httpResource<UserDto>(() => `/api/users/${this.id()}`);
}Héritage des paramètres : changement en v22 v22
// v22 (breaking) : paramsInheritanceStrategy vaut désormais 'always' par défaut
// → les routes enfants héritent de params, data et resolve de TOUS les parents.
{
path: 'orgs/:orgId',
data: { area: 'admin' },
children: [
{ path: 'users/:userId', component: OrgUser }, // OrgUser reçoit orgId + userId + area
],
}
// L’ancien comportement (n’hériter que des parents sans composant / à chemin vide) :
provideRouter(routes, withRouterConfig({ paramsInheritanceStrategy: 'emptyOnly' }));Si une route enfant a un paramètre du même nom que son parent, ou si vous comptiez sur le fait qu’un enfant NE voie PAS le data de son parent, vérifiez après la mise à jour. Pour retrouver l’ancien comportement : paramsInheritanceStrategy: 'emptyOnly'.
Navigation et liens
<a routerLink="/products">Produits</a>
<a [routerLink]="['/products', product().id]">Détails</a>
<a [routerLink]="['user', 2]" [queryParams]="{ tab: 'activitate' }">Relatif + query</a>
<a routerLink="/products" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Exactement active</a>
<router-outlet />Quand OUI, quand NON
- Un écran qui a besoin de son propre lien (partageable, favori, bouton retour).
- Détail d’une entité :
/products/42. - Onglets importants que l’on doit pouvoir ouvrir directement :
/settings/security. - Filtres et pagination partageables → query params.
- État d’interface éphémère : dropdown, survol, boîte de confirmation.
- Étapes d’assistant qui n’ont aucun sens ouvertes directement.
- Petits panneaux secondaires →
@if+ signal.
Pièges fréquents
- Vous oubliez
withComponentInputBinding()→ les inputs restentundefined. - Les params sont des strings : utilisez
transform: numberAttributepour les ids numériques. - Le composant n’est pas recréé lors d’une navigation
/user/1→/user/2(même route) : c’est pourquoi les inputs sont des signals, tout se recalcule tout seul. - Routes eager (
component: Xdansapp.routes.ts) pour de grosses pages → tout le code dans le bundle initial. Migration :ng g @angular/core:route-lazy-loading.