Retourner à : Angular 18 pour débutants
Lors du chapitre précédent de nos cours Angular, nous avons vu comment créer et utiliser des services, et 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 voir:
- comment structurer notre projet afin de gérer au mieux ces différentes URLs ;
- comment créer les routes et les comment les associer à un composant ;
- nous verrons ensuite comment utiliser le router afin d’afficher la bonne route à l’écran ;
- on regardera aussi comment 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, ce chapitre s’inscrit dans une longue lignée de posts, dont le fil rouge est la création d’une application de visualisation et gestion de cartes à collectionner de type Pokémon, Magic, Yu-Gi-Oh! ou autres.

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 « monster-list », un composant « monster » et un composant « not-found », comme démontré:
ng g c pages/monster-list
ng g c pages/monster
ng g c pages/not-found
On va garder le contenu par défaut pour les pages « monster » et « not found » pour le moment. En ce qui concerne la page « monster-list », on va y recopier le code que nous avons tapé dans notre composant « AppComponent » dans les cours précédents:
Commençons par le fichier « monster-list.component.ts »:
import { Component, computed, inject, model, signal } from '@angular/core';
import { Monster } from '../../models/monster.model';
import { MonsterService } from '../../services/monster/monster.service';
import { CommonModule } from '@angular/common';
import { PlayingCardComponent } from '../../components/playing-card/playing-card.component';
import { SearchBarComponent } from '../../components/search-bar/search-bar.component';
@Component({
selector: 'app-monster-list',
standalone: true,
imports: [CommonModule, PlayingCardComponent, SearchBarComponent],
templateUrl: './monster-list.component.html',
styleUrl: './monster-list.component.css'
})
export class MonsterListComponent {
monsterService = inject(MonsterService);
monsters = signal<Monster[]>([]);
search = model('');
filteredMonsters = computed(() => {
return this.monsters().filter(monster => monster.name.includes(this.search()));
})
constructor() {
this.monsters.set(this.monsterService.getAll());
}
addGenericMonster() {
const monster = new Monster();
this.monsterService.add(monster);
this.monsters.set(this.monsterService.getAll());
}
}
Puis, dans le fichier « monster-list.component.html », on aura:
<app-search-bar [(search)]="search"></app-search-bar>
<div id="card-list">
@for (monster of filteredMonsters(); track monster.id) {
<app-playing-card [monster]="monster"/>
} @empty {
<div class="centered">No monsters found !</div>
}
</div>
@if (filteredMonsters().length > 0) {
<div class="centered">Found {{filteredMonsters().length}} monsters !</div>
}
<div class="centered"><button (click)="addGenericMonster()">Add Generic Monster</button></div><br/><br/>
Pour finir, on va rajouter le « css » au fichier « monster-list.component.css »:
#card-list {
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.centered {
margin-top: 40px;
text-align: center;
}
N’oubliez pas d’enlever ce code de votre « AppComponent ». Votre fichier « app.component.html » et « app.component.css » devraient maintenant se retrouver vides, et votre fichier « app.component.ts » devrait ressembler à ça:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
}
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ù vous verrez 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 liste de monstres, et une route « monster » à travers laquelle on pourra créer ou afficher un monstre spécifique:
import { Routes } from '@angular/router';
import { MonsterListComponent } from './pages/monster-list/monster-list.component';
import { MonsterComponent } from './pages/monster/monster.component';
export const routes: Routes = [{
path: 'home',
component: MonsterListComponent
}, {
path: 'monster',
component: MonsterComponent
}];
Là, si vous ouvrez votre navigateur à l’URL « localhost:4200/home », vous verrez que rien ne s’affiche. Cela est normal, car nous devons indiquer dans notre fichier « app.component.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.component.ts »:
import { Component } from '@angular/core';
import { RouterOutlet } from "@angular/router";
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
}
Maintenant, si vous ouvrez votre navigateur aux URLs « /home » ou « /monster », vous voyez que les deux composants s’affichent bien.

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 { MonsterListComponent } from './pages/monster-list/monster-list.component';
import { MonsterComponent } from './pages/monster/monster.component';
export const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'home',
component: MonsterListComponent
},
{
path: 'monster',
component: MonsterComponent
}
];
Comme vous le voyez ici, on n’a pas un paramètre « component », mais un 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.
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 { MonsterListComponent } from './pages/monster-list/monster-list.component';
import { MonsterComponent } from './pages/monster/monster.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';
export const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'home',
component: MonsterListComponent
},
{
path: 'monster',
component: MonsterComponent
},
{
path: '**',
component: NotFoundComponent
}
];
Pour aller plus loin, je veux pouvoir accéder à notre URL « monster » de deux manières différentes. La première méthode sera via l’URL « monster », comme on l’a fait jusqu’à maintenant. Cette URL servira à créer un nouveau monstre. Alternativement, on doit aussi pouvoir afficher les détails d’un monstre spécifique, en renseignant un identifiant d’un monstre à afficher/modifier dans notre URL.
Donc, en plus de notre URL « monster », il nous faut une autre règle qui puisse matcher une URL qui commence par » /monster/ « , suivi d’un identifiant qui dépend du monstre à 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, puis un double point, suivi du nom à donner à ce paramètre de route. Ici, on peut donc définir le path ‘monster/:id’ et on se retrouve donc avec la configuration suivante:
import { Routes } from '@angular/router';
import { MonsterListComponent } from './pages/monster-list/monster-list.component';
import { MonsterComponent } from './pages/monster/monster.component';
export const routes: Routes = [{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},{
path: 'home',
component: MonsterListComponent
}, {
path: 'monster',
component: MonsterComponent
}, {
path: 'monster/:id',
component: MonsterComponent
}, {
path: '**',
component: NotFoundComponent
}];
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 { MonsterListComponent } from './pages/monster-list/monster-list.component';
import { MonsterComponent } from './pages/monster/monster.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';
export const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'home',
component: MonsterListComponent
},
{
path: 'monster',
children: [
{
path: '',
component: MonsterComponent
},
{
path: ':id',
component: MonsterComponent
}
]
},
{
path: '**',
component: NotFoundComponent
}
];
En plus d’être plus claire, cette notation a aussi l’avantage que si un jour on souhaite changer le nom de la route « monster » en autre chose, par exemple « creature », on aura un seul endroit à modifier. Maintenant, ouvrons notre composant « MonsterComponent » 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 « monster.component.ts » et y injecter « ActivatedRoute » qu’on va importer de « @angular/route ».
Nous avons deux 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-monster',
standalone: true,
imports: [],
templateUrl: './monster.component.html',
styleUrl: './monster.component.css'
})
export class MonsterComponent implements OnInit {
private route = inject(ActivatedRoute);
monsterId = signal<number | undefined>(undefined);
ngOnInit(): void {
const params = this.route.snapshot.params;
this.monsterId.set(params['id'] ? parseInt(params['id']) : undefined);
}
}
Adaptons encore le fichier « monster.component.html » afin d’afficher l’identifiant du monstre si jamais il a bien été passé en paramètre:
@if (monsterId()) {
<p>Monster {{monsterId()}}!</p>
} @else {
<p>New monster!</p>
}
Si vous vous rendez maintenant à l’URL « /monster » vous voyez qu’on a le texte « New monster! » qui s’affiche. Si vous naviguez vers l’URL « /monster/2 », vous voyez bien le texte « Monster 2! » à l’écran.

Donc là, vous vous dites sûrement que c’est parfait. Nous pouvons récupérer le paramètre, charger le monstre à afficher et le compte est bon. Alors oui et non, tout va dépendre de votre « use case ». Imaginons que sur la page de détail d’un monstre, vous vouliez avoir un bouton qui permette de naviguer vers le monstre suivant, sans devoir revenir à la liste principale. Ajoutons un tel bouton à notre HTML:
@if (monsterId()) {
<p>Monster {{monsterId()}}!</p>
<button (click)="next()">Next</button>
} @else {
<p>New monster!</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-monster',
standalone: true,
imports: [],
templateUrl: './monster.component.html',
styleUrl: './monster.component.css'
})
export class MonsterComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
monsterId = signal<number | undefined>(undefined);
ngOnInit(): void {
const params = this.route.snapshot.params;
this.monsterId.set(params['id'] ? parseInt(params['id']) : undefined);
}
next() {
let nextId = this.monsterId() || 0;
nextId++;
this.router.navigate(['monster/' + nextId]);
}
}
Là, si vous allez à l’URL « /monster/1 » et que vous appuyez sur le bouton « next », vous voyez que le navigateur change bien l’URL en « /monster/2 », mais l’affichage ne change pas et on a encore le texte « Monster 1 » à l’écran.

Ceci est normal, car Angular détecte que nous sommes toujours à la même URL « /monster/:id » et que le bon composant qui est affiché pour cette URL ne doit pas être rechargé. Ceci a pour effet que la méthode « ngOnInit » n’est pas re-éxecuté 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, nous devons donc faire un « unsubscribe » lors du life-cycle hook “OnDestroy”:
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-monster',
standalone: true,
imports: [],
templateUrl: './monster.component.html',
styleUrl: './monster.component.css'
})
export class MonsterComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
routeSubscription: Subscription | null = null;
monsterId = signal<number | undefined>(undefined);
ngOnInit(): void {
this.routeSubscription = this.route.params.subscribe(params => {
this.monsterId.set(params['id'] ? parseInt(params['id']) : undefined);
});
}
ngOnDestroy(): void {
this.routeSubscription?.unsubscribe();
}
next() {
let nextId = this.monsterId() || 0;
nextId++;
this.router.navigate(['monster/' + nextId]);
}
}
Maintenant, si vous retournez dans votre navigateur à l’URL « /monster/1 » et que vous appuyez sur le bouton « next », vous voyez que le texte affiché à l’écran a bien été mis à jour avec « Monster 2 ». Vous pouvez maintenant appuyer sur le bouton « next » autant de fois que vous le voulez et votre page se mettra bien à jour!

En plus de ce que nous venons de voir ici, il y a d’autres sujets que nous devrons aborder, comme par exemple l’utilisation de liens HTML pour naviguer d’une page à une autre, ou encore l’envoi de paramètres de requêtes à une URL, ainsi la lecture de ces paramètres. Mais on verra tout cela le moment dans des chapitres plus avancés.
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.