Front-end angularforall.com

- Angular pipes : built-in et pipes personnalisés

Angular Pipes Angular 17 Angular 18
Angular pipes : built-in et pipes personnalisés

Maîtrisez les pipes Angular : découvrez les pipes intégrés (date, currency, async, number) et créez vos propres pipes personnalisés pour transformer vos.

Concept et fonctionnement des pipes

Un pipe Angular est une fonction pure de transformation qui s'utilise dans les templates avec l'opérateur |. Contrairement aux méthodes de composant, les pipes purs sont mémoïsés — Angular ne les réévalue que si la valeur d'entrée change.

<!-- Sans pipe : Angular appelle getPrix() à CHAQUE détection de changement -->
<p>{{ getPrix(product.price) }}</p>

<!-- Avec pipe pur : réévalué SEULEMENT si product.price change -->
<p>{{ product.price | currency:'EUR':'symbol':'1.2-2':'fr' }}</p>

<!-- Pipes chaînés -->
<p>{{ article.title | lowercase | titlecase | truncate:60 }}</p>

<!-- Pipe avec paramètres multiples (séparés par :) -->
{{ today | date:'EEEE dd MMMM yyyy':'':'fr-FR' }}
<!-- Résultat : mercredi 30 avril 2026 -->
Performance : Un pipe pur est une optimisation automatique. {{ computeFullName(user) }} s'exécute à chaque cycle de détection. {{ user | fullName }} ne s'exécute que si user change de référence. Sur une liste de 500 éléments, la différence est significative.

Pipes intégrés essentiels avec paramètres

Pipe Import Exemple Résultat
dateDatePipetoday | date:'dd/MM/yyyy'30/04/2026
currencyCurrencyPipe1234.5 | currency:'EUR':'symbol':'1.2-2':'fr'1 234,50 €
numberDecimalPipe3.14159 | number:'1.2-3'3,142
percentPercentPipe0.756 | percent:'1.0-1'75.6%
sliceSlicePipe[1,2,3,4,5] | slice:1:3[2, 3]
keyvalueKeyValuePipeobj | keyvaluePaires {key, value}
jsonJsonPipedata | jsonJSON formaté
titlecaseTitleCasePipe'hello world' | titlecaseHello World
// Tous les pipes Angular importés individuellement (standalone)
import { DatePipe, CurrencyPipe, DecimalPipe, PercentPipe,
         UpperCasePipe, LowerCasePipe, TitleCasePipe,
         SlicePipe, KeyValuePipe, JsonPipe } from '@angular/common';

@Component({
    standalone: true,
    imports: [DatePipe, CurrencyPipe, DecimalPipe, PercentPipe,
              SlicePipe, KeyValuePipe, JsonPipe, TitleCasePipe],
    template: `
        <!-- DatePipe avec locale française -->
        <p>{{ event.startDate | date:'EEEE dd MMMM yyyy à HH:mm':'':'fr-FR' }}</p>
        <!-- Résultat : mercredi 30 avril 2026 à 14:30 -->

        <!-- CurrencyPipe — 3 styles possibles -->
        <p>{{ price | currency:'EUR' }}</p>               <!-- EUR 1,234.50 -->
        <p>{{ price | currency:'EUR':'code' }}</p>        <!-- EUR 1,234.50 -->
        <p>{{ price | currency:'EUR':'symbol':'1.0-0':'fr' }}</p>  <!-- 1 235 € -->

        <!-- KeyValuePipe — itérer un objet -->
        @for (entry of userProfile | keyvalue; track entry.key) {
            <dt>{{ entry.key | titlecase }}</dt>
            <dd>{{ entry.value }}</dd>
        }

        <!-- SlicePipe — afficher les 5 premiers items -->
        @for (item of items | slice:0:5; track item.id) {
            <app-item [data]="item" />
        }
    `
})

Configurer la locale par défaut

// app.config.ts — Activer la locale française pour tous les pipes
import { ApplicationConfig, LOCALE_ID } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';

// Enregistrer la locale française
registerLocaleData(localeFr);

export const appConfig: ApplicationConfig = {
    providers: [
        { provide: LOCALE_ID, useValue: 'fr-FR' }, // Locale globale
    ]
};

// Avec LOCALE_ID = 'fr-FR', les pipes formatent automatiquement en français
// {{ 1234.56 | currency:'EUR' }} → 1 234,56 €
// {{ today | date:'shortDate' }} → 30/04/2026

Le pipe async — gestion des Observables

Le pipe async s'abonne automatiquement à un Observable ou Promise et se désabonne à la destruction du composant — zéro memory leak.

import { AsyncPipe } from '@angular/common';
import { Observable, combineLatest, of } from 'rxjs';
import { map, catchError, startWith } from 'rxjs/operators';

@Component({
    standalone: true,
    imports: [AsyncPipe],
    template: `
        <!-- Pattern 1 : Simple Observable -->
        @if (users$ | async; as users) {
            @for (user of users; track user.id) {
                <app-user-card [user]="user" />
            }
        } @else {
            <app-skeleton />
        }

        <!-- Pattern 2 : Multiple Observables avec combineLatest -->
        @if (vm$ | async; as vm) {
            <div [class.loading]="vm.loading">
                <app-error @if="vm.error" [message]="vm.error" />
                @for (item of vm.items; track item.id) {
                    <app-item [data]="item" />
                }
            </div>
        }
    `
})
export class UserListComponent {
    users$ = this.userService.getAll();

    // ViewModel combiné — une seule souscription async
    vm$ = combineLatest({
        items: this.userService.getAll(),
        loading: this.userService.isLoading$,
        error: this.userService.error$,
    });

    constructor(private userService: UserService) {}
}
Pourquoi le pipe async ? subscribe() dans le composant nécessite un ngOnDestroy() manuel. Le pipe async le fait automatiquement et reste dans le template — le composant reste léger et sans état de subscription.

Créer des pipes personnalisés avancés

ng generate pipe shared/pipes/truncate
ng generate pipe shared/pipes/file-size
ng generate pipe shared/pipes/time-ago
// truncate.pipe.ts — Tronquer avec options
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
    transform(value: string | null, maxLength = 100, suffix = '...', wordBoundary = true): string {
        if (!value || value.length <= maxLength) return value ?? '';

        let truncated = value.substring(0, maxLength);

        if (wordBoundary) {
            // Tronquer au dernier espace pour éviter les mots coupés
            truncated = truncated.substring(0, truncated.lastIndexOf(' '));
        }

        return truncated.trim() + suffix;
    }
}
// file-size.pipe.ts — Formater les tailles de fichiers
@Pipe({ name: 'fileSize', standalone: true })
export class FileSizePipe implements PipeTransform {
    private readonly units = ['o', 'Ko', 'Mo', 'Go', 'To'];

    transform(bytes: number | null, decimals = 2): string {
        if (!bytes || bytes === 0) return '0 o';

        const k = 1024;
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        const value = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals));

        return `${value} ${this.units[i]}`;
    }
}
// Usage : {{ file.size | fileSize }} → "2.45 Mo"
// Usage : {{ file.size | fileSize:0 }} → "2 Mo"
// time-ago.pipe.ts — Afficher un temps relatif ("il y a 3 heures")
import { Pipe, PipeTransform, OnDestroy } from '@angular/core';

@Pipe({ name: 'timeAgo', standalone: true, pure: false }) // impur car dépend du temps
export class TimeAgoPipe implements PipeTransform, OnDestroy {
    private timer: ReturnType<typeof setInterval> | null = null;
    private latestValue: Date | null = null;

    transform(value: Date | string | null): string {
        if (!value) return '';

        const date = value instanceof Date ? value : new Date(value);
        const now = new Date();
        const secondsAgo = Math.floor((now.getTime() - date.getTime()) / 1000);

        if (secondsAgo < 60) return 'À l\'instant';
        if (secondsAgo < 3600) return `Il y a ${Math.floor(secondsAgo / 60)} min`;
        if (secondsAgo < 86400) return `Il y a ${Math.floor(secondsAgo / 3600)} h`;
        if (secondsAgo < 2592000) return `Il y a ${Math.floor(secondsAgo / 86400)} j`;

        return date.toLocaleDateString('fr-FR');
    }

    ngOnDestroy() {
        if (this.timer) clearInterval(this.timer);
    }
}
// Usage : {{ post.createdAt | timeAgo }} → "Il y a 3 h"

Pipes purs vs impurs — impact performance

Caractéristique Pipe pur (pure: true) Pipe impur (pure: false)
Fréquence d'exécutionSeulement si la valeur d'entrée change (référence)À chaque cycle de détection de changements
PerformanceExcellente — mémoïséPeut ralentir si logique lourde
Cas d'usageTransformation de données statiquesFiltrage dynamique, temps relatif
Déclarationpure: true (défaut)pure: false
// Pipe impur nécessaire : le résultat change sans que l'entrée change
@Pipe({ name: 'filterByStatus', standalone: true, pure: false })
export class FilterByStatusPipe implements PipeTransform {
    transform(items: Item[], status: string): Item[] {
        return items.filter(item => item.status === status);
        // ⚠️ Si on mute items[], Angular ne détecte pas le changement avec pure:true
        // Solution : toujours créer un nouveau tableau dans le composant
    }
}

// ✅ MEILLEURE APPROCHE : rendre les données immuables
// Composant :
addItem(item: Item) {
    this.items = [...this.items, item]; // Nouveau tableau = nouveau référence
    // Le pipe pur se réévalue car this.items a changé de référence
}

Chaînage et composition de pipes

<!-- Chaînage de pipes : appliqués de gauche à droite -->
{{ article.title | lowercase | titlecase }}
<!-- "HELLO WORLD" → "hello world" → "Hello World" -->

{{ user.bio | truncate:120:'...' | safeHtml }}
<!-- Tronquer puis sécuriser le HTML -->

{{ order.total | currency:'EUR':'symbol':'1.2-2':'fr' }}
<!-- 1 234,56 € -->

{{ fileSize | fileSize:1 | lowercase }}
<!-- "2.5 mo" -->

<!-- Pipe dans ngClass -->
<div [class]="'status-' + (order.status | lowercase)">
    {{ order.status | titlecase }}
</div>

<!-- Pipe dans une expression ternaire -->
{{ (isLoggedIn ? user.name : 'Invité') | titlecase }}

Tester les pipes

// truncate.pipe.spec.ts
import { TruncatePipe } from './truncate.pipe';

describe('TruncatePipe', () => {
    let pipe: TruncatePipe;

    beforeEach(() => {
        pipe = new TruncatePipe(); // Pas besoin de TestBed pour un pipe pur
    });

    it('retourne la valeur intacte si sous la limite', () => {
        expect(pipe.transform('Hello', 100)).toBe('Hello');
    });

    it('tronque au bon endroit', () => {
        const long = 'Un texte assez long pour être tronqué';
        const result = pipe.transform(long, 15, '...');
        expect(result).toContain('...');
        expect(result.length).toBeLessThanOrEqual(18); // 15 + '...'
    });

    it('gère null et undefined', () => {
        expect(pipe.transform(null)).toBe('');
        expect(pipe.transform(undefined as any)).toBe('');
    });

    it('respecte les limites de mots avec wordBoundary', () => {
        const text = 'Angular est un framework TypeScript';
        const result = pipe.transform(text, 20, '...', true);
        // Ne doit pas couper un mot au milieu
        expect(result).not.toMatch(/\w\.\.\.$/)
    });
});

Pipes standalone et bibliothèque partagée

// shared/pipes/index.ts — Barrel d'exports
export { TruncatePipe } from './truncate.pipe';
export { FileSizePipe } from './file-size.pipe';
export { TimeAgoPipe } from './time-ago.pipe';
export { SafeHtmlPipe } from './safe-html.pipe';

// Constante pratique pour importer tous les pipes d'un coup
export const SHARED_PIPES = [TruncatePipe, FileSizePipe, TimeAgoPipe, SafeHtmlPipe] as const;

// Utilisation dans un composant
import { SHARED_PIPES } from '@shared/pipes';

@Component({
    standalone: true,
    imports: [...SHARED_PIPES],
    template: `
        <p>{{ article.content | truncate:200 }}</p>
        <span>{{ file.size | fileSize }}</span>
        <time>{{ post.date | timeAgo }}</time>
    `
})

Conclusion

Les pipes Angular sont un outil de performance sous-estimé. Bien utilisés, ils remplacent avantageusement les méthodes calculées dans les templates.

  • Préférer les pipes aux méthodes dans les templates — exécution mémoïsée
  • Configurer LOCALE_ID: 'fr-FR' pour les pipes date/currency/number en français
  • Pipe async pour tout Observable — zéro memory leak
  • Pipes purs (défaut) pour les transformations, impurs seulement si le résultat dépend du temps/état externe
  • Créer une bibliothèque de pipes réutilisables avec barrel d'exports
  • Tester les pipes sans TestBed — simples à tester unitairement

Partager