7. Boucles et conditions

0

Dans ce chapitre, l’objectif est d’afficher et de filtrer dynamiquement une liste d’objets dans une application Angular, exclusivement avec les nouvelles syntaxes du framework introduites petit à petit depuis la version 18 d’Angular.

Nous allons faire les choses suivantes:

  • adapter notre code afin de le préparer pour ce nouveau chapitre
  • on va voir comment utiliser les blocks @for et @empty dans nos templates HTML
  • on verra également comment définir des conditions avec @if et @else
  • comment définir des variables dans nos templates avec @let
  • pour finir on verra également le block @switch

A la fin de ce chapitre, on aura une liste d’objets de collections qui seront affichés à l’écran et qu’on pourra filtrer grace à notre barre de recherche:

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.

application de gestion de collections

Préparation du code

Avant de voir comment créer une boucle dans nos templates HTML avec @for adaptons un peu notre projet. Pour commencer créons un nouveau « model », qu’on va appeler « Collection » et qu’on va stocker dans un fichier « collection.ts » dans le dossier « models » :

collection.ts
import { CollectionItem } from "./collection-item";

export class Collection {
    title: string = "My Collection";
    items: CollectionItem[] = [];
}

Ensuite modifions notre fichier app.ts comme suit :

  • éffaçons les attributs count, selectedItemIndex, selectedItem et loggingEffect
  • éffaçons increaseCount et toggleItem
  • modifions l’attribut search afin d’en faire un model
  • créons aussi un nouvel objet de collection qu’on va appeler stamp
  • créons un signal selectedCollection, qui va contenir la collection à afficher
  • créons un signal computed qu’on va appeler displayedItems qui va contenir les objets de la collection selectionée, filtrés par rapport à la valeur contenue dans notre model search

Une fois tout ceci fait, notre code ressemble à ça :

app.ts
import { ChangeDetectionStrategy, Component, computed, effect, model, signal } from '@angular/core';
import { CollectionItemCard } from "./components/collection-item-card/collection-item-card";
import { CollectionItem } from './models/collection-item';
import { SearchBar } from "./components/search-bar/search-bar";
import { Collection } from './models/collection';

@Component({
  selector: 'app-root',
  imports: [CollectionItemCard, SearchBar],
  templateUrl: './app.html',
  styleUrl: './app.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {

  search = model('');

  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() {
    this.coin = new CollectionItem();
    this.coin.name = 'Pièce de 1972';
    this.coin.description = 'Pièce de 50 centimes de francs.';
    this.coin.rarity = 'Commune';
    this.coin.image = 'img/coin1.png';
    this.coin.price = 170;

    this.stamp = new CollectionItem();
    this.stamp.name = 'Timbre 1800';
    this.stamp.description = 'Un vieux timbre';
    this.stamp.rarity = 'Rare';
    this.stamp.image = 'img/timbre1.png';
    this.stamp.price = 555;

    this.linx = new CollectionItem();

    const defaultCollection = new Collection();
    defaultCollection.title = "Collection mix";
    defaultCollection.items = [
      this.coin,
      this.linx,
      this.stamp
    ];
    this.selectedCollection.set(defaultCollection);
  }
  
}

Nous pouvons maintenant adapter notre fichier « app.html », en enlevant toutes les références aux attributs qui n’existent plus etn en vidant pour le moment notre section « collection-grid » :

app.html
<header id="collection-header">
    <h1>My Collection</h1>
    <div>
        <app-search-bar 
            [(search)]="search"
        >
        </app-search-bar>
    </div>
</header>
<section class="collection-grid">
</section>

Une fois tous ces changements effectués, notre code devrait s’executer proprement et on obtient la page suivante :

Pour l’instant notre collection n’est pas encore affichée, mais on va retourner tout de suite dans notre fichier app.html afin de l’adapter et afficher tous les objets de notre collection.

Le block @for et @empty

Afin de pouvoir afficher nos objets de collections, nous avons besoin d’un moyen d’itérer à travers notre liste, directement dans notre template HTML. Et ça tombe bien, car Angular nous met le block @for à disposition qui permet justement de faire ça. Le block @for à la forme suivante:

HTML
@for (<item> of <collection>; track <track by>) {

}

La variable <item> représente ici l’élément de l’itération actuelle de la boucle, tandis que <collection> correspond à la liste que l’on souhaite parcourir. Le paramètre <track by> doit fournir une expression qui permet à Angular d’identifier chaque élément de façon unique : en général ce sera une propriété de l’élément parcouru, tel que item.id, ou tout autre propriété unique.

Si nous appliquons cela à nos displayedItems on obtient la chose suivante :

app.html
<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) {
        <app-collection-item-card [item]="item"></app-collection-item-card>
    }
</section>

Ce qui nous donne le résultat suivant:

Notez que nous avons également adapté le titre de la collection, afin d’y afficher le nom de la collection selectionnée.

Important: Le paramètre track indique à Angular comment reconnaître chaque élément de la liste. Il permet de ne pas recréer inutilement les éléments du DOM quand la liste change : Angular réutilise ainsi les composants existants si leur identifiant est inchangé. Il faut donc toujours choisir une propriété vraiment unique.

Dans notre exercice nous avons fait en sorte que le nom de nos éléments soient uniques, mais dans la pratique veillez à toujours utiliser un identifiant dont unicité est garantie.

Améliorons encore un peu notre exemple, en faisant en sorte que si notre liste à afficher est vide, un message apparaisse à l’écran indiquant qu’aucun objet à été trouvé. Pour cela on peut définir un block @empty directement après le block de notre @for. @empty est un block qui n’est affiché que si la collection passée à @for est vide.

Nous pouvons donc faire la chose suivante:

app.html
<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) {
        <app-collection-item-card [item]="item"></app-collection-item-card>
    } @empty {
        <div>Aucun objet ne correspond à la recherche.</div>
    }
</section>

Et maintenant si notre recherche ne renvoie pas de résultat, nous obtenons :

Les blocks @if et @else

Au lieu d’afficher un message quand notre liste d’objets est vide, ce serait intéressant d’afficher un message indiquant le nombre de résultats affichés, ou le cas échéant, un message indiquant qu’il n’y a pas de résultats. Pour cela on peut utiliser les blocks @if et @else de manière similaire à ce qu’on l’aurait fait en TypeScript :

app.html
<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) {
        <app-collection-item-card [item]="item"></app-collection-item-card>
    } 
</section>
@if(displayedItems().length > 0) {
    <div class="centered">{{displayedItems().length}} objet(s) affiché(s).</div>
} @else {
    <div class="centered">Aucun résultat.</div>
}

Ajoutons aussi un peu de css au fichier « app.scss » afin de centrer ces messages :

app.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;
}

Si on retourne dans le navigateur on voit bien le nombres d’objets s’afficher à l’écran :

Le block @let

Dans l’exemple que nous venons de voir nous utilisons à deux reprises la longueur de la liste displayedItems(). Pour éviter cela, on peut définir une variable dans notre template qui va contenir cette longueur qu’on pourra réutiliser par la suite. De telles variables sont définies en utilisant @let :

app.html
<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) {
        <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>
}

Et comme on peut le voir notre page fonctionne toujours:

Les block @switch et @case

Avant de clôturer ce chapitre, on va encore parler des @switch / @case. Ces blocks nous permettent d’adapter notre HTML en fonction des valeurs d’une expression donnée. La structure d’un @switch / @case est la suivante :

HTML
@switch(<expression>) {
    @case(<valeur1>) {
      
    } 
    @case(<valeur2>) {
    }
    ...
    default {
    }
} 

@switch prend une expression en paramètre et à l’intérieur de son block de code on peut définir plusieurs @cases qui vont prendre en paramètre la valeur que le block de code du case va traiter, et pour finir on peut également définir un block @default, pour traiter les cas qui ne sont capturés par aucun block @case.

Si on souhaite par exemple que nos objets rares et legendaries soient traités de manière différente des autres objets, on peut faire la chose suivante :

app.html
<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>
}

Adaptons encore notre css :

app.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;
}

Et maintenant si on retourne dans notre navigateur on voit bien que nos éléments légendaires et rares ont un rendu différent des autres objets:

Et voila on a fait le tour des différents des blocks de conditions et de boucles. Dans le prochain chapitre on va voir comment créer et utiliser des services.

Laisser un commentaire