Retourner à : Angular 18 pour débutants
C’est reparti pour la suite de notre cours sur Angular. Dans les derniers chapitres, on a vu comment créer des propriétés dans nos composants. Nous avons parlé des inputs ainsi que des outputs, et maintenant, on va voir comment créer des propriétés qui dépendent du contenu d’autres propriétés de notre composant.
Entre autres, nous allons voir le « life cycle hook OnChanges », qui nous permettra de détecter les changements de nos inputs. Nous parlerons des différentes stratégies de détection de changements d’Angular et nous comparerons la stratégie par défaut avec la stratégie « OnPush ». Pour finir, nous expliquerons ce que sont les signaux et parlerons de l’avenir de la détection de changements.
A la fin de ce tuto, on aura le résultat suivant: une carte à jouer avec un bouton qui nous permettra de changer le contenu de la carte en un clic.

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. Et pour chaque nouveau chapitre, je pars du principe que vous avez lu les chapitres précédents.

Commencons par jeter un coup d’oeil à notre projet tel que nous l’avons laissé lors de la dernière vidéo et modifions-le, de sorte à avoir une carte à jouer à l’écran, et un bouton qui nous permettra de changer la carte à afficher. Avant de faire cela, ouvrons notre fichier « playing-card.component.ts ». Dans nos cours précédents, nous avons modifié ce fichier afin qu’il utilise un « signal input », mais pour ce chapitre, je veux qu’on commence par la notation « @Input » traditionnelle et on reviendra sur les signaux en toute fin. Donc adaptons le fichier comme suit:
import { Component, Input } from '@angular/core';
import { Monster } from '../../models/monster.model';
@Component({
selector: 'app-playing-card',
standalone: true,
imports: [],
templateUrl: './playing-card.component.html',
styleUrl: './playing-card.component.css'
})
export class PlayingCardComponent {
@Input() monster = new Monster();
}
Nous devons maintenant aussi adapter le fichier « playing-card.component.html » en enlevant les parenthèses qui suivent la variable « monster », étant donné que celle-ci n’est plus un signal:
<div id="card">
<div id="inside">
<header>
<div class="left">
<div id="name">{{monster.name}}</div>
</div>
<div class="right">
<div id="hp">{{monster.hp}} HP</div>
<img class="energy icon" src="img/electric.png" />
</div>
</header>
<figure id="art">
<img src="img/pika.png" />
<figcaption>{{monster.figureCaption}}</figcaption>
</figure>
<div id="capacities">
<div class="capacity">
<div class="main">
<div class="cost">
<img class="icon energy" src="img/electric.png"/>
<img class="icon energy" src="img/electric.png"/>
</div>
<div class="name">{{monster.attackName}}</div>
<div class="damage">{{monster.attackStrength}}</div>
</div>
</div>
<div class="description"> {{monster.attackDescription}} </div>
</div>
</div>
</div>
Maintenant ouvrons le fichier « app.component.ts » et modifions-le afin qu’il contienne: un tableau de deux monstres qu’on va appeler « monsters », l’index du monstre sélectionné qu’on va appeler « selectedMonsterIndex », et on va également créer une fonction ‘toggleMonster’ qui va permettre de sélectionner le prochain monstre du tableau:
import { Component } from '@angular/core';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { Monster } from './models/monster.model';
import { SearchBarComponent } from './components/search-bar/search-bar.component';
import { MonsterType } from './utils/monster.utils';
@Component({
selector: 'app-root',
standalone: true,
imports: [PlayingCardComponent, SearchBarComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
monsters: Monster[];
selectedMonsterIndex = 0;
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.hp = 60;
monster2.figureCaption = "N°003 Car";
this.monsters.push(monster2);
}
toggleMonster() {
this.selectedMonsterIndex = (this.selectedMonsterIndex + 1) % this.monsters.length;
}
}
Puis dans notre fichier « app.component.html », on va effacer tout le code existant et le remplacer par un bouton qui va exécuter la fonction « toggleMonster », et en dessous, on aura notre composant « app-playing-card » avec le monstre sélectionné.
<button (click)="toggleMonster()">Toggle Card</button>
<br /><br />
<div id="card-list">
<app-playing-card [monster]="monsters[selectedMonsterIndex]"/>
</div>
Là, si on regarde le résultat dans notre navigateur, on voit que les données de la carte affichée changent bien.

Maintenant, en plus des données textuelles de la carte, j’aimerais qu’on modifie également l’image et le type de la carte. Puis, dépendant de ce type, il faudra que la carte change de couleur et que l’icon indiquant le type du monstre reflète aussi ce changement.
Commencer par créer les différents types de monstres que nous souhaitons supporter dans notre application, et associons-leur une image ainsi qu’une couleur. Pour cela, on va créer un nouveau dossier qu’on va appeler « utils », dans lequel on va créer un fichier « monster.utils.ts », Dans ce fichier, on va avoir un « enum » avec les différents types de monstres. On va aussi y créer un interface, qui va contenir les différents attributs d‘un type précis. Pour finir, on y crée aussi un dictionnaire qui va contenir un mapping de chaque type de monstre vers ses propriétés:
export enum MonsterType {
PLANT = 'plant',
ELECTRIC = 'electric',
FIRE = 'fire',
WATER = 'water',
}
export interface IMonsterProperties {
imageUrl: string;
color: string;
}
export const MonsterTypeProperties: { [key: string]: IMonsterProperties } = {
[MonsterType.PLANT]: {
imageUrl: 'img/plant.png',
color: 'rgb(135, 255, 124)',
},
[MonsterType.ELECTRIC]: {
imageUrl: 'img/electric.png',
color: 'rgb(255, 255, 104)',
},
[MonsterType.FIRE]: {
imageUrl: 'img/fire.png',
color: 'rgb(255, 104, 104)',
},
[MonsterType.WATER]: {
imageUrl: 'img/water.png',
color: 'rgb(118, 234, 255)',
},
};
Maintenant, retournons voir notre classe « monster.model.ts » et adaptons-la, afin d’y ajouter le type du monstre ainsi que son image:
import { MonsterType } from "../utils/monster.utils";
export class Monster {
name: string = "Monster";
type: MonsterType = MonsterType.ELECTRIC;
image: string = "img/pika.png";
hp: number = 60;
figureCaption: string = "N°001 Monster";
attackName: string = "Standard Attack";
attackStrength: number = 10;
attackDescription: string = "This is an attack description...";
}
Une fois que nous avons fait ceci, nous pouvons retourner voir notre fichier « app.component.ts » afin d‘y modifier l’un de nos monstres, pour faire en sorte d’avoir deux monstres de type différent avec des images différentes:
...
import { MonsterType } from './utils/monster.utils';
...
constructor() {
…
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);
…
}
...
Adaptons un peu notre « playing-card.component.ts » afin d’y ajouter deux propriétés qu’on modifiera plus tard. La première propriété sera la propriété « monsterTypeIcon » qui contiendra l’URL de l’icon associé au type du monstre, et la deuxième propriété « backgroundColor » contiendra la couleur de la carte correspondant au type du monstre en question.
...
export class PlayingCardComponent {
@Input() monster = new Monster();
monsterTypeIcon: string = 'img/electric.png';
backgroundColor: string = 'rgb(255, 255, 104)';
}
Maintenant, adaptons le fichier « playing-card.component.html » afin qu’il affiche la nouvelle propriété d’image de notre classe « Monster » qu’il adapte la couleur d’arrière-plan en fonction de la propriété « backgroundColor » et qu’il affiche l’icon « monsterTypeIcon »:
<div id="card">
<div id="inside" [style.background-color]="backgroundColor">
<header>
<div class="left">
<div id="name">{{monster.name}}</div>
</div>
<div class="right">
<div id="hp">{{monster.hp}} HP</div>
<img class="energy icon" [src]="monsterTypeIcon">
</div>
</header>
<figure id="art">
<img [src]="monster.image" />
<figcaption>{{monster.figureCaption}}</figcaption>
</figure>
<div id="capacities">
<div class="capacity">
<div class="main">
<div class="cost">
<img class="icon energy" src="img/electric.png"/>
<img class="icon energy" src="img/electric.png"/>
</div>
<div class="name">{{monster.attackName}}</div>
<div class="damage">{{monster.attackStrength}}</div>
</div>
</div>
<div class="description">{{monster.attackDescription}}</div>
</div>
</div>
</div>
Si on relance maintenant notre application et qu’on regarde le résultat, on voit que l’image du monstre est bien adaptée à chaque fois qu’on clique sur notre bouton. En revanche, nous devons encore nous occuper de son type et de la couleur de la carte.

A ce sujet, avant de vous montrer une solution potentielle, je vais vous montrer des idées qu’on aurait pu avoir, mais qui ne marchent pas.
Retournons dans notre fichier « playing-card.component.ts » pour y faire nos modifications. La première méthode à laquelle on aurait pu penser aurait été de mettre à jour nos propriétés « monsterTypeIcon » et « backgroundColor » dans notre constructeur, comme ceci:
...
import { Monster } from '../../models/monster.model';
import { MonsterTypeProperties } from '../../utils/monster.utils';
...
constructor() {
this.monsterTypeIcon = MonsterTypeProperties[this.monster.type].imageUrl;
this.backgroundColor = MonsterTypeProperties[this.monster.type].color;
}
...
Si on relance notre code, on voit que rien n’a changé. Même si on appuie sur le bouton « Toggle Monster », les textes sont bien adaptés, mais le type et la couleur de la carte ne le sont pas.

On pourrait se dire que c’est normal, car comme on a mis le code dans le constructeur, les propriétés ne sont calculées que pour la première carte…Alors faisons un test et sélectionnons la deuxième carte en tant que première carte à afficher en changeant la variable « selectedMonsterIndex » dans le fichier « app.component.ts » à 1:

Et là, surprise, même quand la première carte à afficher est d’un type différent du type par défaut, rien ne change. Et c’est tout à fait normal, car le constructeur est appelé avant que les inputs de notre composant ne soient initialisés.
Alors on pourrait penser à utiliser le « life-cycle hook OnInit » qui est exécuté une fois que notre composant est complètement initialisé. Faisons cela tout de suite en retournant dans notre fichier « playing-card.component.ts »:
import { Component, Input, OnInit } from '@angular/core';
import { Monster } from '../../models/monster.model';
import { MonsterTypeProperties } from '../../utils/monster.utils';
...
@Component({
selector: 'app-playing-card',
standalone: true,
imports: [],
templateUrl: './playing-card.component.html',
styleUrl: './playing-card.component.css'
})
export class PlayingCardComponent implements OnInit {
@Input() monster: Monster = new Monster();
monsterTypeIcon: string = 'img/electric.png';
backgroundColor: string = 'rgb(255,255,104)';
ngOnInit(): void {
this.monsterTypeIcon = MonsterTypeProperties[this.monster.type].imageUrl;
this.backgroundColor = MonsterTypeProperties[this.monster.type].color;
}
}
Si on retourne voir notre navigateur, notre carte est bien bleue! En revanche, si on appuie sur le bouton « Toggle Monster » pour sélectionner le prochain monstre, rien à faire, la carte reste bleue. Là aussi, c’est normal, car on vient de mettre à jour les paramètres après l’initialisation de notre composant, donc après la création de notre carte. Ici on ne recrée pas de nouvelle carte, mais on modifie la carte existante.

Donc, pour détecter ces changements, on à un autre « life-cycle hook » qui s’appelle « OnChanges ». Pour implémenter « OnChanges », on doit rajouter la fonction « ngOnChanges » à notre code. Cette fonction prend un paramètre de type « SimpleChanges ». Ce paramètre est constitué d’un dictionnaire qui contient, en tant que clé, le nom de chacun des inputs qui a changé. Pour chacun de ces inputs, on y retrouve la valeur précédente ainsi que la nouvelle valeur de celui-ci.
Pour implémenter « OnChanges », on peut modifier notre code comme ceci:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Monster } from '../../models/monster.model';
import { MonsterTypeProperties } from '../../utils/monster.utils';
...
export class PlayingCardComponent implements OnChanges {
...
ngOnChanges(changes: SimpleChanges): void {
if (changes['monster']) {
if (changes['monster'].previousValue?.type !== changes['monster'].currentValue.type) {
this.monsterTypeIcon = MonsterTypeProperties[this.monster.type].imageUrl;
this.backgroundColor = MonsterTypeProperties[this.monster.type].color;
}
}
}
}
Cette fois-ci, si on retourne dans notre navigateur, on voit que le couleur de la carte et le type du monstre sont bien adaptés à chaque changement.

Avant d’expliquer ce que sont les signaux, j’aimerais qu’on parle de détection de changements en Angular. Par quel miracle Angular sait-il qu’il faut adapter notre carte quand on clique sur le bouton « Toggle Monster » ?
Aujourd’hui, Angular utilise une librairie qui s’appelle « zone.js » qui va se charger de détecter les changements. A chaque fois que l’utilisateur interagit avec notre application Angular via des inputs, des boutons ou autre, ou alors quand des résultats d’appels asynchrones sont reçus (par exemple, les résultats d’un appel API), « zone.js » va intercepter ces évènements et en informer Angular. A ce stade, dans sa stratégie par défaut, Angular va partir du principe que tout évènement notifié par « zone.js » peut avoir un impact sur n’importe quel composant affiché à l’écran, et Angular va donc vérifier pour chaque composant s’il doit le mettre à jour ou non. Pour cela, Angular va commencer par le composant « root » et va ensuite parcourir un à un chaque composant de l’arbre de composants.

C’est pour cela que lorsqu’on clique sur notre bouton « toggleMonster » qui à priori est indépendant de notre carte. Angular vérifie tout de même le composant lié à notre carte et applique immédiatement le changement à l’écran.
Ce comportement est super, étant donné que nous pouvons modifier nos valeurs n’importe où dans notre code, tout en étant sûr que le rendu de chaque composant qui en dépend sera bien mis à jour. En revanche, au niveau des performances, on pourrait mieux faire, car on vérifie tous les composants à chaque modification, même pour des modifications qui n’impactent qu’un nombre limité de composants.
Cela tombe bien, Angular propose une autre stratégie de détection de changement qui permet d’améliorer ce comportement de détection de changement et qui s’appelle la stratégie « OnPush ». « OnPush » doit être activé explicitement pour chaque composant où vous souhaitez l’utiliser et permet d’indiquer à Angular que le composant ne souhaite pas être mis à jour, sauf lorsque celui-ci est marqué pour validation. Ce marquage est uniquement fait sous certaines conditions, qui sont les suivantes:
- La valeur d’un input du composant a changé (attention: ici Angular va comparer les valeurs par référence)
- Un évènement a eu lieu à l’intérieur du composant, comme, p. ex. un clic ou autre
- Un « pipe async » a reçu une nouvelle valeur (si vous ne savez pas encore ce qu’est un « pipe async », ne vous en faites pas, on en reparlera en temps voulu)
- Ou alors le composant a été marqué manuellement pour vérification
Il est important de noter que, dans un composant OnPush, les modifications locales de propriétés effectuées à l’intérieur de setTimeout, setInterval, d’une Promise ou d’un Observable ne marquent pas automatiquement le composant pour vérification.
Imaginons un arbre de composant avec deux composants enfant qu’on va appeler « B » et « C ». Le composant « B » utilisant la stratégie par défaut, et le composant « C » utilisant la stratégie « OnPush », avec chacun de ces composants ayant à leur tour deux composants fils avec chacun la stratégie « OnPush ».

Si un évènement arrive dans le composant avec la stratégie par défaut, et que cet évènement n’impacte aucun autre composant, il n’y aura que le composant « root » et le composant avec la stratégie par défaut qui seront vérifiés lors du cycle de détection de changements. Tous les autres composants avec la stratégie « OnPush » seront ignorés dans ce cas.

Maintenant si nous considérons qu’un évènement « click » est effectué sur le composant « C » et que cet évènement change un input de l’un des composant fils du composant « C ». Dans ce cas, le composant « B », qui utilise la stratégie par défaut, sera vérifié, malgré qu’il ne soit concerné par aucun changement. Le composant « C » sera vérifié, car l’évènement « click » y a eu lieu, et pour finir, le composant fils dont l’input a changé sera lui aussi vérifié. Tous les autres composants seront ignorés, car ils n’ont pas été marqués pour vérification.

Pour le moment, je ne vais pas entrer dans les détails de la stratégie « OnPush », car quand on débute, on peut très vite se retrouver dans une situation où notre composant ne se rafraîchit pas, sans qu’on ne comprenne pourquoi. Mais je voulais tout de même vous en parler ici afin de mieux vous expliquer où les signaux entrent en jeu et à quoi ils serviront à terme.
Passons maintenant aux signaux. Angular 16 à introduit les signaux sous forme de trois nouvelles primitives. La première primitive étant la primitive « signal » qui peut être considérée comme une variable qui peut être écrite et lue, mais qui, en plus de cela, notifie Angular quand sa valeur change.
Ensuite, nous avons la primitive « computed », qui est un type de signal auquel on ne peut pas assigner de valeur, mais qui va calculer sa valeur en utilisant la valeur d’un ou plusieurs autres signaux. Ce champ « computed » a la particularité qu’il est notifié par Angular quand un des signaux qu’il utilise est modifié et peut donc se mettre à jour automatiquement à chaque fois que la valeur de ces signaux change.
Pour finir, nous avons la primitive « effect » qui va définir une fonction arbitraire qui sera exécutée à chaque fois qu’un signal utilisé dans celle-ci sera mis à jour.
En plus de cela, Angular 17 a apporté les ‘signal inputs’ et les ‘model inputs’ qu’on a déjà vu dans les vidéos précédentes.
Maintenant qu’on a vu la théorie, le plus simple, c’est de passer à du code et de voir comment utiliser tout ça. Pour commencer, je propose d’ouvrir le fichier « app.component.ts » et de transformer notre variable « selectedMonsterIndex » en un signal. Pour cela, on va dire que la variable est égale à la fonction « signal » qu’on importe d’ « @angular/core ». On peut indiquer le type du signal entre les symboles ‘<‘ et ‘>’, suivi de parenthèses. A l’intérieur des parenthèses, on peut lui assigner une valeur par défaut, ce qui nous donne:
import { Component, signal } from '@angular/core';
…
export class AppComponent {
monsters!: Monster[];
selectedMonsterIndex = signal(0);
…
}
Notre variable est maintenant un signal: pour pouvoir accéder à sa valeur, on doit utiliser des parenthèses, et pour modifier sa valeur, on doit utiliser la fonction « set »:
…
toggleMonster() {
this.selectedMonsterIndex.set((this.selectedMonsterIndex() + 1) % this.monsters.length);
}
…
Nous devons également adapter notre fichier « app.component.html » en ajoutant des parenthèses au signal afin de bien utiliser sa valeur:
<button (click)="toggleMonster()">Toggle Card</button>
<br /><br />
<div id="card-list">
<app-playing-card [monster]="monsters[selectedMonsterIndex()]"/>
</div>
Si vous ouvrez votre navigateur, vous pouvez constater que notre application tourne comme avant.

Maintenant, je propose de créer un nouvel attribut qu’on va appeler « selectedMonster » et qui sera un signal calculé sur base de notre signal « selectedMonsterIndex » et qui contiendra le monstre sélectionné. Pour cela, on va créer l’attribut « selectedMonster » qui sera égal à la fonction « computed » qu’on importe d’ « @angular/core » et, entre parenthèses, on va définir une fonction qui retourne le monstre en question:
import { Component, signal, computed } from '@angular/core';
…
export class AppComponent {
monsters!: Monster[];
selectedMonsterIndex = signal(0);
selectedMonster = computed(() => {
return this.monsters[this.selectedMonsterIndex()];
});
…
}
Notez que ce signal sera recalculé automatiquement à chaque fois que l’index changera, gardant ainsi le signal « selectedMonster » à jour. En plus de cela, afin de vous montrer comment la fonction « effect » fonctionne, je propose de rajouter dans notre constructeur un « effect » qui va simplement imprimer le monstre sélectionné à chaque fois que celui-ci change dans la console. Pour cela, on va faire la chose suivante:
import { Component, signal, computed, effect } from '@angular/core';
…
export class AppComponent {
…
constructor() {
effect(() => {
console.log(this.selectedMonster());
});
…
}
…
}
On peut maintenant adapté notre fichier « app.component.html » afin qu’il utilise le signal « selectedMonster »:
<button (click)="toggleMonster()">Toggle Card</button>
<br /><br />
<div id="card-list">
<app-playing-card [monster]="selectedMonster()"/>
</div>
Ce qu’on vient de faire est déjà très bien, mais on peut aller plus loin. Ouvrons de nouveau notre fichier « playing-card.components.ts », et là, on va commencer par modifier notre input afin d’y utiliser un « signal input ». Ensuite, on va modifier nos propriétés « monsterTypeIcon » et « backgroundColor », afin d’en faire des signaux « computed »:
import { Component, computed, input } from '@angular/core';
import { Monster } from '../../models/monster.model';
import { MonsterTypeProperties } from '../../utils/monster.utils';
@Component({
selector: 'app-playing-card',
standalone: true,
imports: [],
templateUrl: './playing-card.component.html',
styleUrl: './playing-card.component.css'
})
export class PlayingCardComponent {
monster = input<Monster>(new Monster());
monsterTypeIcon = computed(() => {
return MonsterTypeProperties[this.monster().type].imageUrl;
});
backgroundColor = computed(() => {
return MonsterTypeProperties[this.monster().type].color;
});
}
N’oublions pas d’adapter notre fichier « playing-card.component.ts » en y rajoutant des parenthèses à chaque fois qu’on veut accéder à la valeur de l’un de nos signaux:
<div id="card">
<div id="inside" [style.background-color]="backgroundColor()">
<header>
<div class="left">
<div id="name">{{ monster().name }}</div>
</div>
<div class="right">
<div id="hp">{{ monster().hp }} HP</div>
<img class="energy icon" [src]="monsterTypeIcon()">
</div>
</header>
<figure id="art">
<img [src]="monster().image" />
<figcaption>{{ monster().figureCaption }}</figcaption>
</figure>
<div id="capacities">
<div class="capacity">
<div class="main">
<div class="cost">
<img class="icon energy" src="img/electric.png"/>
<img class="icon energy" src="img/electric.png"/>
</div>
<div class="name">{{ monster().attackName }}</div>
<div class="damage">{{ monster().attackStrength }}</div>
</div>
</div>
<div class="description"> {{ monster().attackDescription }} </div>
</div>
</div>
</div>
Voilà, si on ouvre le tout dans notre navigateur, on voit que notre bouton « Toggle Monster » fonctionne toujours, et qu’à chaque fois qu’on clique dessus, on a bien notre « effect » qui logue le monstre en question dans la console. Donc on a le même comportement, avec moins de code et basé sur les signaux.

Maintenant vous vous posez sûrement la question « Quel est le rapport avec la détection de changement? »; et aujourd’hui la réponse est: « aucun », car effectivement à l’heure actuelle, Angular utilise encore « zone.js », et donc la détection de changement par défaut ou « OnPush », telle qu’on l’a vue, il y a quelques instants. Mais l’intérêt des signaux est, qu’à terme, Angular veut se débarrasser de « zone.js » et utiliser les signaux afin de déterminer quels changements ont un impact sur quels composants. Donc, si on revient sur notre exemple d’un arbre de composants comme celui-ci:

Si un évènement « click » modifie un signal qui est utilisé en tant que ‘signal input’ d’un composant enfant, qui est lui-même utilisé directement ou indirectement dans le rendu à l’écran. Eh bien, Angular sera capable de lancer la détection de changement uniquement sur ces deux composants de manière ciblée, sans traverser l’arbre de composants de haut en bas. Et là, on aurait un gain significatif de performance.

Voilà qui conclut ce chapitre. Maintenant les connaisseurs parmi vous se demandent sûrement pourquoi je n’ai pas parlé de « RXJS », et bien entendu on en parlera, mais pas pour l’instant, car avant de passer à « RXJS », il y a encore plein d’autres sujets de base que je veux aborder. Mais patience, on y viendra.
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.