Input, output, viewChild... Encore plus de signaux 📡
Dans un précédent article nous avons vu
comment utiliser les Signal
avec Angular. Dans cet article nous allons découvrir quelles fonctionnalités d’Angular
utilisent des signaux.
Vous trouverez un exemple complet avec le code au bas de cet article.
Au sommaire :
- input() : en entrée du composant
- output() : en sortie du composant
- viewChild(), viewChildren()
- contentChild(), contentChildren()
- model() : liaisons bidirectionnelles
- Liens et exemple complet
input() : en entrée du composant
L’une des fonctionnalités les plus courrament utilisées dans Angular est la communication entre un composant “parent” et un composant “enfant” via les entrées “input”. Avec l’introduction des signaux, cette communication devient plus facile.
Auparavant, si l’on avait un signal en entrée d’un composant, il fallait utiliser le décorateur @Input()
afin de
déclarer son “input” :
Dorénavant nous pouvons utiliser l’équivalent sous forme de signal en utilisant la fonction input()
fournie par
Angular. Cette fonction prend un paramètre de type et/ou une valeur (si une valeur est passée, le type peut être
automatiquement inféré au type de cette valeur).
Les signaux ne sont pas beaucoup moins verbeux, alors pourquoi utiliser la syntaxe sous forme de signaux plutôt que les décorateurs ? Il y a plusieurs bonnes raisons :
- On a la puissance des signaux de notre côté. Il est très facile de calculer automatiquement des valeurs dérivées
avec
computed()
. L’équivalent sous forme de décorateur serait bien plus verbeux, avec des setters et/ou Subjects RxJs. - Lorsqu’un signal utilisé dans un template change, le composant va automatiquement être marqué comme “dirty” si la stratégie de détection de changement est “OnPush” (et c’est ce que vous utilisez par défaut, pas vrai ? Pas vrai ??).
- Les signaux sont type-safe. Dans le cas du
required
, plus besoin de tricher avec Typescript pour lui assurer qu’une valeur sera définie. On peut également fournir une fonction de transformation au signal pour transformer, par exemple, un “input” de typestring
enboolean
("yes"
->true
) :
- C’est un sujet d’article en soi, mais Angular a pour ambition de devenir “zoneless” (comprendre : sans “zone.js”) et l’utilisation des signaux est une pierre angulaire pour y parvenir.
Puisque cet “input” est un signal, il s’utilise comme si on appelait une fonction pour obtenir la valeur qu’il contient :
output() : en sortie du composant
Attention : output()
ne renvoie pas du tout un signal.
La fonction output()
a été ajoutée, en remplacement du décorateur @Output()
pour être cohérente avec le reste des
changements de l’API d’Angular suite à l’introduction des signaux. Cette fonction renvoie un EventEitterRef
(similaire Ă un EventEmitter
mais sans dépendre de RxJs) et sa valeur peut être changée grâce à la méthode emit()
:
Son utilisation dans le composant parent ne change pas :
viewChild() et viewChildren()
De la même façon que pour les “input” et les “output”, une alternative aux décorateurs @ViewChild()
et @ViewChildren()
a été ajoutée.
Avec le décorateur, pour récupérer un unique enfant du composant, nous aurions utilisé un code semblable à :
Le code équivalent utilisant les signaux est un peu plus concis et peut faire usage d’ effect()
:
Mais le gros avantage, c’est surtout qu’il n’y a plus besoin d’implémenter AfterViewInit
ou de savoir quand le
composant enfant sera disponible : en effet puisqu’on utilise un signal, la valeur sera tout simplement émise lorsqu’elle
est disponible et nous pouvons commencer à l’utiliser à partir de ce moment là !
Nous pouvons mĂŞme utiliser viewChild.required(...)
si l’on souhaite qu’il y ait obligatoirement une correspondance :
Les options disponibles sur le décorateurs le sont aussi pour la fonction viewChild()
, elles peuvent être passées en
second paramètre :
On peut Ă©galement utiliser les variables du template (#mavariable
) pour identifier un élément. Par exemple si on a
<pokemon-info-evolution [pokemon]="evolution" #evolutionComponent />
:
Qu’en est-il de viewChildren()
? Il fonctionne exactement de la même manière, mais vous l’aurez deviné, il récupère
tous les éléments correspondant à la requête :
contentChild() et contentChildren()
Le principe est exactement le mĂŞme que pour viewChild()
et viewChildren()
mais appliqué aux décorateurs
@ContentChild()
et @ContentChildren()
.
Afin d’illustrer l’utilisation de viewChildren()
avec un exemple, imaginons qu’une liste arbitraire de statistiques
d’un Pokémon puisse être passée à un sous-composant pokemon-info-stats
qui aura besoin de les lister :
Le composant PokemonInfoStatsComponent
utilise viewChildren()
pour récupérer toutes les directives
<pokemon-info-stat ...>
qui ont été placées dans le contenu du composant (voir ci-dessus) :
model() : liaisons bidirectionnelles
Si vous avez travaillé avec des formulaires, vous avez forcément déjà vu ce type de notations :
Il s’agit d’un “two-way binding” ou “double data binding”. Il permet de passer une valeur à la fois en lecture (input) au
composant enfant et Ă la fois Ă la modifier (en output) lorsque le composant enfant la change. Au passage, pour se
souvenir de l’ordre des []
et des ()
pensez à la banane dans la boîte (“banana in the box”) : [(banana)]
.
Contrairement aux signaux provenant d’un input()
, les signaux renvoyés par model()
sont “writable”, on peut changer
leur valeur. Heureusement, sinon il serait impossble de faire du two-way binding ! Avec model()
, inutile de déclarer
un output ...Changed()
dans le composant, comme il fallait le faire auparavant.
Prenons un exemple. On souhaite afficher un compteur de like sur le composant racine. Un bouton “like” doit se situer sur la fiche d’info du Pokémon. Lorsqu’on clique sur le bouton “like” le compteur doit être incrémenté. Mais lorsqu’on clique sur “suivant” dans le composant racine, le compteur doit être réinitialisé.
Commençons par définir un model()
nommé “likes” dans les informations du Pokémon et y attacher le bouton “like” :
Dans le composant parent, nous affichons le nombre de likes et nous assurons que lorsqu’on change de Pokémon, le
compteur est réinitialisé. Nous définissons le compteur sous la forme d’un signal likesCounter
:
Et c’est tout ! Lorsqu’on clique sur “like”, le model likes
est mis à jour et comme il est associé en lecture (input) et en
Ă©criture (output) au signal likesCounter
, ce signal est également mis à jour et par conséquent l’affichage dans le template change.
Lorsqu’on remet à zéro le signal likesCounter
, puisqu’il est associé en input/output au model likes
, ce dernier est
également remis à zéro !
Liens et exemple complet
Vous pouvez retrouver l’exemple complet sur Stackblitz.
Voici Ă©galement quelques liens vers des sources de documentation et de quoi aller plus loin :