Pour afficher une donnée sous un format spécifique, Angular vous propose les Pipes. Le framework en fournit plusieurs, comme :
DatePipe, pour les dates ;
UpperCasePipe, LowerCasePipe et TitleCasePipe pour les chaînes de caractères ;
DecimalPipe, PercentPipe et CurrencyPipe pour les nombres.
Mais saviez-vous qu'Angular vous permet d'aller plus loin, en créant vos propres Pipes ? Imaginons quelques cas de figure :
vous voulez afficher un aperçu d'un texte, avec au plus 50 caractères, suivis de
…
pour montrer qu'il y a une suite ;vous recevez des objets User de votre backend qui contiennent séparément les noms et prénoms des utilisateurs, et vous avez souvent besoin d'afficher le nom complet sous la forme "NOM Prénom" ;
au lieu d'afficher une date exacte, vous voulez afficher un texte descriptif de type "il y a quelques minutes".
Dans ces trois situations, la solution du Pipe est pertinente : vous voulez afficher une donnée sous un format spécifique sans modifier la donnée sous-jacente.
Formatez vos données
Puisque les Pipes que vous créez ici pourront être utiles dans toute l'application, vous allez les déclarer dans SharedModule. Vous allez commencer par le Pipe qui affichera un aperçu d'un texte, suivi des …
.
Dans le dossier shared
, créez un dossier pipes
, et créez-y un fichier shorten.pipe.ts
:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'shorten'
})
export class ShortenPipe implements PipeTransform {
transform(value: string): string {
if (value.length <= 50) {
return value;
}
return value.substring(0, 50) + '…';
}
}
Pour créer un Pipe personnalisé :
vous utilisez le décorateur
@Pipe
, auquel vous passez unname
qui sera le nom utilisé dans les templates pour appliquer ce Pipe ;vous créez une classe qui implémente l'interface
PipeTransform
;pour implémenter
PipeTransform
, votre classe doit contenir une méthodetransform()
– c'est cette méthode qui prendra la donnée comme argument et la retournera transformée.
Ici, si le texte reçu fait moins de 50 caractères, vous le retournez non transformé. S'il en fait plus, vous le retournez tronqué, suivi du caractère …
.
Pour utiliser un Pipe dans l'application, il faut le déclarer. Ajoutez-le aux déclarations de SharedModule, ainsi qu'aux exports pour le rendre disponible à toute l'application :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CommentsComponent } from './components/comments/comments.component';
import { MaterialModule } from './material.module';
import { ReactiveFormsModule } from '@angular/forms';
import { ShortenPipe } from './pipes/shorten.pipe';
@NgModule({
declarations: [
CommentsComponent,
ShortenPipe
],
imports: [
CommonModule,
MaterialModule,
ReactiveFormsModule
],
exports: [
CommentsComponent,
MaterialModule,
ReactiveFormsModule,
ShortenPipe
]
})
export class SharedModule { }
Vous pouvez maintenant appliquer ce Pipe sur le content des Posts. Dans post-list-item.component.html
:
<span class="post-content">{{ post.content | shorten }}</span>
Dans le DOM, ça donne :
Aussi simple que ça ! Mais on peut l'améliorer !
Acceptez un argument
Pour rendre ShortenPipe
encore plus utile, vous allez lui faire accepter un argument optionnel pour fixer le nombre maximal de caractères (plutôt que garder les 50 caractères codés en dur). Ça se fait en ajoutant un deuxième paramètre à transform
:
transform(value: string, maxLength = 50): string {
if (value.length <= maxLength) {
return value;
}
return value.substring(0, maxLength) + '…';
}
Cela vous permet, dans vos templates, de choisir le nombre de caractères auquel vous limitez les textes :
<span class="post-content">{{ post.content | shorten: 100 }}</span>
Allez, passons à un Pipe qui accepte autre chose qu'une chaîne de caractères comme paramètre.
Nommez vos utilisateurs
À vous de jouer !
Je vous propose d'essayer d'implémenter vous-même ce prochain Pipe qui :
acceptera un objet de type
{ firstName: string, lastName: string }
;retournera
"LASTNAME Firstname"
.
Par exemple, l'objet { lastName: 'Alexander', firstName: 'Will' }
doit retourner 'ALEXANDER Will'
. Vous pourrez essayer votre Pipe en créant un objet temporaire dans l'un de vos components actuels.
Faites l'essai, et je vous propose ma solution ci-dessous.
Suivez ma solution
Voici ma proposition :
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'username'
})
export class UsernamePipe implements PipeTransform {
transform(value: { firstName: string, lastName: string }): string {
return `${value.lastName.toUpperCase()} ${value.firstName}`;
}
}
Besoin de rien de plus ! Si vous déclarez ce Pipe et que vous l'utilisez dans l'un de vos templates avec un objet User temporaire, ça fonctionne tout seul !
Voici une idée pour une amélioration de UsernamePipe
:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'username'
})
export class UsernamePipe implements PipeTransform {
transform(value: { firstName: string, lastName: string }, locale: 'en' | 'fr' = 'fr'): string {
return locale === 'fr' ?
`${value.lastName.toUpperCase()} ${value.firstName}` :
`${value.firstName} ${value.lastName}`;
}
}
Ici, je permets au développeur de choisir de localiser l'affichage du nom :
soit en
'fr'
(qui est également la valeur par défaut), en gardant le nom en majuscules suivi du prénom ;soit en
'en'
, avec le prénom suivi du nom.
Allez, un dernier Pipe qui demande un peu plus d'une ligne de code, et qui crée un comportement que plusieurs de mes clients m'ont demandé !
Comptez les secondes, les heures, les jours
L'idée ici est d'afficher un texte descriptif de type "il y a quelques minutes" à la place des dates exactes.
Dans votre application, les dates createdDate
sont sous forme de string, mais pour que ce Pipe soit le plus réutilisable possible, vous allez prendre en compte les deux possibilités : string
et Date
.
Voici l'implémentation que je vous propose :
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'timeAgo'
})
export class TimeAgoPipe implements PipeTransform {
timeDiffs = {
minute: 60 * 1000,
hour: 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000,
week: 7 * 24 * 60 * 60 * 1000,
month: 30 * 24 * 60 * 60 * 1000,
year: 365 * 24 * 60 * 60 * 1000
};
transform(value: string | Date): any {
const now = Date.now();
const then = new Date(value).getTime();
const diff = now - then;
if (diff < this.timeDiffs.minute) {
return 'Il y a quelques secondes';
} else if (diff < this.timeDiffs.hour) {
return 'Il y a quelques minutes';
} else if (diff < this.timeDiffs.day) {
return 'Il y a quelques heures';
} else if (diff < this.timeDiffs.week) {
return 'Il y a quelques jours';
} else if (diff < this.timeDiffs.month) {
return 'Il y a quelques semaines';
} else if (diff < this.timeDiffs.year) {
return 'Il y a quelques mois';
} else {
return 'Il y a plus d\'un an';
}
}
}
Dans ce Pipe :
l'objet
timeDiffs
permet de regrouper (et de nommer) des durées en millisecondes pour la comparaison de deux dates ;la méthode
transform
accepte desstring
ou desDate
;une première constante
now
contient le moment exact du runtime sous forme du nombre de millisecondes depuis le 1er janvier 1970 (l'époque UNIX) ;une deuxième constante
then
crée un nouvel objetDate
à partir de value – ça marche pour les typesstring
etDate
– et utilisegetTime()
pour récupérer le nombre de millisecondes correspondant ;une dernière constante
diff
correspond à la différence entrenow
etthen
.
On compare la valeur de diff
aux différentes durées dans timeDiffs
pour retourner un texte descriptif approprié.
Déclarez et exportez TimeAgoPipe
dans SharedModule, et remplacez DatePipe
dans PostListItemComponent ainsi que dans CommentsComponent, et vous aurez :
Implémenter ce genre de comportement serait possible sans Pipe, mais j'espère vous avoir convaincu que lorsque vous devez transformer une valeur pour l'afficher dans un template, l'utilisation d'un Pipe est généralement le meilleur choix architectural !
En résumé
On déclare un Pipe avec le décorateur
@Pipe
en y passant le nom que l'on souhaite utiliser.La classe du Pipe implémente l'interface PipeTransform avec une méthode
transform
qui accepte au moins un paramètre, et le retourne transformé.Passer un deuxième argument à
transform
permet d'implémenter une configuration depuis le template ; ex. :{{ myText | shorten: 100 }}
;Les Pipes peuvent accepter plusieurs types en paramètre, y compris des objets complexes.
Maintenant que vous savez implémenter vos propres Pipes, suivez-moi au prochain chapitre pour apprendre comment créer des comportements dynamiques avec vos propres Directives !