I. Comment lire ce tutoriel ?▲
Il y a deux façons de profiter de ce tutoriel, selon votre objectif. Si vous ne connaissez pas l’utilisation des pipes avec Angular, je vous invite à installer un socle d’application sur votre machine et tester les exemples de code au fur et à mesure que vous avancez dans le tutoriel. Dans ce cas, vous devez lire le tutoriel dans l’ordre, du haut vers le bas.
Si vous utilisez déjà des pipes dans vos projets Angular, alors vous pouvez utiliser le menu de cet article pour lire (ou relire) seulement la partie qui vous intéresse : « Comment paramétrer un pipe ? », « Comment développer un pipe personnalisé ? », « Qu’est-ce que les pipes purs et impurs ? », etc.
I-A. Installation d’un socle initial avec Angular-CLI▲
Nous allons démarrer rapidement un nouveau projet Angular avec Angular-CLI. Il s’agit d’un outil conçu pour faciliter les tâches de développement les plus courantes : démarrer un petit serveur de développement, créer un nouveau composant, exécuter des tests automatisés, etc. Cet outil va nous permettre de démarrer un nouveau projet Angular en quelques lignes de commandes seulement, afin de mettre en pratique certains exemples de code.
L’installation et l’utilisation d’Angular-CLI sont assez simples. Pour installer ou mettre à jour Angular-CLI, utilisez la commande suivante :
npm install –g @angular/cli
Ensuite, générez un nouveau projet. La commande suivante va créer un nouveau dossier ng-pipes, avec le contenu de votre projet à l’intérieur :
ng new ng-pipes
Déplacez-vous dans l’arborescence des dossiers, pour pointer vers notre application.
cd ng-pipes/
Démarrez ensuite notre application :
ng serve --open
Cette commande va démarrer un petit serveur de développement, afin d’héberger notre application. L’option open va directement afficher l’application dans un navigateur.
Vous êtes maintenant prêt pour commencer !
Laissez tourner la commande ng serve pendant que vous suivez ce tutoriel. Le rendu de votre navigateur sera automatiquement mis à jour en fonction des modifications que vous apporterez à votre code. Si vous suivez ce tutoriel à différents moments, pensez à redémarrer la commande ng serve avant de commencer à travailler.
Si vous travaillez sur un environnement Mac et que la commande ng n’est pas reconnue, vous devrez surement ajouter l’alias de la commande ng vous-même :
vim ~/.bash_profile
alias ng= “/Users/<votre_nom>/npm/lib/node_modules/@angular/cli/bin/ng”
Enregistrez vos modifications et redémarrez votre terminal. La commande suivante devrait maintenant fonctionner :
ng—version
Voici le lien vers la correction sur Github.
I-B. Prérequis▲
Avant de commencer à suivre ce tutoriel, il est recommandé de connaître un minimum Angular, à savoir connaître au moins le fonctionnement de base d’un composant et d’un template. Si vous savez déjà ce qu'est un pipe, c’est un plus, mais ce n’est pas nécessaire pour suivre ce tutoriel.
De plus, les exemples de code ci-dessous sont en TypeScript, donc cet article nécessite un minimum de connaissance sur ce sujet également.
II. Les pipes▲
II-A. Qu’est-ce qu’un « pipe » ? Et à quoi ça sert ? ▲
Oh là, cela fait déjà deux questions !
Essayons d’aborder la chose en douceur. Pensez à toutes les applications que vous avez développées (ou allez développer). Toutes ces applications sont en fait assez semblables : vous obtenez des données, vous effectuez des transformations dessus, et ensuite vous les affichez aux utilisateurs.
Obtenir ces données peut être aussi simple que de déclarer une nouvelle variable dans votre composant, ou aussi complexe que la diffusion en temps réel à travers des WebSocket. Dans tous les cas, une fois que les données arrivent, vous avez deux choix.
- Vous pouvez les afficher directement dans la vue. Vous recevez le prénom « Jean », et vous affichez « Jean » dans votre template. Rien de compliqué, vous n’avez pas besoin d’effectuer de transformations dans ce cas.
- Imaginez maintenant que vous recevez la date « Fri Apr 15 1988 00:00:00 GMT-0700 » depuis le serveur que vous avez appelé. Est-ce que vous avez envie d’afficher cette date à vos utilisateurs ? Personnellement, en tant qu’utilisateur, je préférerais visualiser une date dans un format plus lisible, comme « 15 avril 1988 ».
En fait, certaines valeurs gagnent à être légèrement retouchées avant d’être affichées dans le template. Et on voudra souvent effectuer les mêmes transformations à plusieurs reprises au sein d’un projet.
Vous pouvez essayer de voir ces transformations comme des règles de style CSS. Vous pouvez les appliquer dans vos différents templates HTML, et centraliser leur définition dans un fichier commun.
Eh bien, c’est cela les pipes avec Angular : un moyen d’écrire des transformations pour nos données, que l’on peut ensuite appliquer dans nos templates.
Pour les développeurs AngularJS, sachez que les pipes d’Angular remplacent les filtres présents dans AngularJS.
II-B. Utiliser un pipe : les principes de base▲
Le fonctionnement d’un pipe est plutôt simple : un pipe reçoit des données en entrée, et les transforme dans le format de sortie souhaité. Il s’agit d’une règle d’or, ce fonctionnement est valable, quel que soit le pipe que vous appliquez : pipe avec ou sans paramètres, personnalisé ou natif. Nous verrons tous ces cas un peu plus loin dans ce tutoriel.
Pour commencer, je vous propose de voir comment utiliser un pipe pour transformer une date d’anniversaire en une date sympathique à afficher à l’utilisateur. Vous pouvez bien sûr mettre votre propre date d’anniversaire à la place de celle que je propose ci-dessous :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
import
{
Component }
from
'@angular/core'
;
@Component
({
selector
:
'app-root'
,
template
:
`<p>Mon anniversaire est le : {{ birthday | date }}</p>`
// L’affichage avec le pipe donne : « Dec 19, 1992 »
}
)
export
class
AppComponent {
birthday =
new
Date
(
1992
,
11
,
19
);
// Le nombre 11 correspond au mois de décembre, car ça commence à zéro !
}
Pour le moment, on ajoute le code de notre template directement dans le fichier du composant, et on ignore le fichier du template app.component.html généré par Angular-cli.
Concentrez-vous sur le template du composant, à la ligne 5 ci-dessous :
<p>Mon anniversaire est le : {{ birthday | date }}</p>
À l’intérieur de cette expression d’interpolation, vous pouvez apercevoir votre premier pipe ! Voici comment cela fonctionne :
- D’abord, tout à gauche de cette expression, on a une propriété birthday.
- Ensuite, on applique une transformation sur notre propriété grâce à l’opérateur de pipe qui est la barre verticale (|).
- Enfin, on ajoute le pipe date à appliquer (à droite).
Retenez que le symbole « | » est l’opérateur des pipes.
Sans l’application du pipe date, voici ce que j’ai dans mon template :
Et en appliquant simplement le pipe Date :
|
Figure 2 - Affichage de la même date, avec le pipe Date ! |
C’est quand même plus sympathique comme affichage pour l’utilisateur !
Tous les pipes fonctionnent de cette manière : valeur d’entrée + opérateur de pipe + pipe à appliquer.
II-C. Les pipes natifs▲
Avez-vous remarqué que pour pouvoir utiliser le pipe Date, nous n’avons rien eu besoin d’importer, précédemment ? Nous avons simplement appliqué directement le pipe Date dans notre template.
En fait, Angular est proposé avec un ensemble de pipes de base. On les appelle les pipes « natifs ». Ils permettent de couvrir plusieurs usages de base, et sont des pipes disponibles de base avec Angular :
- DatePipe : permet de formater l’affichage d’une date ;
- UpperCasePipe : transforme le texte passé en entrée en majuscule ;
- LowerCasePipe : transforme le texte passé en entrée en minuscule ;
- TitleCasePipe : pour une phrase passée en paramètre, permet de mettre la première lettre de chaque mot en majuscule, et le reste en minuscule ;
- CurrencyPipe : prend en paramètre une devise pour mettre en forme un nombre comme monnaie ;
- PercentPipe : formate un nombre en tant que pourcentage. Par exemple, le chiffre 0.26 devient 26 % lorsque vous appliquez ce pipe ;
- JsonPipe : convertit la valeur passée en paramètre en chaîne JSON, grâce à la méthode JSON.stringify. Ce pipe peut être utile pour débugger votre code et retrouver plus facilement vos erreurs.
Je vous ai listé les pipes principaux, ceux que vous utiliserez le plus souvent lors de vos développements. Cependant, je dois quand même vous donner la liste complète des pipes natifs. La liste complète est disponible sur cette page de la documentation officielle.
Vous trouverez pour chaque pipe toutes les informations nécessaires, ainsi qu’un exemple d’utilisation pour chaque cas.
Parmi la liste des pipes, évitez bien sûr d’utiliser les éléments ayant « Deprecated » dans leur nom. Il s’agit de pipes qui ne sont plus maintenus par l’équipe d’Angular, et qui sont appelés à disparaître !
Angular ne dispose pas des filtres FilterPipe ou OrderByPipe qui étaient populaires sous AngularJS, pour les raisons que je vous donnerai un peu plus tard. Il s’agit essentiellement d’une problématique de performance.
II-D. Paramétrer un pipe▲
Il est possible de passer des paramètres à certains pipes, afin de personnaliser leur rendu. Concernant les paramètres des pipes, il y a deux règles d’or à retenir :
- D’abord, un pipe peut accepter n’importe quel nombre de paramètres d’entrée.
- Ensuite, tous les paramètres attendus sont forcément optionnels. C’est-à-dire qu’un pipe doit toujours pouvoir s’utiliser sans ses paramètres éventuels. Il faut donc prévoir un cas d’utilisation par défaut, quel que soit le nombre de paramètres prévus.
Pour passer des paramètres à un pipe, cela se fait dans votre template. Vous devez ajouter un deux-points « : » après le nom du pipe, puis la valeur du paramètre. Par exemple, dans le cas d’une devise en dollars canadiens, il faut passer le paramètre 'currency' avec la valeur 'CAD' :
Le prix en dollars canadiens est : {{ 100 | currency:'CAD' }}. // Ce pipe affichera 'CA$100'
Si le pipe accepte plusieurs paramètres, séparez chacun des paramètres avec un deux-points « : ».
Dans le cas de notre date d’anniversaire, nous allons modifier notre template pour passer au pipe date un paramètre, afin d’adapter le format de la date en sortie. Après le formatage, la date d’anniversaire du 19 décembre deviendra « 19/12/1992 » :
<p> Mon anniversaire est le : {{ birthday | date:"dd/MM/yyyy" }}</p>
Voilà, maintenant ce template affichera un beau message à l’utilisateur : « Mon anniversaire est le 19/12/1992 ». C’est quand même plus agréable pour l’utilisateur, que d’afficher la date brute !
II-E. Contrôler dynamiquement la valeur d’un paramètre▲
La valeur du paramètre de votre pipe n’est pas forcément statique. Cette valeur peut tout à fait être dynamique, et l’affichage de votre pipe peut être modifié en fonction d’un événement quelconque : un Timer, une action de l’utilisateur, etc.
La valeur du paramètre peut être n’importe quelle expression de template valide, comme une chaîne de caractères ou une propriété du composant associé. Dans notre exemple de date d’anniversaire, cela signifie que vous pouvez contrôler le format de sortie de la date en passant par une propriété du composant.
Je vous propose d’ajouter un bouton dans notre template qui nous permet d’afficher la date d’anniversaire soit au format britannique (‘19 December 1992’), soit au format américain (‘December 19, 1992’). Par défaut, la date d’anniversaire sera affichée au format britannique.
Voyons cela en pratique. Récrivez notre composant pour qu’il lie le paramètre format du pipe à une nouvelle propriété format du composant, que nous allons ajouter. Voici déjà le template mis à jour :
2.
3.
4.
template
:
`
<p>Mon anniversaire est le : {{ birthday | date:format }}</p>
<button (click)="toggleFormat()">Changer le format</button>
`
On a ajouté deux nouveaux éléments dans cet extrait de code :
- À la ligne 2, le paramètre du pipe date a été remplacé par la variable format plutôt qu’une valeur écrite en dure.
- À la ligne 3, j’ai ajouté un nouveau bouton dans le template, qui appelle la méthode toggleFormat lorsqu’on clique dessus. Cette méthode permet de basculer le format de la date entre le format américain et le format britannique.
Et voici le code du composant à jour :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
export
class
AppComponent {
birthday =
new
Date
(
1992
,
11
,
19
);
britishFormat =
'd MMMM y'
;
americanFormat =
'MMMM d, y'
;
toggle =
true
;
// Le format britannique est le format par défaut
get
format
(
) {
return
this
.
toggle ?
this
.
britishFormat
:
this
.
americanFormat;
}
toggleFormat
(
) {
this
.
toggle =
!
this
.
toggle;
}
}
Lorsque vous cliquez sur le bouton, la date affichée alterne entre « 19 December 1992 » et « December 19, 1992 ».
Si vous vous demandez quel est le rôle du mot clé get devant la méthode format, sachez qu’il s’agit de la syntaxe d’un getter avec TypeScript.
II-F. Combiner les pipes▲
Vous pouvez combiner plusieurs pipes afin d’obtenir le résultat que vous souhaitez. Dans l’exemple suivant, on souhaite afficher la date d’anniversaire en majuscule. Pour cela, on combine deux pipes natifs différents sur la date d’anniversaire : le pipe DatePipe et UpperCasePipe :
<
p>
Mon anniversaire est le
:
{{
birthday |
date
:
format |
uppercase }}</
p>
Cet exemple affichera notre date d’anniversaire « 19 DECEMBER 1992 » en majuscule. Voilà, rien de très compliqué !
Lorsque vous combinez plusieurs pipes, ils s’appliquent de la gauche vers la droite par rapport à leur ordre de déclaration. Dans l’exemple ci-dessus, c’est d’abord le pipe Date qui est appliqué, et ensuite le pipe Uppercase.
III. Les pipes et la détection de changement▲
Nous allons commencer avec un peu de théorie. Ensuite, nous développerons deux pipes personnalisés (oui, deux !), afin de bien illustrer tous les concepts théoriques que nous aurons vus.
Il y a un point que nous n’avons pas encore vu, c’est celui de la performance et du fonctionnement interne d’Angular avec les pipes. En effet, en tant que développeur, vous avez une responsabilité, c’est de développer une application qui soit optimisée et fonctionnelle. Vous ne devez pas consommer plus de ressources que nécessaire simplement parce que votre code est mal écrit.
Vous devez donc savoir comment Angular fonctionne en interne avec les pipes, afin de ne pas avoir la mauvaise surprise de vous retrouver avec une application très lente à la fin de vos développements.
Pour détecter les changements de valeurs au sein de vos pipes, Angular recherche des changements de valeurs liées aux données, grâce à un processus de détection de changement qui s’exécute après chaque événement du DOM : chaque frappe de clavier, déplacement de la souris, les réponses du serveur, etc. Tout cela peut être coûteux. Angular s’efforce de réduire le coût chaque fois que cela est possible.
Cependant, Angular choisit un algorithme de détection de changement plus simple et plus rapide lorsque vous utilisez un pipe. C’est pourquoi vous devez comprendre un minimum comment cela fonctionne afin d’utiliser les pipes au mieux, et de développer une application fiable.
Mais ne vous inquiétez pas, on va voir tout cela ensemble.
III-A. La stratégie par défaut de détection de changement▲
Dans l’exemple que nous allons voir, le composant utilise la stratégie de détection des changements par défaut. Il s’agit de la stratégie de détection de changement la plus agressive, c’est-à-dire la façon de faire la plus gourmande en ressource (la plus « bourrin », si vous voulez).
Je vous montre comment l’utiliser à titre d’exemple uniquement, ne reproduisez pas ça chez vous bien entendu.
Prenons comme exemple une liste d’animaux quelconque, en sachant que certains animaux volent, et d’autres non. À chaque fois que l’utilisateur ajoute un nouvel animal, nous allons surveiller cet ajout, et mettre à jour l’affichage du template en conséquence. Voici le template du composant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
<
h1>Les animaux</h1>
Ajouter un animal:
<
input type="text" #box
(
keyup.enter
)=
"addAnimal(box.value); box.value=''"
placeholder
=
"Nom de l’animal"/>
<
button (click)="reset()">Reset</button>
<
h2>Tous les animaux</h2>
<
div *ngFor="let animal of animals">
{{animal.name
}}
<
/div>
La classe du composant associé fournit un certain nombre d’éléments au tableau animals. Elle permet aussi d’ajouter de nouveaux animaux dans le tableau, et peut les réinitialiser également.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
export
class
AppComponent implements
OnInit {
animals
:
any
[]
=
[];
canFly =
true
;
ngOnInit
(
) {
this
.
animals =
[
{
name
:
'Lion'
,
canFly
:
false
},
{
name
:
'Eagle'
,
canFly
:
true
},
{
name
:
'Tortoise'
,
canFly
:
false
},
{
name
:
'Dog'
,
canFly
:
false
},
{
name
:
'Seagull'
,
canFly
:
true
},
{
name
:
'Snake'
,
canFly
:
false
},
]
}
// On ajoute un nouvel animal dans le tableau des animaux
addAnimal
(
name
:
string
) {
name =
name.trim
(
);
if
(!
name) {
return
;
}
let
animal =
{
name,
canFly
:
this
.
canFly};
this
.
animals.push
(
animal);
this
.
animals =
this
.
animals.slice
(
);
}
// On réinitialise le tableau des animaux
reset
(
) {
this
.
animals =
[];
}
}
Vous pouvez essayer d’ajouter des animaux depuis l’interface. Par défaut, tous les animaux que vous ajoutez sont volants (ligne 3 ci-dessus). Comme vous pourrez le voir, Angular met à jour l’interface pour la liste de tous les animaux. Si vous cliquez sur le bouton de réinitialisation, Angular remplace les animaux par un nouveau tableau vide, et met à jour l’affichage en conséquence.
Si vous avez ajouté la possibilité de supprimer ou de modifier un animal, Angular détecterait ces changements et mettrait également à jour l’affichage.
Maintenant, passons à ce qui nous intéresse, les pipes.
III-B. FlyingAnimalsPipe▲
Ajoutez le code HTML ci-dessous dans votre template. Ce code permet de filtrer la liste des animaux pour afficher uniquement les animaux volants :
2.
3.
4.
<
h2>
Animaux volants</
h2>
<
div *
ngFor=
"let animal of animals | flyingAnimal"
>
{{
animal.
name}}
</
div>
Ensuite, créons ce nouveau pipe FlyingAnimalsPipe que nous appelons dans notre template. Pour créer notre pipe, on utilise une commande de Angular-CLI:
Ng generate pipe flying-animal
Notre pipe a été généré par Angular-CLI, qui s’occupe automatiquement pour nous de déclarer notre nouveau pipe auprès du module racine de notre application.
Maintenant, modifiez le code du pipe comme ceci :
2.
3.
4.
5.
6.
7.
8.
9.
import
{
Pipe,
PipeTransform }
from
'@angular/core'
;
import
{
Flyer }
from
'./animals'
;
@Pipe
({
name
:
'flyingAnimals'
}
)
export
class
FlyingAnimalsPipe implements
PipeTransform {
transform
(
allAnimals
:
Flyer[]
) {
return
allAnimals.filter
(
animal =>
animal.
canFly);
}
}
Avant de continuer, laissez-moi vous expliquer les quelques lignes ci-dessus. Il y a plusieurs éléments importants à remarquer dans ce code :
- Quel que soit l’objectif du pipe que vous envisagez de créer, vous devez importer deux éléments : Pipe et PipeTransform, à la ligne 1. Vous importez ces éléments depuis le paquet @angular/core.
- On remarque également qu’un pipe est une simple classe, décoré avec l’annotation @Pipe, à la ligne 3. C’est comme ça qu’Angular peut savoir qu’il s’agit d’une classe de Pipe et non d’un autre élément.
- L’annotation @Pipe vous permet de définir le nom du pipe que vous utiliserez dans les expressions de templates. Le nom de notre pipe est ici « flyingAnimals ».
- Notre classe implémente la méthode transform de l’interface PipeTransform qui accepte une valeur d’entrée, suivie par autant de paramètres que nécessaire. Dans notre cas nous n’avons qu’un seul paramètre, il s’agit de la liste des animaux à trier, à la ligne 6. La méthode transform doit ensuite renvoyer la valeur transformée, c’est-à-dire la liste des animaux volants exclusivement.
- La méthode transform est essentielle pour qu’un pipe puisse fonctionner. Ne l’oubliez pas sous peine de lever une erreur !
Et que se passe-t-il si on ne passe pas de paramètre à un pipe qui en a besoin, notre application va-t-elle crasher ?
Non, pas du tout. Rappelez-vous, tous les paramètres des pipes sont facultatifs. Un pipe doit donc pouvoir fonctionner avec ou sans paramètres.
Pour revenir à notre application, remarquez le comportement suivant : quand vous ajoutez des animaux volants, aucun d’entre eux n’est affiché sous le libellé « Animaux volants » !
Bien que vous n’obteniez pas le comportement souhaité, Angular fonctionne normalement !
Il s’agit simplement d’un algorithme de détection des changements différent, qui ignore les modifications apportées à la liste ou à l’un de ses éléments. Remarquez comment on ajoute un animal dans notre composant :
this
.
animals.push
(
animal);
Vous ajoutez l’animal dans le tableau des animaux, mais la référence au tableau n’a pas changé. C’est le même tableau, avec une nouvelle valeur. C’est tout ce dont Angular se soucie. De son point de vue, c’est le même tableau, et il n’y donc pas de changements, donc pas de mise à jour à afficher ! Angular ne s’intéresse pas aux modifications à l’intérieur du tableau !
Pour corriger cela, il faut créer un nouveau tableau avec le nouvel animal ajouté et l’attribuer au tableau animals. Cette fois, Angular détectera que la référence du tableau a changé. Il exécutera donc le pipe et mettra à jour l'affichage avec le nouveau tableau, qui comprend le nouvel animal volant.
Pour faire simple, si vous modifiez uniquement les valeurs du tableau, sans modifier sa référence, aucun pipe n’est appelé, et l’affichage n’est pas mis à jour. Si vous remplacez le tableau, le pipe s’exécute et l’affichage est mis à jour.
Remplacer le tableau est donc un moyen efficace de signaler à Angular de mettre à jour l’affichage.
Et quand est-ce qu’on doit remplacer le tableau ?
Eh bien, lorsque les données sont modifiées. C'est une règle facile à suivre dans cet exemple, où la seule façon de modifier les données est d'ajouter un nouvel animal :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
// …
// On ajoute un nouvel animal dans le tableau des animaux
addAnimal
(
name
:
string
) {
name =
name.trim
(
);
if
(!
name) {
return
;
}
let
animal =
{
name,
canFly
:
this
.
canFly};
this
.
animals.push
(
animal);
// pas de détection, car la référence au tableau est identique
this
.
animals =
this
.
animals.slice
(
);
// On attribue une nouvelle copie du tableau, la référence est différente
}
// …
Ici, c’est la méthode slice() qui permet d’attribuer une nouvelle copie du tableau à la liste d’animaux. Les valeurs des tableaux sont identiques, mais comme la référence du tableau a changé, cela permet de forcer la détection de changement. Notre exemple étant assez simple, on peut faire cela nous-mêmes.
Mais le plus souvent, vous ne savez pas quand les données ont changé. En particulier dans les applications qui modifient les données de plusieurs façons (interactions des utilisateurs, modifications des données du serveur, etc.). Un composant d'une telle application ne peut habituellement pas connaître tous ces changements. C’est pour cela que Angular nous propose d’utiliser les pipes impurs, que nous allons utiliser pour filtrer plus efficacement nos animaux volants.
Parfois, il peut-être tentant de bricoler dans le composant pour s’accommoder d’un pipe trop contraignant. Cependant, rappelez-vous d’une règle de base : la classe de vos composants doit être indépendante du HMTL ! Le composant doit donc aussi ignorer le fonctionnement de vos pipes !
III-C. Les pipes purs et impurs▲
Angular exécute un pipe pur uniquement lorsqu’il détecte une modification d’une valeur d’entrée primitive (chaîne de caractères, nombre, booléen, etc.) ou une référence d’objet modifiée, comme nous venons de le voir. (une date, un tableau, un objet, une fonction, etc.).
Cela peut sembler restrictif, mais c’est aussi plus rapide. Une vérification de la référence de l’objet est beaucoup plus rapide qu’une vérification approfondie des différences des valeurs à l’intérieur de cet objet. De sorte qu’Angular peut rapidement déterminer s’il peut éviter l’exécution du pipe et la mise à jour de la vue.
Pour cette raison, un pipe pur est préférable lorsque vous pouvez vous adapter avec la stratégie de détection des changements (comme nous l’avons fait avec la méthode slice dans notre exemple précédent). Lorsque vous ne le pouvez pas, vous pouvez utiliser un pipe impur.
Ou vous pouvez ne pas utiliser un pipe du tout. Il est peut-être préférable de poursuivre le but initial du composant via une propriété du composant, un point qui est discuté plus bas sur cette page. => TODO
III-C-1. Pipes impurs▲
Angular exécute un pipe impur pendant chaque cycle de détection de changement du composant. Un pipe impur est donc appelé souvent, à chaque frappe au clavier, ou au moindre mouvement de souris. Il peut être appelé plusieurs fois par seconde !
Ayez donc ce point en tête lorsque vous mettez en œuvre un pipe impur. Vous devez ajouter un nouveau pipe impur dans votre application avec beaucoup de précautions. Un pipe mal conçu et trop exigeant en ressource pourrait détruire l’expérience utilisateur.
III-C-2. Un pipe impur pour le FlyingAnimalsPipe▲
Regardons comment switcher le FlyingHeroesPipe en un FlyingHeroesImpurePipe. La mise en œuvre complète est la suivante :
La version impure, FlyingAnimalsImpurePipe :
2.
3.
4.
5.
@Pipe
({
name
:
'flyingAnimalsImpure'
,
pure
:
false
}
)
export
class
FlyingAnimalsImpurePipe extends
FlyingAnimalsPipe {}
Et la version pure, FlyingAnimalsPipe :
2.
3.
4.
5.
6.
7.
8.
9.
import
{
Pipe,
PipeTransform }
from
'@angular/core'
;
import
{
Flyer }
from
'./animals'
;
@Pipe
({
name
:
'flyingAnimals'
}
)
export
class
FlyingAnimalsPipe implements
PipeTransform {
transform
(
allAnimals
:
Flyer[]
) {
return
allAnimals.filter
(
animal =>
animal.
canFly);
}
}
Dans la version impure, on hérite de FlyingAnimalsPipe, car on ne change pas le fonctionnement interne du pipe. Il fonctionnera de la même manière. La seule différence est l’annotation ‘pure’ dans l’annotation @Pipe, que nous avons définie à false pour indiquer à Angular que ce pipe n’est pas pur.
C’est un bon candidat pour un pipe impur, car sa fonction transform est triviale et rapide :
return
allHeroes.filter
(
hero =>
hero.
canFly);
On peut ensuite appliquer notre nouveau pipe impur dans notre composant :
2.
3.
<
div *
ngFor=
"let animal of (animals | flyingAnimalsImpure)"
>
{{
hero.
name}}
</
div>
Le seul changement que nous avons ajouté est d’adapter notre pipe pour une version impure. Maintenant, si vous réessayez d’ajouter un nouvel animal, même si vous n’appliquez pas la méthode slice, les nouveaux animaux volants s’afficheront bien dans votre interface.
IV. Des pipes utiles▲
IV-A. Le pipe impur AsyncPipe▲
Le pipe natif AsyncPipe est un excellent exemple de pipe impur. Ce pipe accepte une promesse ou un Observable en entrée. Il s’abonne automatiquement à l’élément passé en entrée, et reçoit les nouvelles valeurs émises au fur et à mesure qu’elles arrivent. Il met également l’interface utilisateur à jour à chaque nouvelle valeur. Bref, il fait une bonne partie du boulot pour nous ! 😊
L’exemple suivant lie un Observable avec un template, grâce au pipe async. L’Observable émet le dernier message reçu (message$), et nous souhaitons l’afficher dans le template :
2.
3.
4.
5.
6.
7.
8.
9.
…
@Component
({
selector
:
'async-message'
,
template
:
`
<h2>Dernier message asynchrone reçu : </h2>
<p>Message: {{ message$ | async }}</p>`
,
}
)
export
class
AsyncMessageComponent {
…}
Comme vous pouvez le voir, le pipe Async nous économise beaucoup de travail. Tout ce que nous aurions dû développer dans le composant est fait par le pipe : s’inscrire à la source de données asynchrones, extraire les valeurs résolues, les exposer pour la liaison de données, se désabonner de l’Observable lorsque le composant est détruit, etc. (Nous verrons le cas du désabonnement à un Observable un peu plus loin).
IV-B. JsonPipe▲
Le pipe JsonPipe est un pipe très simple, mais extrêmement utile pour le débogage. Par exemple, pour connaître le format de la réponse JSON du serveur que vous venez de recevoir, il suffit d’appliquer le pipe JsonPipe dessus :
<p>{{someDataFromServer|json}}</p>
Et là, c’est magique ! Vous pouvez visualiser la réponse de votre serveur directement dans votre template, sans passer par la console de débogage de votre navigateur ! Bon, dans la réalité, si la réponse du serveur est trop longue, il faudra mieux passer par la console de débogage pour la visualiser. Cependant, ce pipe fournit un moyen simple de diagnostiquer une liaison de données mystérieusement défaillante, ou d’inspecter un objet pour une future liaison de données. Et rappelez-vous que ce pipe est impur, donc il se met à jour automatiquement avec d’éventuelles données asynchrones ! 😊
IV-C. Les filtres abandonnés : FilterPipe et OrderByPipe▲
Angular ne fournit pas de pipes natifs pour filtrer ou trier des listes. Les développeurs familiers avec AngularJS connaissent bien les filtres filter et orderBy. Sachez qu’il n’y a pas d’équivalent en Angular, et que ce n’est pas un oubli, mais un choix délibéré de la part des équipes chargées du développement d’Angular.
Les filtres AngularJS sont l’équivalent des pipes en Angular. Le nom a changé, mais le principe est identique.
Le filtrage, et surtout le tri, sont des opérations coûteuses. L’expérience utilisateur peut se dégrader sévèrement même pour des listes de taille modérée, lorsque Angular appelle ces pipes plusieurs fois par seconde. Les filtres filter et orderBy ont souvent été utilisés avec abus dans les applications AngularJS, entraînant des plaintes selon lesquelles AngularJS est lent…
Cette accusation est juste dans le sens où AngularJS a préparé le terrain pour cette faille de performance, en mettant en avant les filtres filter et orderBy, sans prévenir que leur mauvaise utilisation pouvait entraîner une dégradation des performances.
Il existe également un risque concernant la minification de votre code. Imaginez un pipe appliqué à une liste d’animaux. La liste peut être triée en fonction du nom et de la date d’ajout des animaux :
<
div *
ngFor=
"let animal of animals | orderBy:'name,date’"
></
div>
Les champs de tris sont identifiés par des chaînes de caractères, en attendant que le pipe fasse référence à une propriété de l’objet (animal[‘name’]). Cependant, lorsque vous minifiez votre code, cela entraîne une modification des noms de propriété de la classe Animal, de telles sortes que Animal.name et Animal.date deviennent quelque chose comme Animal.a et Animal.b. Du coup, animal[‘name’] ne fonctionnera plus !
Bien que certains ne se soucient pas de minifier leur code, Angular ne doit pas empêcher les développeurs de minimiser leur code. L'équipe Angular a donc décidé que tout ce qui est fourni par Angular pourra être minifié en toute sécurité, y compris les pipes. C’est aussi pour cela que les pipes filter et orderBy n’existent plus.
L’équipe Angular, et de nombreux développeurs Angular expérimentés recommandent donc fortement de filtrer et de trier les listes dans la logique du composant lui-même. Le composant peut exposer une propriété filteredAnimals ou sortedAnimals, et prendre le contrôle sur la fréquence d’exécution des mises à jour de la propriété. Toutes les fonctionnalités que vous auriez mises dans un pipe peuvent être écrites dans un service dédié au filtrage et au tri, puis injectées dans le composant.
Si ces considérations de performance et de minification ne s’appliquent pas pour vous, vous pouvez toujours créer vos propres pipes comme vous le souhaitez, ou utiliser ceux développés par la communauté. Mais gardez ces problématiques de performances et de minification en tête, où vous pourriez avoir de mauvaises surprises lors de vos prochains développements ! 😊
V. Créer un pipe timeAgo impur avec des Observables▲
En fonction de la complexité de votre application, les pipes disponibles de base ne sont pas toujours satisfaisants par rapport à vos besoins. Vous devrez parfois développer vos propres pipes pour répondre aux besoins de votre application. Heureusement, Angular nous permet de faire ça simplement.
Je vous propose de développer un nouveau pipe impur personnalisé, qui aura pour rôle d’afficher le temps depuis lequel vous avez ajouté un nouvel animal dans votre application. On retrouve souvent ce système dans les applications de messagerie : « Envoyé il y a 1 minute », « Envoyé il y a 2 heures », « Envoyé hier ». Ici on adaptera simplement ces indications par « Ajouté il y a 1 minute », etc.
Pour implémenter cela, nous utiliserons la programmation réactive et les Observables dans notre pipe. Nous allons nommer notre pipe timeAgoObs.
Créons donc un nouveau pipe avec la commande suivante :
ng generate pipe timeAgoObs
Ensuite, je vous invite à copier-coller le code ci-dessous dans le fichier de votre pipe. Je vous donne quelques explications juste après, mais lisez bien les commentaires dans le code ci-dessous :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
import
{
OnDestroy,
ChangeDetectorRef,
Pipe,
PipeTransform }
from
'@angular/core'
;
import
{
AsyncPipe }
from
'@angular/common'
;
import
{
Observable,
interval,
of
,
timer }
from
'rxjs;
import
{
repeatWhen,
startWith,
takeWhile,
mergeMap,
map }
from
‘rxjs/
operators’;
@Pipe
({
name
:
'timeAgoObs'
,
pure
:
false
}
)
export
class
TimeAgoObsPipe implements
PipeTransform,
OnDestroy {
private
readonly
async
:
AsyncPipe;
private
isDestroyed =
false
;
private
value
:
Date
;
private
timer
:
Observable<
string
>;
// On récupère une instance de ChangeDetectorRef,
// afin de contrôler nous-mêmes la stratégie de détection de changement à appliquer
constructor
(
ref
:
ChangeDetectorRef) {
this
.
async
=
new
AsyncPipe
(
ref);
}
public
transform
(
sinceDate
:
string
):
string
{
// On vérifie la valeur d'entrée du pipe, qui doit être une chaîne de caractères représentant une date
if
(
new
Date
(
sinceDate).toString
(
) ===
"Invalid Date"
||
Date
.parse
(
sinceDate) ===
NaN
) {
throw
new
Error
(
'Ce pipe ne fonctionne qu’avec des chaînes de caractères représentant des dates'
);
}
// On convertit la valeur d’entrée du pipe en date JavaScript
this
.
value =
new
Date
(
sinceDate);
if
(!
this
.
timer) {
this
.
timer =
this
.getObservable
(
);
// On initialise le Timer de notre pipe
}
// On retourne un pipe asynchrone, avec les valeurs émises par notre Timer.
return
this
.
async
.transform
(
this
.
timer);
}
// Fonction outil permettant de renvoyer la date courante
public
now
(
):
Date
{
return
new
Date
(
);
}
// On évite les fuites de mémoire au sein de notre pipe, en se désabonnant à notre Timer
public
ngOnDestroy
(
) {
// On met fin au Timer, au prochain intervalle de temps, le Timer émettra un événement de type ‘complete’
this
.
isDestroyed =
true
;
}
private
getObservable
(
) {
// On crée un flux contenant le nombre 1.
return
Observable.of
(
1
)
.repeatWhen
(
notifications =>
{
// On émet un événement à certains intervalles de temps :
return
notifications.mergeMap
((
x,
i) =>
{
// si le temps écoulé est inférieur à soixante secondes, on met à jour l’interface toutes les secondes,
// sinon on met à jour l’interface toutes les trente secondes
const
sleep =
i <
60
?
1000
:
30000
;
// Après que le temps ci-dessus soit écoulé, on émet un événement
return
Observable.timer
(
sleep);
}
);
}
)
// On émet un événement tant que la méthode ngOnDestroy n’a pas été appelée
.takeWhile
(
_ =>
!
this
.
isDestroyed)
// On renvoie une chaîne de caractères indiquant le temps écoulé, grâce à la méthode elapsed
.map
((
x,
i) =>
this
.elapsed
(
));
};
// On détermine la chaîne de caractères à émettre en fonction du temps écoulé
// depuis la date passée en paramètre du pipe
private
elapsed
(
):
string
{
// On récupère la date actuelle pour calculer le temps écoulé
let
now =
this
.now
(
).getTime
(
);
// On calcule le delta en secondes entre la date actuelle et la date passée en paramètre du pipe
let
delta = (
now -
this
.
value.getTime
(
)) /
1000
;
// On formate la chaîne de caractères à retourner
if
(
delta <
60
) {
return
`
${Math.floor(delta)}
seconde(s)`
;
}
else
if
(
delta <
3600
) {
return
`
${Math.floor(delta / 60)}
minute(s)`
;
}
else
if
(
delta <
86400
) {
return
`
${Math.floor(delta / 3600)}
heure(s)`
;
}
else
{
return
`
${Math.floor(delta / 86400)}
jour(s)`
;
}
}
}
Les explications dans le code ci-dessus sont assez explicites. Retenez le principe de base, on convertit une durée écoulée en une chaîne de caractère lisible par l’utilisateur : « Il y a 2 heure(s) », par exemple. Comme la durée écoulée évolue à chaque seconde, on utilise un Observable pour émettre des événements régulièrement sur l’avancée de cette durée, et un pipe impur pour mettre à jour notre interface utilisateur en conséquence.
Si vous avez compris cela, c’est l’essentiel. Le reste est moins important, comme le désabonnement à l’Observable dans la méthode de cycle de vie ngOnDestroy.
Bon, j’imagine que vous avez hâte de tester notre nouvel Observable ? Pour cela, nous allons ajouter quelques données d’exemple dans notre composant, et modifier notre méthode d’ajout d’animaux :
ngOnInit() {
this.animals = [
{name: 'Lion', canFly: false, date: new Date()},
{name: 'Eagle', canFly: true, date: new Date()},
{name: 'Tortoise', canFly: false, date: new Date()},
{name: 'Dog', canFly: false, date: new Date()},
{name: 'Seagull', canFly: true, date: new Date()},
{name: 'Snake', canFly: false, date: new Date()},
]
}
addAnimal(name: string) {
name = name.trim();
if (!name) { return; }
let animal = {name, canFly: this.canFly, date: new Date()}; // On ajoute une propriété ‘date’ aux animaux
//this.animals.push(animal); // no detection
this.animals.push(animal);
this.animals = this.animals.slice();
}
Ensuite, nous pouvons utiliser notre nouveau pipe personnalisé dans l’application, comme n’importe quel pipe :
Je vous rappelle le danger que représente l’utilisation abusive des pipes impurs, en termes de performance. Ici, j’ai appliqué un pipe impur au sein d’une boucle, car il s’agit d’une petite application de démonstration. Dans le cadre d’une application en production, évitez de reproduire ce code !
Maintenant, la magie va opérer. Voici ce que vous devriez obtenir :
Voilà, le résultat est là ! L’interface est assez simple, mais l’essentiel est que notre pipe fonctionne. Et rien ne vous empêche d’améliorer un peu le visuel par la suite. 😊
En tous cas, félicitations. Vous êtes arrivés au bout de ce tutoriel, d’autres n’en auraient pas eu le courage. J’espère que vous avez une meilleure compréhension des pipes avec Angular, et que cela vous sera utile dans vos futurs développements. Bonne continuation pour vos prochains apprentissages.
VI. Conclusion▲
Nous savons désormais comment fonctionne un pipe de base, et comment effectuer quelques manipulations de base, comme passer un paramètre à un pipe, ou combiner plusieurs pipes. Mais nous avons surtout vu la différence entre les pipes purs et impurs, et comment créer votre propre pipe personnalisé, pour les besoins spécifiques de votre application.
Et pour terminer, rappelez-vous des dangers de l’utilisation d’un pipe impur en termes de performance ! Il vaut parfois mieux passer par un service dédié que par un pipe impur. 😉
VI-A. En résumé▲
- Un pipe est un moyen d’écrire des transformations pour nos données, que l’on peut ensuite appliquer dans nos templates.
- Il existe des pipes natifs fournis par Angular, que l’on peut appliquer dans nos templates sans importations.
- On peut passer des paramètres à nos pipes, mais ils sont tous facultatifs. Un paramètre ne peut pas être obligatoire pour un pipe.
- On peut contrôler dynamiquement les paramètres d’un pipe depuis le composant.
- Il existe deux types de pipes : pur et impur. Les pipes impurs permettent de développer des fonctionnalités plus avancées, mais peuvent entraîner une dégradation des performances de l’application s’ils sont mal implémentés.
- Pour afficher des données asynchrones, on peut utiliser le pipe async.
- Il est possible de créer ses propres pipes personnalisés pour répondre aux besoins spécifiques de son application.
VI-B. Pour aller plus loin▲
Cet article est un extrait de ma formation « Maîtriser Angular 6 : Développer pour l’entreprise». Si vous souhaitez recevoir d’autres guides complets sur Angular 6, directement dans votre boîte email, vous pouvez vous abonner à ma liste email via le lien ci-dessus. À très vite pour les plus courageux ! 😉
VI-C. Remerciements▲
Je tiens à remercier f-leb pour la relecture orthographique.