Retourner à : Angular en 2025
Lors du chapitre précédent, nous avons vu comment créer et utiliser des services. Dans ce nouveau chapitre, nous allons voir comment gérer des routes qui nous permettrons d’afficher des composants spécifiques, dépendant de l’URL à afficher. Pour cela on va :
- préparer notre code en créant de nouveaux composant qu’on utilisera tout le long de ce chapitre
- voir comment créer les routes et comment les associer à un composant
- regardercomment passer des paramètres dans nos routes et comment les lire dans un composant
- pour finir, nous verrons plusieurs méthodes qui permettent de rediriger notre utilisateur vers une page
Pour rappel, tout au long de ce cours, on va créer une application de gestion de collections de timbres, pièces, figurines, cartes à jouer ou autre.

Préparation du code
Pour commencer, avant d’expliquer comment fonctionne la navigation en Angular, restructurons notre projet en créant un dossier qu’on va appeler pages et qui va contenir les différents composants qu’on veut associer à des URL. Dans ce dossier pages, je veux créer 3 composants: un composant collection-detail, un composant collection-item-detail et un composant not-found, comme démontré:
ng g c pages/collection-detail
ng g c pages/collection-item-detail
ng g c pages/not-foundOn va garder le contenu par défaut pour les pages collection-item-detail et not-found pour le moment. En ce qui concerne la page collection-detail, on va y recopier le code que nous avons tapé dans notre composant App dans les cours précédents:
import { ChangeDetectionStrategy, Component, computed, inject, model, signal } from '@angular/core';
import { SearchBar } from "../../components/search-bar/search-bar";
import { Collection } from '../../models/collection';
import { CollectionService } from '../../services/collection-service';
import { CollectionItemCard } from '../../components/collection-item-card/collection-item-card';
import { CollectionItem } from '../../models/collection-item';
@Component({
selector: 'app-collection-detail',
imports: [CollectionItemCard, SearchBar],
templateUrl: './collection-detail.html',
styleUrl: './collection-detail.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CollectionDetail {
collectionService = inject(CollectionService);
count = 0;
search = model('');
collection!: Collection;
coin!: CollectionItem;
linx!: CollectionItem;
stamp!: CollectionItem;
selectedCollection = signal<Collection | null>(null);
displayedItems = computed(() => {
const allItems = this.selectedCollection()?.items || [];
return allItems.filter(item =>
item.name.toLowerCase().includes(
(this.search() || '').toLocaleLowerCase()
)
);
});
constructor() {
const allCollections = this.collectionService.getAll();
if (allCollections.length > 0) {
this.selectedCollection.set(allCollections[0]);
}
}
addGenericItem() {
const genericItem = new CollectionItem();
const collection = this.selectedCollection();
if (!collection) return;
const updatedCollection =this.collectionService.addItem(
collection, genericItem
);
this.selectedCollection.set(updatedCollection);
}
}Puis, dans le fichier html, on aura :
<header id="collection-header">
<h1>{{selectedCollection()?.title}}</h1>
<div>
<app-search-bar
[(search)]="search"
>
</app-search-bar>
</div>
</header>
<section class="collection-grid">
@for (item of displayedItems(); track item.name) {
@switch (item.rarity) {
@case ("Legendary") {
<div>
<app-collection-item-card [item]="item"></app-collection-item-card>
<hr class="gold">
</div>
}
@case ("Rare") {
<div>
<app-collection-item-card [item]="item"></app-collection-item-card>
<hr class="dashed">
</div>
}
@default {
<app-collection-item-card [item]="item"></app-collection-item-card>
}
}
}
</section>
@let displayedItemsCount = displayedItems().length;
@if(displayedItemsCount > 0) {
<div class="centered">{{displayedItemsCount}} objet(s) affiché(s).</div>
} @else {
<div class="centered">Aucun résultat.</div>
}
<div class="centered">
<button (click)="addGenericItem()">Ajouter Objet</button>
</div>Pour finir, on va rajouter le CSS au fichier collection-detail.scss :
#collection-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 1rem;
}
.collection-grid {
display: flex;
width: 100%;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
}
.centered {
margin-top: 1rem;
text-align: center;
}
hr.gold {
border-color: goldenrod;
}
hr.dashed {
border-style: dashed;
}N’oublions pas d’enlever ce code de notre composant App. Les fichiers app.html et app.scss doivent maintenant être vides, et notre fichier app.ts doit ressembler à cela :
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {
}Les routes
Maintenant que nos 3 pages sont prêtes, il est temps de définir nos URLs. Pour cela, nous allons nous rendre dans le fichier app.routes.ts, où on retrouve le code suivant :
import { Routes } from '@angular/router';
export const routes: Routes = [];Nous y trouvons, pour l’instant, un tableau routes qui est vide. Eh bien, c’est dans ce tableau que nous allons définir nos routes.
Pour cela, nous allons indiquer quel composant afficher pour quelle route en ajoutant un dictionnaire au tableau routes, pour chaque route que nous souhaitons créer. Ce dictionnaire va avoir deux paramètres, un paramètre path qui va contenir le chemin d’accès que nous souhaitons configurer, et un paramètre component, qui va indiquer le composant à afficher.
Dans notre cas, nous allons définir 2 routes: une route « home » qui va afficher notre collection par défaut, et une route item, à travers laquelle on pourra créer ou afficher un objet de collection spécifique:
import { Routes } from '@angular/router';
import { CollectionDetail } from './pages/collection-detail/collection-detail';
import { CollectionItemDetail } from './pages/collection-item-detail/collection-item-detail';
export const routes: Routes = [{
path: 'home',
component: CollectionDetail
}, {
path: 'item',
component: CollectionItemDetail
}];Là, si vous ouvrez votre navigateur à l’URL http://localhost:4200/home, vous verrez que rien ne s’affiche. C’est normal, car nous devons indiquer dans notre fichier app.html, l’endroit où nous voulons afficher les routes en question en ajoutant le tag <router-outlet> à notre html:
<router-outlet></router-outlet>En plus de cela, nous devons importer RouterOutlet dans notre fichier app.ts :
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from "@angular/router";
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet]
})
export class App {
}Maintenant, si vous ouvrez votre navigateur aux URLs /home ou /item, vous voyez que les deux composants s’affichent bien.

La page item ressemble à ceci :

On vient de voir comment mapper une URL sur un composant précis. Maintenant j’aimerais qu’on regarde aussi comment effectuer des redirections. Par exemple, j’aimerais que l’URL racine redirige l’utilisateur sur la page home. Pour cela, on peut faire la chose suivante:
import { Routes } from '@angular/router';
import { CollectionDetail } from './pages/collection-detail/collection-detail';
import { CollectionItemDetail } from './pages/collection-item-detail/collection-item-detail';
export const routes: Routes = [{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}, {
path: 'home',
component: CollectionDetail
}, {
path: 'item',
component: CollectionItemDetail
}];
Ici on a défini un nouveau path à une chaine de caractères vide, et au lieu de définir un paramètre component, on utilise le paramètre redirectTo, qui indique l’URL vers laquelle on souhaite rediriger l’utilisateur. Nous avons aussi renseigné le paramètre pathMatch à full, qui indique à Angular de prendre l’adresse complète et de la matcher avec notre chemin d’accès.
Attention : Angular évalue les routes dans l’ordre où elles sont définies. Dès que l’une d’entre elles correspond, Angular l’utilise et s’arrête là.
Sans pathMatch: ‘full’, la route avec un path vide utilise la stratégie par défaut « prefix ». Avec cette stratégie, Angular considère que la chaine de caractères vides match toutes les routes, et les autres routes ne seraient donc jamais évaluées.
J’aimerais maintenant qu’on crée une autre route, qui indique à Angular que pour toutes les routes qu’on n’a pas spécifiées, on souhaite afficher notre page not-found. Pour cela, on peut utiliser en tant que path, deux étoiles ‘**’ qui vont matcher n’importe quelle URL:
import { Routes } from '@angular/router';
import { CollectionDetail } from './pages/collection-detail/collection-detail';
import { CollectionItemDetail } from './pages/collection-item-detail/collection-item-detail';
import { NotFound } from './pages/not-found/not-found';
export const routes: Routes = [{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}, {
path: 'home',
component: CollectionDetail
}, {
path: 'item',
component: CollectionItemDetail
}, {
path: '**',
component: NotFound
}];
Maintenant si on navigue sur page qui n’existe pas, par exemple http://localhost:4200/abc on obtient le résultat suivant :

Pour aller plus loin, je veux pouvoir accéder à notre URL /item de deux manières différentes. La première méthode sera via l’URL /item, comme on l’a fait jusqu’à maintenant. Cette URL servira à créer un nouvel objet de collection. Alternativement, on doit aussi pouvoir afficher les détails d’un objet spécifique, en renseignant un identifiant d’un objet de collection à afficher/modifier dans notre URL.
Donc, en plus de notre URL /item, il nous faut une autre règle qui puisse matcher une URL qui commence par /item/ , suivi d’un identifiant qui dépend de l’objet à afficher/modifier.
Eh bien, en Angular, c’est très simple de définir une URL avec un paramètre de route. Il suffit de placer dans le segment de l’URL où l’on souhaite avoir un paramètre, un double point, suivi du nom à donner à ce paramètre de route. Ici, on peut donc définir le path ‘item/:id’ de la manière suivante:
import { Routes } from '@angular/router';
import { CollectionDetail } from './pages/collection-detail/collection-detail';
import { CollectionItemDetail } from './pages/collection-item-detail/collection-item-detail';
import { NotFound } from './pages/not-found/not-found';
export const routes: Routes = [{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}, {
path: 'home',
component: CollectionDetail
}, {
path: 'item',
component: CollectionItemDetail
}, {
path: 'item/:id',
component: CollectionItemDetail
}, {
path: '**',
component: NotFound
}];Pour plus de clarté, on peut aussi regrouper les routes enfants en dessous de la route parent grâce au paramètre children:
import { Routes } from '@angular/router';
import { CollectionDetail } from './pages/collection-detail/collection-detail';
import { CollectionItemDetail } from './pages/collection-item-detail/collection-item-detail';
import { NotFound } from './pages/not-found/not-found';
export const routes: Routes = [{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}, {
path: 'home',
component: CollectionDetail
}, {
path: 'item',
children: [{
path: '',
component: CollectionItemDetail
}, {
path: ':id',
component: CollectionItemDetail
}]
}, {
path: '**',
component: NotFound
}];En plus d’être plus claire, cette notation a aussi l’avantage que si un jour on souhaite changer le nom de la route item en autre chose, par exemple collection-item, on aura un seul endroit à modifier.
Maintenant, ouvrons notre composant CollectionItemDetail et regardons comment vérifier, si oui ou non, l’utilisateur a passé un identifiant dans l’URL et, si oui, lequel. Pour cela, nous allons ouvrir notre fichier collection-item-detail.ts et y injecter ActivatedRoute qu’on va importer de @angular/route.
Nous avons trois manières d’accéder aux paramètres de routes. La première méthode que je vais vous montrer consiste à lire la valeur des paramètres de routes à un instant précis, en prenant un snapshot des paramètres:
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-collection-item-detail',
imports: [],
templateUrl: './collection-item-detail.html',
styleUrl: './collection-item-detail.scss'
})
export class CollectionItemDetail implements OnInit {
private route = inject(ActivatedRoute);
itemId = signal<number | null>(null);
ngOnInit(): void {
const params = this.route.snapshot.params;
const selectedId = params['id'] ? parseInt(params['id']) : null;
this.itemId.set(selectedId);
}
}Adaptons encore le fichier collection-item-detail.html afin d’afficher l’identifiant du monstre si jamais il a bien été passé en paramètre:
@if (itemId()) {
<p>Item {{ itemId() }}!</p>
} @else {
<p>New Item!</p>
}Si vous vous rendez maintenant à l’URL /item vous voyez qu’on a le texte « New item! » qui s’affiche.

Et si vous naviguez vers l’URL /item/2 , vous voyez bien le texte « Item 2! » à l’écran.

Là, on pourrait ce dire que le tour est joué. Nous pouvons récupérer le paramètre, charger l’objet à afficher et le compte est bon. Alors oui et non, tout va dépendre de notre « use case ». Imaginons que sur la page de détail d’un objet, nous voulions avoir un bouton qui permette de naviguer vers l’objet suivant, sans devoir revenir à la liste principale. Ajoutons un tel bouton à notre HTML:
@if (itemId()) {
<p>Item {{ itemId() }}!</p>
<button (click)="next()">Next</button>
} @else {
<p>New Item!</p>
}Implémentons également la fonction next. Pour cela, on va utiliser le Router qu’on va importer de @angular/router et qu’on va injecter dans notre composant. On peut ensuite utiliser this.router.navigate afin de naviguer vers une URL de la manière suivante :
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-collection-item-detail',
imports: [],
templateUrl: './collection-item-detail.html',
styleUrl: './collection-item-detail.scss'
})
export class CollectionItemDetail implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
itemId = signal<number | null>(null);
ngOnInit(): void {
const params = this.route.snapshot.params;
const selectedId = params['id'] ? parseInt(params['id']) : null;
this.itemId.set(selectedId);
}
next() {
const nextId = (this.itemId() || 0) + 1;
this.router.navigate(['item', nextId]);
}
}La méthode navigate du router prend un tableau qui représente les différents segments de l’URL vers laquelle on veut naviguer. Donc ici le tableau [‘item’, nextId] représente l’URL item/<nextId>.
Là, si vous allez à l’URL /item/1 et que vous appuyez sur le bouton next, vous voyez que le navigateur change bien l’URL en /item/2, mais l’affichage ne change pas et on a encore le texte Item 1! à l’écran.

Ceci est normal, car Angular détecte que nous sommes toujours à la même URL /item/:id et que le composant affiché est toujours le bon pour cette URL, et ne doit donc pas être rechargé. Ceci a pour effet que la méthode ngOnInit n’est pas ré-éxecutée et nous ne récupérerons donc pas la nouvelle valeur de notre route.
Si on veut pouvoir gérer ce genre de cas, où une route peut changer dynamiquement sans quitter le composant actuellement affiché, nous devons utiliser l’Observable this.route.params et nous y abonner en faisant un subscribe à celui-ci.
A partir de là, à chaque fois que l’URL changera, la fonction que nous passerons à la méthode « subscribe » sera exécutée, et nous pourrons donc être notifié de chaque changement d’URL. A noter que pour chaque subscribe, on doit aussi faire un unsubscribe, afin de ne pas se retrouver avec des fuites mémoire. Ici, nous devons donc faire un unsubscribe lors du life-cycle hook OnDestroy:
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-collection-item-detail',
imports: [],
templateUrl: './collection-item-detail.html',
styleUrl: './collection-item-detail.scss'
})
export class CollectionItemDetail implements OnInit, OnDestroy {
private route = inject(ActivatedRoute);
private router = inject(Router);
itemId = signal<number | null>(null);
routeParamSubscription: Subscription | null = null;
ngOnInit(): void {
this.routeParamSubscription = this.route.params.subscribe(params => {
const selectedId = params['id'] ? parseInt(params['id']) : null;
this.itemId.set(selectedId);
})
}
next() {
const nextId = (this.itemId() || 0) + 1;
this.router.navigate(['item', nextId]);
}
ngOnDestroy(): void {
if (this.routeParamSubscription) {
this.routeParamSubscription.unsubscribe();
}
}
}Avant de continuer : si vous ne savez pas ce que sont les Observables, ne vous en faite pas, j’ai des vidéos dédiés sur ce sujet sur la chaine.
Pour l’instant il vous suffit de retenir qu’un Observable retourne un flux de données auquel on peut s’abonner avec la méthode subscribe. A chaque nouvelle valeur la fonction qu’on passe en paramètre à subscribe est exécutée. Et il faut toujours se désabonner avec un unsubscribe quand on ne souhaite plus être notifié de nouvelles valeurs émises par l’Observable.
Maintenant, si vous retournez dans votre navigateur à l’URL /item/1 et que vous appuyez sur le bouton next, vous voyez que le texte affiché à l’écran a bien été mis à jour avec Item 2!. Vous pouvez maintenant appuyer sur le bouton next autant de fois que vous le voulez et votre page se mettra bien à jour!

Il existe un moyen plus simple d’accéder à la valeur d’un paramètre de route, tout en étant notifié de tout changement à celui-ci. En effet. on peut également configurer le router afin qu’il lie automatiquement les paramètres de routes avec les inputs de même nom des composants.
Pour cela nous devons ajouter le paramètre withComponentInputBinding() qu’on importe de @angular/router et qu’on passe en deuxième paramètre à provideRouter dans le fichier app.config.ts.
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes, withComponentInputBinding()),
]
};
Maintenant on peut adapter notre fichier collection-item-detail.ts de la manière suivante :
import { Component, inject, input } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-collection-item-detail',
imports: [],
templateUrl: './collection-item-detail.html',
styleUrl: './collection-item-detail.scss'
})
export class CollectionItemDetail {
private router = inject(Router);
id = input<string | null>(null);
next() {
const nextId = parseInt(this.id() || "0") + 1;
this.router.navigate(['item', nextId]);
}
}On peut confirmer que le tout fonctionne encore comme avant :

Ici, nous avons dû renommer itemId en id, afin que le nom de notre input coincide avec le nom du paramètre de notre route. Notons également que le paramètre ainsi obtenu est de type string. et que nous devons donc le convertir en entier dans notre fonction next avant de l’incrémenter.
Le fichier collection-item-detail.html doit être adapté comme ceci :
@if (id()) {
<p>Item {{ id() }}!</p>
<button (click)="next()">Next</button>
} @else {
<p>New Item!</p>
}Comme on l’a vu dans notre chapitre sur les inputs, on peut paramétrer notre input, afin de faire en sorte que le nom de notre variable soit différent du nom de l’input, et en plus on peut également appliquer des transformations sur l’input. Donc si nous voulons stocker notre input dans une variable itemId de type number, on peut faire la chose suivante :
import { Component, inject, input } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-collection-item-detail',
imports: [],
templateUrl: './collection-item-detail.html',
styleUrl: './collection-item-detail.scss'
})
export class CollectionItemDetail {
private router = inject(Router);
itemId = input<number | null, string | null>(null, {
alias: 'id',
transform: ((id: string | null) => id ? parseInt(id) : null)
});
next() {
const nextId = (this.itemId() || 0) + 1;
this.router.navigate(['item', nextId]);
}
}Et nous pouvons donc également réadapter notre fichier html :
@if (itemId()) {
<p>Item {{ itemId() }}!</p>
<button (click)="next()">Next</button>
} @else {
<p>New Item!</p>
}Nous pouvons retourner dans notre navigateur et confirmer que tout fonctionne encore :

Une dernière chose que j’aimerai vous montrer, est que vous pouvez aussi naviguer vers une route en utilisant le paramètre [routerLink] mis à disposition par Angular dans vos balises HTML et en lui passant les segments de routes en paramètre. Nous pouvons donc remplacer l’évènement click par :
@let id = itemId();
@if (id) {
<p>Item {{ id }}!</p>
<button [routerLink]="['/item', id + 1]">Next</button>
} @else {
<p>New Item!</p>
}Nous devons aussi importer RouterLink dans le fichier TypeScript et on n’a plus besoin de la fonction next :
import { Component, inject, input } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
@Component({
selector: 'app-collection-item-detail',
imports: [RouterLink],
templateUrl: './collection-item-detail.html',
styleUrl: './collection-item-detail.scss'
})
export class CollectionItemDetail {
private router = inject(Router);
itemId = input<number | null, string | null>(null, {
alias: 'id',
transform: ((id: string | null) => id ? parseInt(id) : null)
});
}Si on retest le code, on voit que tout fonctionne comme avant :

Et voilà, nous avons fait le tour des bases de la navigation. Bien entendu, ce sujet est bien plus vaste, et il y aurait encore pleins d’autres choses qu’on pourrait aborder, mais nous allons nous arrêter là pour cette introduction.
Resources
Vous retrouvez ci-dessous un lien vers le code source utilisé dans ce chapitre. Ce lien est uniquement disponible pour les abonnés Standard et Premium.
Quiz
Répondez au quiz ci-dessous afin de vérifier que vous avez bien compris le contenu de cette leçon. Les quiz sont uniquement disponibles pour les abonnés Standard et Premium.