Angular : créer un système de favoris pour des articles avec les Signals

Dans cet exemple, nous allons créer une petite application Angular qui affiche une liste d’articles et permet à l’utilisateur de les ajouter ou de les retirer de ses favoris.

Les favoris sont enregistrés dans une API afin qu’ils soient conservés même après le rechargement de la page.

Nous allons utiliser :

  • HttpClient pour communiquer avec l’API ;

  • les signals Angular pour gérer les données ;

  • @for et @if dans le template ;

  • Bootstrap pour la mise en page ;

  • Bootstrap Icons pour les icônes.

Structure des données

Nous commençons par définir la structure d’un article et d’un favori.

interface Article {
  id: number;
  titre: string;
  description: string;
}

interface Favori {
  id: number | string;
}

Article représente les articles reçus depuis notre API.

Chaque article possède :

  • un identifiant ;

  • un titre ;

  • une description.

Favori contient simplement l’identifiant de l’article enregistré comme favori.

Création du Signals

Dans notre composant Angular, nous utilisons plusieurs signals.

readonly articles = signal<Article[]>([]);
readonly favoris = signal<number[]>([]);

readonly favorisCharges = signal(false);
readonly sauvegardeEnCours = signal(false);

readonly erreurFavoris = signal('');

Le signal articles contient la liste des articles.

Le signal favoris contient uniquement les identifiants des articles enregistrés comme favoris.

Les autres signals permettent de savoir si les favoris sont chargés, si une sauvegarde est en cours ou si une erreur s’est produite.

Charger les articles et les favoris

Au démarrage du composant, nous utilisons ngOnInit().

ngOnInit(): void {

  // Chargement des articles
  this.http.get<Article[]>('http://localhost:3000/artiles').subscribe({
    next: (articles) => {

      // On transforme les identifiants en nombres
      this.articles.set(
        articles.map(article => ({
          ...article,
          id: Number(article.id)
        }))
      );

    },

    error: (erreur) => {
      console.error('Lecture des articles impossible', erreur);
    }
  });


  // Chargement des favoris
  this.http.get<Favori[]>(this.favorisUrl).subscribe({

    next: (favoris) => {

      // Nous conservons uniquement les identifiants.
      this.favoris.set(
        favoris.map(favori => Number(favori.id))
      );

      // Les favoris sont maintenant disponibles.
      this.favorisCharges.set(true);
    },

    error: () => {
      this.erreurFavoris.set(
        'Impossible de charger les favoris. Vérifiez le serveur puis rechargez la page.'
      );
    }

  });
}

Nous effectuons donc deux requêtes HTTP.

La première récupère les articles.

La seconde récupère les favoris déjà enregistrés dans notre API.

Vérifier si un article est en favori

Pour savoir si un article est déjà présent dans les favoris, nous utilisons la méthode includes().

estFavori(id: number): boolean {
  return this.favoris().includes(id);
}

Par exemple, si notre signal contient :

estFavori(5);

retournera :

true

Cette petite méthode sera très utile dans notre template HTML.

Ajouter un article aux favoris

Lorsque l’utilisateur clique sur le bouton, nous exécutons :

changerFavori(id: number): void {

  // On évite une action si les favoris ne sont pas encore chargés
  // ou si une sauvegarde est déjà en cours.
  if (!this.favorisCharges() || this.sauvegardeEnCours()) {
    return;
  }

  // Si l'article est déjà en favori,
  // nous allons le supprimer.
  if (this.estFavori(id)) {
    this.supprimerFavori(id);
    return;
  }

  // Suppression d'un ancien message d'erreur.
  this.erreurFavoris.set('');

  // Une sauvegarde commence.
  this.sauvegardeEnCours.set(true);

  // Enregistrement du favori dans l'API.
  this.http.post<Favori>(
    this.favorisUrl,
    { id: String(id) }
  )
  .pipe(

    // Cette fonction est exécutée
    // lorsque la requête est terminée.
    finalize(() => {
      this.sauvegardeEnCours.set(false);
    })

  )
  .subscribe({

    next: (favori) => {

      // Ajout du nouvel identifiant dans le signal.
      this.favoris.update(ids => [
        ...ids,
        Number(favori.id)
      ]);
    },

    error: () => {
      this.erreurFavoris.set(
        'Impossible d’ajouter le favori. Réessayez.'
      );
    }

  });
}

Après la réponse du serveur, nous ajoutons également l’identifiant dans notre signal favoris.

L’interface se met alors à jour automatiquement.

Retirer un article des favoris

Pour supprimer un favori, nous utilisons une requête HTTP DELETE.

supprimerFavori(id: number): void {

  // Vérifications avant la suppression.
  if (
    !this.favorisCharges() ||
    this.sauvegardeEnCours() ||
    !this.estFavori(id)
  ) {
    return;
  }

  this.erreurFavoris.set('');
  this.sauvegardeEnCours.set(true);

  // Exemple :
  // DELETE http://localhost:3000/estFavori/3
  this.http.delete<void>(
    `${this.favorisUrl}/${id}`
  )
  .pipe(

    finalize(() => {
      this.sauvegardeEnCours.set(false);
    })

  )
  .subscribe({

    next: () => {

      // On conserve tous les favoris
      // sauf celui qui vient d'être supprimé.
      this.favoris.update(ids =>
        ids.filter(favoriId => favoriId !== id)
      );

    },

    error: () => {
      this.erreurFavoris.set(
        'Impossible de supprimer le favori. Réessayez.'
      );
    }

  });
}

La méthode filter() permet ici de reconstruire la liste sans l’identifiant supprimé.

Afficher les articles avec @for

Dans notre template HTML, nous parcourons les articles avec la syntaxe moderne d’Angular :

@for (item of articles(); track item.id; let index = $index) {

  <article>

    <h3>
      {{ item.titre }}
    </h3>

    <p>
      {{ item.description }}
    </p>

  </article>

}

Modifier le bouton selon l’état du favori

Nous pouvons maintenant utiliser estFavori() directement dans notre template.

<button
  type="button"
  class="btn"
  [class.btn-primary]="estFavori(item.id)"
  [class.btn-outline-primary]="!estFavori(item.id)"
  (click)="changerFavori(item.id)">

  {{ estFavori(item.id)
      ? 'Retirer des favoris'
      : 'Ajouter aux favoris'
  }}

</button>

Si l’article n’est pas en favori, le bouton affiche :

Ajouter aux favoris

Après le clic, le signal est mis à jour et Angular affiche automatiquement :

Retirer des favoris

Nous n’avons donc pas besoin de modifier manuellement le HTML.

Afficher le nombre de favoris

Comme notre signal contient tous les identifiants favoris, nous pouvons facilement connaître leur nombre.

<p>
  {{ favoris().length }}
</p>

Nous pouvons également gérer le singulier et le pluriel :

<p>
  {{ favoris().length === 1
    ? 'article en favori'
    : 'articles en favoris'
  }}
</p>

Informer l’utilisateur pendant l’enregistrement

Notre signal :

readonly sauvegardeEnCours = signal(false);

permet également d’afficher un message lorsque la requête HTTP est en cours.

@if (sauvegardeEnCours()) {

  <span
    class="spinner-border spinner-border-sm me-2">
  </span>

  Enregistrement de vos favoris…

}

Le bouton peut également être désactivé pendant cette période :

[disabled]="!favorisCharges() || sauvegardeEnCours()"

Cela évite que l’utilisateur clique plusieurs fois très rapidement sur le bouton.

Conclusion

Cet exemple montre surtout l’intérêt des signals : lorsque nous modifions leurs données, Angular peut automatiquement mettre à jour l’interface concernée.

C’est une approche simple pour commencer à construire des interfaces dynamiques avec Angular.