Cours Angular · Module 4: Routing
Les paramètres de route - lire l'ordre inscrit dans l'adresse
Dans cette leçon7
À l'étape précédente, tu as tracé les routes du Tokaido, et parmi elles cette entrée : { path: 'samurai/:id', component: SamuraiDetailComponent }. Les deux-points placés devant id ont prévenu le routeur que ce tronçon de l'adresse est variable. Ouvrir /samurai/7 affiche l'écran de profil, ouvrir /samurai/128 affiche exactement le même écran - et c'est là que l'ennui commence. Le composant qui apparaît n'a pas la moindre idée du guerrier dont il est question. La route l'a conduit jusqu'à la bonne porte, puis s'est tue.
À l'Académie, la scène ressemble à ceci : un messager accourt depuis un poste de garde et te tend un parchemin. Le parchemin porte un sceau avec le numéro d'un guerrier, et dans la marge quelqu'un a griffonné des détails - comment trier la liste, par quelle page commencer. La route a livré le parchemin devant la porte, mais personne ne le lira à ta place. Ta tâche est de briser le sceau. Tu vas apprendre ici deux choses : lire l'adresse sur laquelle tu te tiens, et donner l'ordre de marche vers une autre adresse directement depuis le code de ta classe.
Le parchemin de la route active - ActivatedRoute
La description de la route actuellement ouverte, Angular la conserve dans un service nommé ActivatedRoute. Un service est un objet tout prêt que le framework fabrique lui-même et te remet sur demande - tu ne le crées pas avec l'opérateur new, tu le réclames. La demande se formule avec la fonction inject(), importée du paquet @angular/core : tu lui donnes le nom du service, elle te rend l'exemplaire prêt à l'emploi. Ce geste s'appelle l'injection de dépendances et tu le démonteras pièce par pièce dans le module consacré aux guildes d'artisans. Ici, une seule phrase te suffit : inject(ActivatedRoute) renvoie le parchemin qui décrit la route sur laquelle tu te tiens.
Retiens tout de suite ce qu'il ne sert à rien de chercher. Les noms RouteParams et NavigationService ont tout l'air de noms plausibles, mais ils n'existent tout simplement pas dans Angular - le paquet @angular/router n'exporte rien de tel et le compilateur rejettera un import pareil. Il existe en revanche un service Router, que tu découvriras dans la seconde moitié de la leçon, sauf que celui-là sert à se déplacer d'une route à l'autre, pas à lire la route courante. Pour lire les paramètres, il y a une seule adresse : ActivatedRoute.
1import { Component, inject } from '@angular/core';
2import { ActivatedRoute } from '@angular/router';
3
4@Component({
5 selector: 'app-samurai-detail',
6 standalone: true,
7 template: '<h1>Profil du samurai</h1>'
8})
9export class SamuraiDetailComponent {
10 private route = inject(ActivatedRoute);
11}Rien n'a changé à l'écran - le template affiche toujours le même titre, sans numéro. La seule chose qui a bougé, c'est que le champ route détient désormais le parchemin et que tu peux y regarder de plus près. Remarque deux détails. D'abord, il n'y a aucun constructeur ici : tu appelles inject() directement à la déclaration du champ, et Angular aura déjà préparé le service à ce moment-là. Ensuite, le mot private indique que le parchemin reste une affaire interne à la classe - le template n'y accède pas, et ne devrait pas y accéder.
L'instantané de la route - snapshot
La lecture la plus simple tient en un seul regard jeté sur le parchemin au moment où tu franchis la porte. ActivatedRoute possède un champ snapshot, c'est-à-dire un instantané : l'état de la route figé à l'instant où tu le demandes. Dans cet instantané se trouve paramMap, la carte des paramètres du chemin, un objet de type ParamMap qui expose une méthode get(nom). Tu lui passes le nom du paramètre sans les deux-points, donc 'id', et tu récupères la valeur, ou bien null si ce paramètre est absent de l'adresse.
La valeur revient toujours sous forme de texte, même pour /samurai/7 - tu obtiens la chaîne '7', pas un nombre. Puisque get() peut renvoyer null, il faut prévoir ce cas. C'est le rôle de l'opérateur ??, appelé en français opérateur de coalescence des nuls : il renvoie son opérande de droite uniquement si celui de gauche vaut null ou undefined. La lecture complète forme une chaîne de quatre maillons sur une seule ligne : this.route, puis .snapshot, puis .paramMap, et enfin .get('id'). Nous la placerons dans ngOnInit, ce même crochet du cycle de vie que tu as rencontré avec les rituels du guerrier, et la classe annoncera au passage l'interface OnInit.
1import { Component, inject, OnInit } from '@angular/core';
2import { ActivatedRoute } from '@angular/router';
3
4@Component({
5 selector: 'app-samurai-detail',
6 standalone: true,
7 template: '<h1>Samurai #{{ samuraiId }}</h1>'
8})
9export class SamuraiDetailComponent implements OnInit {
10 private route = inject(ActivatedRoute);
11 samuraiId = '';
12
13 ngOnInit(): void {
14 this.samuraiId = this.route.snapshot.paramMap.get('id') ?? '';
15 }
16}Ouvrir /samurai/7 affiche désormais le texte "Samurai #7". Ce qui n'a pas changé : le fichier des routes. La configuration samurai/:id est identique à celle d'hier, et le routeur ne dépose toujours rien de lui-même dans le composant - c'est le composant qui va chercher la donnée. Méfie-toi d'un piège : le nom :id dans la configuration de la route et la chaîne 'id' dans get('id') doivent coïncider à la lettre près. Une faute de frappe ne déclenche aucune erreur, elle renvoie discrètement null, ?? glisse une chaîne vide à la place, et tu contemples un titre vide en accusant le routeur. Si tu croises || '' au lieu de ?? '' dans du code existant, le résultat sera le même ici, puisque seule la valeur null est en jeu.
Les notes en marge - les paramètres de requête
Un paramètre de chemin est obligatoire : l'adresse /samurai, sans numéro, ne correspond pas du tout à la route samurai/:id, car il lui manque un tronçon entier, et le routeur ira donc chercher une autre route ou affichera l'écran d'erreur. Mais les parchemins ont aussi une marge, sur laquelle le messager note ce qui est facultatif - l'ordre de tri, le numéro de page, le filtre choisi. Dans l'adresse, ces notes vivent après le point d'interrogation, comme dans /samurai?sort=name, et on les appelle paramètres de requête. Angular les range dans une carte distincte nommée queryParamMap, qui s'interroge avec la même méthode get() et suit la même règle : un paramètre absent donne null.
La différence tient en un seul point, mais ce point compte : les paramètres de requête ne participent pas à la mise en correspondance des routes. Les adresses /samurai/7 et /samurai/7?sort=level aboutissent à la même route et au même composant. C'est donc l'endroit idéal pour tout ce sans quoi l'écran doit pouvoir s'ouvrir correctement. Ci-dessous, j'ajoute à la classe que tu connais déjà un champ supplémentaire et une seule ligne de lecture.
1// meme classe, imports inchanges
2sortOrder = 'default';
3
4ngOnInit(): void {
5 this.samuraiId = this.route.snapshot.paramMap.get('id') ?? '';
6 this.sortOrder = this.route.snapshot.queryParamMap.get('sort') ?? 'default';
7}Ouvrir /samurai/7?sort=level donne maintenant samuraiId égal à '7' et sortOrder égal à 'level', tandis qu'ouvrir simplement /samurai/7 laisse à sortOrder sa valeur de repli 'default'. Remarque que ni la façon de lire ni la gestion de l'absence de valeur n'ont bougé - seule la carte que tu interroges a changé : paramMap pour les tronçons du chemin, queryParamMap pour les notes qui suivent le point d'interrogation. C'est la seule distinction à faire ici.
Quand l'instantané ne suffit plus
Voici le moment le plus important de la leçon. Tu es sur /samurai/7 et tu cliques sur le lien "guerrier suivant", qui mène à /samurai/8. L'adresse change dans la barre, le routeur fait correspondre la même route et le même composant, il ne le recrée donc pas : il réutilise l'exemplaire existant. Et puisque le composant ne renaît pas, ngOnInit ne sera pas appelé une seconde fois. Ton champ samuraiId reste avec son sept alors que la barre d'adresse affiche un huit.
Il vaut la peine de comprendre où se situe exactement la faute, car ce n'est pas ce que suggère l'intuition. L'instantané lui-même est bel et bien mis à jour à chaque navigation - si tu relisais this.route.snapshot.paramMap.get('id') après le passage, tu obtiendrais huit. L'ennui, c'est que plus personne ne le relit, faute d'occasion. Il te faut quelque chose qui te prévienne tout seul du changement.
C'est le rôle de paramMap lue sans snapshot, directement sur ActivatedRoute. Ce n'est pas une valeur unique, mais un flux de valeurs qui apparaissent au fil du temps, autrement dit un Observable. Pour en tirer quoi que ce soit, tu t'inscris aux notifications avec la méthode subscribe(), en lui passant une fonction qu'Angular appellera à chaque nouvelle valeur. En termes d'Académie : snapshot, c'est le parchemin reçu une seule fois à la porte, tandis que subscribe(), c'est le messager permanent qui accourt avec un nouveau parchemin chaque fois que l'ordre change.
1ngOnInit(): void {
2 this.route.paramMap.subscribe(params => {
3 this.samuraiId = params.get('id') ?? '';
4 });
5}La première ligne ouvre l'abonnement, la deuxième effectue la lecture, la troisième referme la fonction et l'appel - et c'est dans cet ordre que ce code existe dans le fichier. L'objet params est exactement du même type que snapshot.paramMap, si bien que la méthode get() et la gestion du null par ?? s'écrivent à l'identique. Angular appellera cette fonction dès l'arrivée sur la route, puis une fois de plus à chaque passage vers un autre numéro. Rien n'a changé, sinon le moment de la lecture : d'unique, elle est devenue répétée.
Quand choisir quoi ? Je te propose une règle simple : si la vue contient le moindre lien menant à la même route avec un paramètre différent, prends le flux. Dans tous les autres cas, l'instantané est plus court, plus lisible et ne laisse derrière lui aucun abonnement. Cet abonnement que tu ouvres, il convient d'ailleurs de le ranger à la destruction du composant, donc dans ngOnDestroy - les outils qui font cela avec élégance t'attendent chez les éclaireurs ninja, dans le module consacré à RxJS. Il existe encore une troisième voie : l'option withComponentInputBinding() passée à la configuration du routeur fait tomber les paramètres de route directement dans les entrées du composant. Elle réclame toutefois des connaissances sur les signaux que tu acquerras plus tard, alors nous restons aujourd'hui avec ActivatedRoute.
L'ordre de marche - le service Router
Jusqu'ici, c'est routerLink qui te déplaçait depuis le template. Très souvent, pourtant, la décision de changer d'écran est prise dans le code : après l'enregistrement d'un formulaire, après une connexion réussie, après le choix d'un guerrier dans une liste. Il te faut alors un second service issu de @angular/router, nommé Router, et sa méthode navigate(). Le partage des rôles est net et mérite d'être retenu une fois pour toutes : ActivatedRoute décrit où tu te tiens, Router t'emmène ailleurs.
Deux autres noms prêtent ici à confusion. RouterOutlet n'est pas un service mais une directive - la balise <router-outlet> qui, dans le template, réserve l'emplacement où sera rendu le composant de la route ; on ne peut pas l'injecter pour aller où que ce soit. Quant à Location, du paquet @angular/common, c'est une fine couche posée sur l'historique du navigateur, avec des méthodes comme back() ou go(). Elle sait faire reculer l'utilisateur ou remplacer l'adresse, mais elle ignore ta configuration de routes et ne déclenche pas leur mise en correspondance. Pour rejoindre une route définie dans le fichier des routes, il y a Router, et lui seul.
1import { Component, inject } from '@angular/core';
2import { Router } from '@angular/router';
3
4@Component({
5 selector: 'app-samurai-list',
6 standalone: true,
7 template: '<button (click)="goToSamurai(7)">Voir le guerrier</button>'
8})
9export class SamuraiListComponent {
10 private router = inject(Router);
11
12 goToSamurai(id: number): void {
13 this.router.navigate(['/samurai', id]);
14 }
15}Un clic sur le bouton emmène l'utilisateur sur /samurai/7, où le composant de la première moitié de la leçon prend la parole et lit le sept. Le détail essentiel se cache entre les crochets : la méthode navigate() accepte un tableau de segments, et non une chaîne toute faite. L'écriture ['/samurai', id] signifie "la route samurai, et dedans ce numéro", et c'est Angular qui recolle ces morceaux avec une barre oblique et qui encode les caractères spéciaux. Un nombre dans le tableau est parfaitement autorisé - le routeur le convertira en texte, même si à la lecture, de l'autre côté, il reviendra de toute façon sous forme de chaîne. La ligne entière se lit de gauche à droite : this.router, puis .navigate(, puis ['/samurai', id], et pour finir );.
Les notes au départ - queryParams
Puisque l'adresse peut transporter des paramètres de requête, il doit exister un moyen de les inscrire au moment même de la navigation. La méthode navigate() accepte pour cela un second argument, facultatif : un objet d'options de voyage. Le champ queryParams de cet objet est un simple ensemble de clés et de valeurs qu'Angular collera à l'adresse après le point d'interrogation, dans l'ordre où tu les as écrites.
1sortByName(): void {
2 this.router.navigate(['/samurai'], {
3 queryParams: { sort: 'name', order: 'asc' }
4 });
5}Après cet appel, la barre d'adresse affichera /samurai?sort=name&order=asc. La mise en correspondance de la route n'a pas bougé pour autant - le routeur choisit toujours la route samurai, exactement la même que sans les notes, puisque les paramètres de requête ne participent pas à la correspondance. Seule l'adresse a changé, celle que verra un composant qui lit queryParamMap.
Par défaut, chaque nouvelle navigation abandonne les paramètres de requête en cours. Si l'utilisateur a réglé un tri et des filtres et que tu l'emmènes sur l'écran suivant, les notes disparaissent. Tu pilotes cela avec le champ queryParamsHandling, qui accepte trois valeurs sensées : 'preserve' conserve les notes existantes et ignore celles que tu viens de passer, 'merge' fusionne les anciennes avec les nouvelles, et 'replace' rétablit le comportement par défaut. Je te recommande 'merge' comme premier choix car 'preserve' jette silencieusement à la corbeille les paramètres transmis dans le même appel, ce qui peut te coûter une heure de recherche de bug.
1goToSamuraiKeepingFilters(id: number): void {
2 this.router.navigate(['/samurai', id], {
3 queryParamsHandling: 'preserve'
4 });
5}Si l'utilisateur se trouvait sur /samurai?sort=level, cet appel le conduira sur /samurai/7?sort=level - le numéro du guerrier a changé, la note de tri a survécu. Ce qui n'a pas changé, en revanche, c'est la méthode elle-même : c'est toujours navigate() avec son tableau de segments, une seule option est venue s'ajouter dans le second argument.
navigateByUrl - l'adresse entière en une seule chaîne
Il arrive que l'adresse te parvienne d'un seul tenant, sans rien pour construire un tableau. Le cas classique est l'adresse mémorisée avant la connexion, pour ramener l'utilisateur, une fois identifié, exactement là où il avait commencé. Découper une telle chaîne en segments serait une perte de temps, alors Router propose une seconde méthode : navigateByUrl(). Elle prend l'adresse entière comme un seul texte, paramètres de requête et fragment après le dièse compris.
1goToSavedUrl(): void {
2 this.router.navigateByUrl('/samurai/123?sort=level');
3}N'essaie pas la même chose avec navigate() - l'appel navigate('/samurai/123') ne compilera pas, car le premier argument de cette méthode doit être un tableau et TypeScript refusera une chaîne isolée. Ne cherche pas non plus de méthodes goTo() ou redirect() : le service Router n'en possède aucune et le compilateur te dira franchement qu'une telle propriété n'existe pas. Le mot redirectTo peut induire en erreur, car tu le connais bel et bien depuis la configuration des routes, mais c'est un champ d'une entrée du tableau de routes, pas une méthode du service.
Au quotidien, je te recommande navigate() comme choix par défaut : elle assemble les segments sans danger, encode les caractères spéciaux et accepte des options comme queryParams. Réserve navigateByUrl() aux cas où l'adresse te vient de l'extérieur déjà rédigée, et où la démonter relèverait de l'art pour l'art.
Souviens-toi : ActivatedRoute est le parchemin que tu lis et Router l'ordre de marche que tu donnes - l'instantané suffit tant que tu restes en place, mais dès que la route se dérobe sous tes pas, il te faut un messager.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel service utilise-t-on pour lire les paramètres du chemin actuel ?
2. Quel service (en plus d'ActivatedRoute) injecte-t-on pour changer la route de façon programmatique depuis la classe d'un composant ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne sur une seule ligne la lecture du paramètre 'id' depuis le snapshot de la route :
- Éditeur de code
Complète le composant pour lire le paramètre 'id' de l'URL à l'aide d'inject() et d'ActivatedRoute.
- Tri horizontal
Ordonne sur une seule ligne l'appel de navigation vers /samurai avec le paramètre id :
- Tri vertical
Ordonne les lignes d'un abonnement aux paramètres de route (de haut en bas, comme dans le composant) :