Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

Bonnes Pratiques UX pour le routing dans Angular

Le routing est un élément crucial de toute application web moderne. Une bonne gestion du routing permet d'offrir une expérience utilisateur fluide et intuitive. Imaginons un client dans un grand magasin : il ne voudrait pas avoir à retourner à l'entrée chaque fois qu'il change de rayon. C'est la même chose pour votre application !

Conservation du contexte de navigation

Le ReturnUrl pattern

CONSEIL

Toujours sauvegarder la route actuelle avant une redirection forcée (comme vers la page de login) permet à l'utilisateur de reprendre exactement là où il en était.

Voici comment implémenter cette fonctionnalité :

ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard = () => {
  const router = inject(Router);
  const authService = inject(AuthService);

  if (authService.isAuthenticated()) {
    return true;
  }
  
  return router.createUrlTree(['/login'], { 
    queryParams: { returnUrl: router.routerState.snapshot.url } 
  });
};
ts
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
// AuthService serait un service d'authentification qui permettrait de se connecter à l'application
import { AuthService } from './auth.service';

@Component({
  standalone: true,
  selector: 'app-login',
  imports: [FormsModule],
  template: `
    @if (error) {
      <div class="error">{{ error }}</div>
    }
    <form (ngSubmit)="onSubmit()">
      <!-- Formulaire de login -->
    </form>
  `
})
export class LoginComponent {
  private route = inject(ActivatedRoute);
  private router = inject(Router);
  private authService = inject(AuthService);
  
  async onSubmit() {
    try {
    // Pour faciliter,  ça renvoie une promesse mais ça pourrait être un observable dans votre code
      await this.authService.login();
      const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
      await this.router.navigateByUrl(returnUrl);
    } catch (error) {
      this.error = 'Échec de la connexion';
    }
  }
}
ts
import { Routes } from '@angular/router';
import { authGuard } from './auth.guard';

export const routes: Routes = [
  { 
    path: 'users',
    loadComponent: () => import('./users/users.component'), // on utilise le lazy loading pour charger le composant
    canActivate: [authGuard]  // Protection de la route avec notre guard
  },
  { 
    path: 'login',
    loadComponent: () => import('./login/login.component') // on utilise le lazy loading aussi
  }
];

La méthode createUrlTree

La méthode createUrlTree est une méthode moderne d'Angular qui remplace le retour booléen traditionnel des guards. Elle permet de :

  • Créer directement un arbre d'URL pour la redirection
  • Éviter les problèmes de timing liés aux redirections asynchrones (quand plusieurs redirections se produisent en même temps, certaines peuvent être perdues ou exécutées dans le mauvais ordre)
  • Ajouter facilement des paramètres de requête

COMPRENDRE L'ARBRE D'URL

Un arbre d'URL (UrlTree) est une structure de données qui représente une URL décomposée en segments. Imaginez un arbre où chaque segment de l'URL est une branche :

/users/123/edit?returnUrl=/dashboard

├── users
│   └── 123
│       └── edit
└── queryParams
    └── returnUrl: '/dashboard'

Par exemple, pour construire cet arbre :

ts
// Construction simple d'un UrlTree
router.createUrlTree(['/users', '123', 'edit']); 

// Avec des paramètres de requête
router.createUrlTree(
  ['/users', '123', 'edit'], 
  { 
    queryParams: { returnUrl: '/dashboard' } 
  }
);

Cette structure permet à Angular de gérer efficacement la navigation et les paramètres associés.

La propriété router.routerState.snapshot.url capture l'URL complète actuelle avant la redirection. Par exemple, si l'utilisateur essaie d'accéder à /users/123/edit, cette URL sera sauvegardée intégralement.

Récupération du returnUrl

Dans le composant de login, la ligne :

ts
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';

fait deux choses :

  1. Récupère le paramètre returnUrl de l'URL (ex: /login?returnUrl=/users/123/edit)
  2. Utilise '/' comme fallback si le paramètre n'existe pas, assurant ainsi une redirection par défaut vers la page d'accueil

ASTUCE

Vous pouvez également utiliser cette technique pour d'autres cas de redirection, comme après une action qui nécessite une confirmation.

Gestion du Rafraîchissement

ATTENTION

Le comportement par défaut du router lors d'un rafraîchissement peut parfois être déroutant pour l'utilisateur.

Parfois, le comportement par défaut du router lors d'un rafraîchissement peut empêcher de recharger le composant comme attendu. Pour ces cas, vous pouvez configurer l'option onSameUrlNavigation

ts
this.router.navigate(['/users'], { 
  relativeTo: this.route,
  onSameUrlNavigation: 'reload' // Force le rechargement même sur la même URL
});

La propriété onSameUrlNavigation configure comment Angular doit réagir quand l'utilisateur navigue vers l'URL actuelle. Par exemple, si l'utilisateur est sur /users et clique sur un lien qui mène aussi vers /users :

  • Avec 'ignore' (défaut) : Angular ne fait rien
  • Avec 'reload' : Angular recharge le composant, même si l'URL est identique

EXEMPLE CONCRET

Imaginez une liste d'utilisateurs avec un bouton "Rafraîchir" :

  • Sans reload : cliquer sur le bouton ne rechargerait pas la liste
  • Avec reload : la liste est mise à jour, même si l'URL ne change pas

Cette configuration est particulièrement utile pour :

  • Les pages avec des données en temps réel
  • Les listes qui doivent être rafraîchies
  • Les formulaires qui doivent être réinitialisés

Feedback Visuel pendant la Navigation

Pour améliorer l'expérience utilisateur, ajoutez des indicateurs de chargement :

ts
import { Injectable } from '@angular/core';
import { signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LoadingService {
  loading = signal(false);
  
  setLoading(value: boolean) {
    this.loading.set(value);
  }
}
ts
import { Component, inject } from '@angular/core';
import { Router, Event, NavigationStart, NavigationEnd } from '@angular/router';
import { LoadingService } from './loading.service';

@Component({
  standalone: true,
  selector: 'app-root',
  template: `
    @if (loadingService.loading()) {
      <div class="loading-indicator">Chargement...</div>
    }
    <router-outlet />
  `
})
export class AppComponent {
  private router = inject(Router);
  private loadingService = inject(LoadingService);
    
  constructor() {
    this.router.events.subscribe((event: Event) => {
      if (event instanceof NavigationStart) {
        this.loadingService.setLoading(true);
      }
      if (event instanceof NavigationEnd) {
        this.loadingService.setLoading(false);
      }
    });
  }
}

Le router Angular émet différents événements pendant la navigation. Les plus importants sont :

  • NavigationStart : émis quand la navigation commence
  • NavigationEnd : émis quand la navigation se termine avec succès

Dans notre exemple :

ts
this.router.events.subscribe((event: Event) => {
  if (event instanceof NavigationStart) {
    this.loadingService.setLoading(true);  // Affiche l'indicateur de chargement
  }
  if (event instanceof NavigationEnd) {
    this.loadingService.setLoading(false); // Cache l'indicateur de chargement
  }
});

Cette approche permet de :

  1. Détecter le début de la navigation pour montrer un indicateur de chargement
  2. Détecter la fin de la navigation pour cacher l'indicateur
  3. Informer visuellement l'utilisateur que l'application traite sa demande

OPTIMISATION

Utilisez les signaux Angular pour une gestion réactive et performante de l'état de chargement.

En savoir plus sur les événements du router