7. Boucles et conditions

0

Lors des derniers chapitres , on a vu comment créer des inputs et des outputs et comment réagir aux changements. Aujourd’hui, on va voir comment utiliser des conditions et des boucles dans nos pages Angular, afin d’arriver à un résultat final où on aura une liste de cartes qu’on pourra filtrer grâce à notre barre de recherche. En-dessous de la liste, on aura un texte qui nous affichera combien de cartes sont affichées, et si aucune carte ne correspond à notre recherche, on aura un autre texte qui s’affichera à l’écran.

Loops and conditions final result

Ainsi, pour faire cela, on va voir:

  • Comment créer des boucles dans nos templates HTML avec l’ancienne directive *ngFor
  • Comment créer des conditions avec *ngIf
  • Comment utiliser des « else »
  • Comment utiliser les nouvelles syntaxes qui ont été introduites dans Angular 18 en version stable. Là, on va commencer par parler des @if et @else
  • Nous verrons aussi la nouvelle syntaxe pour la création de boucles @for et @empty

Pour rappel, cette série de posts s’inscrit dans une longue lignée de posts dont le fil rouge est la création d’une application de visualisation et de gestion de cartes à collectionner de type Pokémon, Magic, Yu-Gi-Oh! ou autre. Pour chaque nouveau chapitre, je pars du principe que vous avez lu les chapitres précédents.

Résultat final projet Angular

Pour commencer, avant de voir comment créer des boucles, il va falloir qu’on crée une liste de monstres à afficher à l’écran. Pour cela, on va ouvrir notre fichier « app.component.ts » et on va y ajouter quelques monstres. En plus de cela, on va supprimer tout le code dont on n’aura pas besoin et on va garder uniquement notre liste de monstres et notre constructeur. Notez qu’on va aussi importer « CommonModule » qui va nous permettre d’utiliser les boucles et les conditions dans notre template « html ».

app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { SearchBarComponent } from './components/search-bar/search-bar.component';
import { Monster } from './models/monster.model';
import { MonsterType } from './utils/monster.utils';

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [CommonModule, PlayingCardComponent, SearchBarComponent],
    templateUrl: './app.component.html',
    styleUrl: './app.component.css'
})
export class AppComponent {

    monsters!: Monster[];

    constructor() {
        this.monsters = [];

        const monster1 = new Monster();
        monster1.name = "Pik";
        monster1.hp = 40;
        monster1.figureCaption = "N°002 Pik";
        this.monsters.push(monster1);

        const monster2 = new Monster();
        monster2.name = "Car";
        monster2.image = "img/cara.png";
        monster2.type = MonsterType.WATER;
        monster2.hp = 60;
        monster2.figureCaption = "N°003 Car";
        this.monsters.push(monster2);

        const monster3 = new Monster();
        monster3.name = "Bulb";
        monster3.image = "img/bulbi.png";
        monster3.type = MonsterType.PLANT;
        monster3.hp = 60;
        monster3.figureCaption = "N°004 Bulb";
        this.monsters.push(monster3);

        const monster4 = new Monster();
        monster4.name = "Sala";
        monster4.image = "img/sala.png";
        monster4.type = MonsterType.FIRE;
        monster4.hp = 60;
        monster4.figureCaption = "N°004 Sala";
        this.monsters.push(monster4);
    }

}

Maintenant dans notre fichier « app.component.html », on va pouvoir itérer à travers notre liste de monstres pour les afficher à l’écran. Pour cela, on va garder uniquement notre « div » avec l’id « card-list ». A l’intérieur de ce div, on aura notre « app-playing-card ». Pour faire en sorte que cette « app-playing-card » soit répétée pour chacune de nos cartes, on va pouvoir utiliser l’attribut « *ngFor ». Puis, entre guillemets, on va pouvoir définir notre boucle en faisant un “let”, suivi du nom de la variable qui va contenir l’élément actuel de l’itération, suivi de “of” de la liste à travers laquelle on veut itérer. Donc ici, on se retrouve avec un *ngFor=”let monster of monsters”. Maintenant, nous pouvons passer l’élément actuel de notre boucle en input à notre « app-playing-card » en assignant notre variable monstre à l’input monstre:

app.component.html
<div id="card-list">
	<app-playing-card *ngFor="let monster of monsters" [monster]="monster"/>
</div>

Et voilà, si on regarde le résultat, on voit qu’on a bien une carte par monstre de notre liste qui est affichée.

Liste of all monsters

Maintenant, j’aimerais qu’on rajoute une barre de recherche à notre html, et qu’on puisse filtrer les monstres dont le nom correspond à la recherche effectuée. Commençons par ajouter un nouvel attribut à notre AppComponent, qu’on va appeler « search » et qui sera égal à un « model » initialisé à la chaîne de caractères vide. Ensuite, on va utiliser cet attribut pour calculer la liste des monstres à afficher en utiliser un signal « computed »:

app.component.ts
import { Component, computed, model } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { SearchBarComponent } from './components/search-bar/search-bar.component';
import { Monster } from './models/monster.model';
import { MonsterType } from './utils/monster.utils';

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [CommonModule, PlayingCardComponent, SearchBarComponent],
    templateUrl: './app.component.html',
    styleUrl: './app.component.css'
})
export class AppComponent {

    monsters!: Monster[];
    search = model('');
    filteredMonsters = computed(() => {
        return this.monsters.filter(monster => monster.name.includes(this.search()));
    });

    constructor() {
        this.monsters = [];

        const monster1 = new Monster();
        monster1.name = "Pik";
        monster1.hp = 40;
        monster1.figureCaption = "N°002 Pik";
        this.monsters.push(monster1);

        const monster2 = new Monster();
        monster2.name = "Car";
        monster2.image = "img/cara.png";
        monster2.type = MonsterType.WATER;
        monster2.hp = 60;
        monster2.figureCaption = "N°003 Car";
        this.monsters.push(monster2);

        const monster3 = new Monster();
        monster3.name = "Bulb";
        monster3.image = "img/bulbi.png";
        monster3.type = MonsterType.PLANT;
        monster3.hp = 60;
        monster3.figureCaption = "N°004 Bulb";
        this.monsters.push(monster3);

        const monster4 = new Monster();
        monster4.name = "Sala";
        monster4.image = "img/sala.png";
        monster4.type = MonsterType.FIRE;
        monster4.hp = 60;
        monster4.figureCaption = "N°004 Sala";
        this.monsters.push(monster4);
    }

}

Adaptons maintenant notre « app.component.html ». On va y ajouter une « app-search-bar » et on va lui passer notre attribut « search », qui va contenir l’état de la recherche. En plus de cela, on va aussi modifier notre « *ngFor ». Au lieu d’itérer à travers la liste de monstre, on va itérer à travers la liste de monstres filtrée qu’on a créée à l’instant:

app.component.html
<app-search-bar [(search)]="search"></app-search-bar>
<div id="card-list">
    <app-playing-card *ngFor="let monster of filteredMonsters()" [monster]="monster"></app-playing-card>
</div>

Si on retourne dans notre navigateur et qu’on tape une recherche, vous voyez que la liste de cartes affichée va automatiquement se mettre à jour pour n’inclure que les monstres dont le nom contient la chaîne de caractères tapée dans la barre de recherche.

Search monsters

J’aimerais maintenant qu’on ajoute un texte indiquant qu’aucun monstre ne correspond à la recherche. Si la recherche ne retourne aucun monstre, et si elle retourne des résultats, j’aimerais afficher un message indiquant le nombre de résultats retournés. Pour cela, on va créer 2 nouveaux « div » avec une classe « center » qu’on va définir dans un instant, et à l’intérieur desquels on va taper “No monsters found !”, respectivement “Found {{filteredMonsters().length}} monsters”. Pour faire en sorte que seul le bon « div » s’affiche à l’écran, on va rajouter à chaque « div » un attribut « *ngif » auquel on va passer notre condition, donc « filteredMonsters().length == 0 » respectivement « filteredMonsters().length > 0 »:

app.component.ts
<app-search-bar [(search)]="search"></app-search-bar>

<div id="card-list">
    <app-playing-card *ngFor="let monster of filteredMonsters()" [monster]="monster"></app-playing-card>
</div>

<div class="centered" *ngIf="filteredMonsters().length == 0">
    No monsters found!
</div>

<div class="centered" *ngIf="filteredMonsters().length > 0">
    Found {{ filteredMonsters().length }} monsters
</div>

Ajoutons aussi notre classe « centered » à notre fichier « app.component.css »:

app.component.css
h1 {
    background-color: black;
    color: white;
}

#card-list {
    display: flex;
    gap: 20px;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
}

.centered {
    margin-top: 40px;
    text-align: center;
}

Maintenant, si on retourne voir le résultat on voit que tant qu’on a des monstres qui correspondent à notre recherche, on a bien le nombre de monstres affiché qui est écrit à l’écran:

Loops and conditions final result

Là on a utilisé deux « if », mais on aurait aussi pu utiliser un genre de « if / else ». Pour cela, on doit définir un « ng-template » auquel on va donner un nom en utilisant la notation #<nom> . Dans ce « ng-template », on va copier notre div avec le contenu “Found {{filteredMonsters().length}} monsters”, mais sans le « *ngIf » qu’on avait créé précédemment. Maintenant dans notre « *ngIf », on va pouvoir rajouter un « block else », en ajoutant un point-virgule après notre condition et en tapant « else » suivi du nom du template à afficher pour notre cas « else »:

app.component.ts
<app-search-bar [(search)]="search"></app-search-bar>

<div id="card-list">
    <app-playing-card *ngFor="let monster of filteredMonsters()" [monster]="monster"></app-playing-card>
</div>

<div *ngIf="filteredMonsters().length == 0; else found">
    <div class="centered">
      No monsters found!
    </div>
</div>

<ng-template #found>
    <div class="centered">
        Found {{ filteredMonsters().length }} monsters
    </div>
</ng-template>

Je vous laisse vérifier que tout fonctionne comme avant. On va maintenant regarder comment séparer le contenu de notre « if », du div dans lequel on l’a exécuté. Pour cela, on va ajouter un deuxième « ng-template » auquel on va donner le nom #notFound et dans lequel on va recopier notre div “No monsters found !”, ici aussi sans sa condition. Avant ces deux templates, on va garder un div vide avec notre « *ngif », dans lequel on aura notre condition suivi d’un point-virgule. Puis, on aura le mot-clé « then » suivi du nom du template à afficher si la condition est vraie, et juste après on garde notre « else » comme avant :

app.component.ts
<app-search-bar [(search)]="search"></app-search-bar>

<div id="card-list">
    <app-playing-card *ngFor="let monster of filteredMonsters()" [monster]="monster"></app-playing-card>
</div>

<div *ngIf="filteredMonsters().length == 0; then notFound else found"></div>

<ng-template #notFound>
    <div class="centered">No monsters found!</div>
</ng-template>

<ng-template #found>
    <div class="centered">Found {{ filteredMonsters().length }} monsters</div>
</ng-template>

Là aussi, si on regarde notre navigateur, on a toujours le même résultat:

Search monsters

Passons maintenant à la nouvelle syntaxe introduite par Angular 17 en « developer preview » et qui est passée stable à partir d’Angular 18. Cette nouvelle syntaxe nous permet d’avoir du code bien plus lisible et facile à suivre. Commençons par regarder les mot-clés « @if » et « @else ». Pour les utiliser, rien de plus simple, il suffit de taper un « @if » suivi de parenthèses contenant notre expression à évaluer. On va mettre ensuite le code html à afficher si jamais la condition de notre « if » est vraie à l’intérieur d’accolades, comme on l’aurait fait dans notre code TypeScript. Pour ajouter un « else », on va rajouter un @else juste après la fermeture des accolades de notre « if ». Pour finir, on aura de nouveau, entre accolades, le contenu à afficher si jamais notre condition est fausse:

app.component.ts
<app-search-bar [(search)]="search"></app-search-bar>

<div id="card-list">
    <app-playing-card *ngFor="let monster of filteredMonsters()" [monster]="monster"/>
</div>

@if (filteredMonsters().length == 0) {
    <div class="centered">No monsters found !</div>
} @else {
    <div class="centered">Found {{ filteredMonsters().length }} monsters</div>
}

Ici aussi, le résultat est le même qu’avant, mais notre code est beaucoup plus simple à suivre.

Search monsters

Maintenant, on peut passer à la nouvelle syntaxe pour les boucles. Ici aussi, on se retrouve avec du code très similaire à notre « typescript » habituel, on aura un « @for » suivi de parenthèses avec la variable qui va contenir la valeur de l’itération en cours, suivi de « of ». Puis on aura de nouveau la liste à travers laquelle on veut itérer. On doit faire attention à une particularité ici, car après la définition de notre boucle, on va devoir ajouter un point-virgule et le mot-clé « track » suivi par une expression qui va indiquer à notre « for » comment identifier un objet précis de notre tableau. Ici, on va simplement l’identifier par la référence de la variable en elle même, mais si notre monstre avait un identifiant unique on aurait pu utiliser cet identifiant. On n’oublie pas nos accolades après notre « for » et on ajoute notre « app-playing-card » à l’intérieur, en prenant soin d’enlever le « *ngFor » qu’on avait utilisé précédemment.

app.component.ts
<app-search-bar [(search)]="search"></app-search-bar>

<div id="card-list">
    @for (monster of filteredMonsters(); track monster) {
        <app-playing-card [monster]="monster"/>
    }
</div>

@if (filteredMonsters().length == 0) {
    <div class="centered">No monsters found !</div>
} @else {
    <div class="centered">Found {{ filteredMonsters().length }} monsters</div>
}

Vous vous demandez sûrement à quoi pourrait bien servir ce paramètre « track ». Eh bien, Angular utilise cette expression afin de déterminer quels objets ont été ajoutés, modifiés ou supprimés du DOM afin d’effectuer le moins d’opérations possibles en cas de changements. Ce paramètre est particulièrement utile dans les cas où on a des listes avec des objets qui pourront potentiellement se retrouver avec des références différentes, alors que les objets en soi sont restés les mêmes.

Imaginez, par exemple, le cas où notre barre de recherche ne filtrerait pas simplement les objets de notre liste, mais lancerait une recherche sur un serveur qui renverrait une nouvelle liste de monstres à afficher. On prendrait ensuite cette liste et on la stockerait dans de nouvelles variables qu’on afficherait à l’écran. Maintenant, imaginons qu’on parle de notre liste avec nos 4 monstres et qu’on tape un « a » dans notre barre de recherche.

Ici notre serveur nous reverrait deux monstres qui sont déjà affichés à l’écran. Cependant, comme on stockerait ces deux monstres dans de nouvelles variables, ces dernières auraient une autre référence en mémoire. Si on garde notre « track monster », Angular va enlever tous les monstres affichés à l’écran et créer un nouveau rendu pour ces nouveaux monstres, car ces nouvelles références ne correspondent à aucune carte présente dans le DOM. En revanche, si on passe en tant que « track » un « monster.name », Angular va voir que les deux monstres sont déjà affichés à l’écran et va donc uniquement effacer les deux monstres qui ne doivent plus être affichés.

app.component.ts
<app-search-bar [(search)]=“search” >
<div id="card-list">
    @for (monster of filteredMonsters(); track monster.name) {
        <app-playing-card [monster]="monster"/>
    }
</div>
@if (filteredMonsters().length == 0) {
    <div class="centered">No monsters found !</div>
} @else {
    <div class="centered">Found {{filteredMonsters().length}} monsters</div>
}

En plus de rendre notre code plus lisible, ce « for » nous permet en plus de cela de définir du code à afficher si jamais notre liste ne retourne aucun élément. Pour cela, il suffit d’ajouter un @empty, suivi d’accolades à l’intérieur desquelles on ajoute ce qu’on veut afficher dans le cas où la liste ne retourne aucun élément. Adaptons notre code afin d’afficher notre message “No monster found !” si on n’a aucun monstre à afficher:

app.component.ts
<app-search-bar [(search)]="search"></app-search-bar>
<div id="card-list">
    @for (monster of filteredMonsters(); track monster.name) {
        <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>
}

Pour finir, j’aimerais aussi mentionner que le « for » vous met quelques variables additionnelles à disposition qui sont les suivantes:

  • $index: contient l’index de l’itération en cours
  • $count: contient le nombre total d’éléments de la liste
  • $first: « true » uniquement pour le premier élément
  • $last: « true » uniquement pour le dernier élément
  • $even: « true » pour tous les éléments pairs
  • $odd: « true » pour les éléments impairs.

Vous pouvez utiliser ces variables directement dans votre « for » comme ceci:

app.component.ts
<app-search-bar [(search)]="search"></app-search-bar>
<div id="card-list">
    @for (monster of filteredMonsters(); track monster.name) {
        <div> Index: {{$index}}</div>
        <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>
}

Ce qui nous donne le résultat suivant:

List of monsters with index

Comme vous pouvez l’imaginer, si vous utilisez des « for » imbriqués, la lecture de ses variables peut très vite devenir confuse. Au lieu de les utiliser directement, vous pouvez les assigner à des variables locales en ajoutant un point-virgule après l’instruction « track », et en définissant des variables locales et en leur assignant les valeurs que vous souhaitez utiliser:

app.component.ts
<app-search-bar [(search)]="search"></app-search-bar>
<div id="card-list">
    @for (monster of filteredMonsters(); track monster; let monsterIndex = $index) {
        <div> Index: {{monsterIndex}}</div>
        <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>
}

Si vous vous demandez si vous avez quelque chose d’équivalent pour les « *ngFor », la réponse est « oui »: vous avez exactement les mêmes variables locales, à la différence qu’elles ne commencent pas par « $ » et que vous devez obligatoirement les assigner à une variable locale. Comme le processus est identique à ce que nous avons vu ci-dessus, je vous laisse ici un lien vers la documentation officielle pour plus de détails.

Et voilà, vous savez tout sur les boucles et les conditions en Angular.

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.

Laisser un commentaire