A pipe transforms a value for display, directly in the template: {{ price | currency }}. It is a reusable pure function that you import like any component. By default pipes are pure: Angular re-runs them only when the input value or the arguments change.
Built-in pipes and custom pipes
<p>{{ order().createdAt | date: 'dd MMM yyyy, HH:mm' }}</p>
<p>{{ order().total | currency: 'RON' : 'symbol' : '1.2-2' }}</p>
<p>{{ ratio() | percent: '1.0-1' }}</p>
<p>{{ user().name | initials }}</p> <!-- custom pipe -->
<p>{{ post().publishedAt | timeAgo }}</p>
@for (entry of config() | keyvalue; track entry.key) {
<dt>{{ entry.key }}</dt><dd>{{ entry.value }}</dd>
}
<!-- async: subscribes and unsubscribes by itself (useful while you still have Observables) -->
@let user = user$ | async;date: 'fullDate' | joi, 22 octombrie 2026 |
date: 'HH:mm' | 09:11 |
currency: 'RON' | 1.234,50 RON |
currency: 'EUR' : 'symbol' : '1.0-0' | 247 EUR |
number: '1.2-2' | 411,50 |
percent | 18,8 % |
initials (custom) | AM |
timeAgo (custom) | 5 h ago |
keyvalue | limba=ronotificari=truetema=dark |
How to write a pipe
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'timeAgo' }) // standalone by default, pure by default
export class TimeAgoPipe implements PipeTransform {
transform(value: Date | string | number, now: number = Date.now()): string {
const seconds = Math.round((now - new Date(value).getTime()) / 1000);
if (seconds < 45) return 'just now';
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes} min ago`;
const hours = Math.round(minutes / 60);
if (hours < 24) return `${hours} h ago`;
const days = Math.round(hours / 24);
return days === 1 ? 'yesterday' : `${days} days ago`;
}
}Locale formatting (ro-RO)
// Formatting in Romanian for date, currency, number
import { registerLocaleData } from '@angular/common';
import localeRo from '@angular/common/locales/ro';
import { LOCALE_ID } from '@angular/core';
registerLocaleData(localeRo);
export const appConfig: ApplicationConfig = {
providers: [
{ provide: LOCALE_ID, useValue: 'ro' },
{ provide: DEFAULT_CURRENCY_CODE, useValue: 'RON' },
],
};Pipe vs method vs computed()
A method called from the template runs on every change detection of the component. A pure pipe and a computed() are memoised: they don’t run again if their inputs haven’t changed.
// How to choose:
//
// DISPLAY transformation, reused in many components → pipe (date, currency, initials, timeAgo)
// Calculation specific to one component, on its signals → computed()
// Business logic / state derivation → computed() / store
// Method called from the template → only for trivial things
//
// Why not a method? It is called on every change detection of the component.
// The pure pipe and the computed() are recalculated only when their inputs change.Impure pipes
// Impure pipe: runs on EVERY change detection, whatever the input
@Pipe({ name: 'filterBy', pure: false })
export class FilterByPipe implements PipeTransform {
transform<T>(items: T[], predicate: (item: T) => boolean): T[] {
return items.filter(predicate); // runs often → avoid it on large lists
}
}
// Almost always the right choice is a computed() in the component:
readonly activeUsers = computed(() => this.users().filter((u) => u.active));When the value depends on signals, a computed() is usually simpler than a pipe: see signal() and computed(). Formatting dates and numbers per locale is covered in {i18n}.
When YES, when NO
- Display formatting used in several places (relative date, file size, initials, IBAN masking).
- A pure transformation: the same input → the same output.
- You want to use it in templates that have no logic class of their own (e.g. dumb components).
- A calculation specific to one component →
computed(). - Filtering/sorting business data →
computed()or a store. - You need HTTP or state → a pipe is not the place (use resource / a service).
- An impure pipe “to make it work” → you almost certainly need a signal.
Common pitfalls
- You mutate an array and wonder why the pure pipe doesn’t re-run: the reference is the same. Create a new array.
- You forget the import: “The pipe 'date' could not be found” → add
DatePipetoimports. - Unregistered locale:
LOCALE_ID = 'ro'withoutregisterLocaleData(localeRo)throws an error. - New arguments on every render (
| pipe: { a: 1 }) cancel the memoisation in some cases. Keep the arguments stable.