Angular Pipes: A Complete Guide

January 6, 2025

1,424 words

Post contents

As I explain in my book, The Framework Field Guide, that teaches React, Angular, and Vue all at once, having derived data is mission critical for any sufficiently mature and production-ready framework.

The general idea is as such:

const count = 1;// How to get this to regenerate when the variable above changesconst doubleCount = count * 2;// Such that this bit of UI is always up-to-dateel.innerText = doubleCount;

While Angular has a great way of handling this inside of class logic via computed:

@Component({    selector: "app-root",	template: `		<p>{{doubleCount}}</p>	`})class AppComponent {	count = signal(1);	doubleCount = computed(() => this.count() * 2);}

It's often not a perfect solution when you need an in-template variable.

For example, let's say that you had a list of dates you wanted to display to your user:

@Component({    selector: "app-root",	template: `		@for (dateObj of dates(); track dateObj) {			<p>{{dateObj}}</p>		}	`})class AppComponent {	dates = signal([        new Date("03-15-2005"),        new Date("07-21-2010"),        new Date("11-02-2017"),        new Date("06-08-2003"),        new Date("09-27-2014")    ]);}

Now, to visually display the date in a form akin to "March 15, 2005", we'd need to pass each Date object through Intl.DateTimeFormat like so:

new Intl.DateTimeFormat("en-US", {    year: "numeric",    month: "long",    day: "numeric",}).format(dateGoesHere);

We can do this using a computed field:

@Component({    selector: "app-root",	template: `		@for (dateObj of dates(); track dateObj) {			<p>{{dateObj}}</p>		}	`})class AppComponent {	dates = signal([        new Date("03-15-2005"),        new Date("07-21-2010"),        new Date("11-02-2017"),        new Date("06-08-2003"),        new Date("09-27-2014")    ]);        displayableDates = computed(() => this.dates.map(date =>    	new Intl.DateTimeFormat("en-US", {            year: "numeric",            month: "long",            day: "numeric",   	 	}).format(date)    );}

But this can be a bit verbose and tricky to keep track of in larger codebases.

Instead, Angular provides us a different API: Pipes.

Introducing pipes

To solve this problem, Angular introduced a nice way to call functions right from the template itself.

We start with the @Pipe definition:

import { Pipe, PipeTransform } from "@angular/core";@Pipe({ name: "formatDate" })class FormatDatePipe implements PipeTransform {	transform(value: Date): string {		return new Intl.DateTimeFormat("en-US", {            year: "numeric",            month: "long",            day: "numeric",   	 	}).format(value);	}}

You may then use these pipes in your components directly inside the template.

@Component({    selector: "app-root",	imports: [FormatDatePipe],	template: `		@for (dateObj of dates(); track dateObj) {			<p>{{dateObj | formatDate}}</p>		}	`})class AppComponent {	dates = signal([        new Date("03-15-2005"),        new Date("07-21-2010"),        new Date("11-02-2017"),        new Date("06-08-2003"),        new Date("09-27-2014")    ]);}

Multiple Input Pipes

You may notice the similarities between pipes and functions. After all, pipes are effectively functions you're able to call in your template. Much like functions, they're not limited to a single input property, either.

Let's add a second input to have formatDate return a specific date format.

@Pipe({ name: "formatDate" })class FormatDatePipe implements PipeTransform {    // `dateFormat` is an optional argument. If left empty, will simply `DateTimeFormat`    transform(value: Date, dateFormat?: string): string {        // Stands for "Long format month, day of month, year"        if (dateFormat === "MMMM d, Y") {            return new Intl.DateTimeFormat("en-US", {                year: "numeric",                month: "long",                day: "numeric",            }).format(value);        }        return new Intl.DateTimeFormat("en-US").format(value);    }}

Then, we can use it in our template while passing a second argument:

@Component({    selector: "app-root",	imports: [FormatDatePipe],	template: `		@for (dateObj of dates(); track dateObj) {			<p>{{dateObj | formatDate: 'MMMM d, Y'}}</p>		}	`})class AppComponent {	dates = signal([        new Date("03-15-2005"),        new Date("07-21-2010"),        new Date("11-02-2017"),        new Date("06-08-2003"),        new Date("09-27-2014")    ]);}

Built-In Pipes

Luckily, Angular's all-in-one methodology means that there's a slew of pipes that the Angular team has written for us. One such pipe is actually a date formatting pipe. We can remove our own implementation in favor of one built right into Angular!

To use the built-in pipes, we need to import them from CommonModule into the component. In this case, the pipe we're looking to use is called DatePipe. This provided date pipe is, expectedly, called date when used in the template and can be used like so:

import { DatePipe } from "@angular/common";@Component({    selector: "app-root",	imports: [DatePipe],	template: `		@for (dateObj of dates(); track dateObj) {			<p>{{dateObj | date: 'MMMM d, Y'}}</p>		}	`})class AppComponent {	dates = signal([        new Date("03-15-2005"),        new Date("07-21-2010"),        new Date("11-02-2017"),        new Date("06-08-2003"),        new Date("09-27-2014")    ]);}

List of Built-in Pipes

Since Angular 19, the following pipes are available out of the box in @angular/common:

NameDescription
AsyncPipeHandles Promises and RxJS Observables.
CurrencyPipeHandles the conversion from a number to a locale'd currency string.
DatePipeHandles the conversion from a Date to a locale'd date string.
DecimalPipeHandles the conversion from a number to a locale'd decimal string.
I18nPluralPipeHandles pluralizing a string in a locale'd manner.
I18nSelectPipeHandle the mapping from a value to a locale
JsonPipeHandles data via JSON.stringify.
KeyValuePipeHandles key value pair transform of objects.
LowerCasePipeHandles lowercasing of a string.
PercentPipeHandles the conversion from a number to a locale'd percent string
SlicePipeHandles subsets of an array using slice
TitleCasePipeHandles title-casing of a string.
UpperCasePipeHandles uppercasing of a string.

Non-Prop Derived Values

While we've primarily used component inputs to demonstrate derived values today, both of the methods we've used thus far work for the internal component state and inputs.

Let's say that we have a piece of state called number in our component and want to display the doubled value of this property without passing this state to a new component:

@Pipe({ name: "doubleNum" })class DoubleNumPipe implements PipeTransform {  transform(value: number): number {    return value * 2;  }}@Component({  selector: "app-root",  imports: [DoubleNumPipe],  template: `    <div>      <p>{{ number() }}</p>      <p>{{ number() | doubleNum }}</p>      <button (click)="addOne()">Add one</button>    </div>  `,})class AppComponent {  number = signal(0);  addOne() {    this.number.set(this.number() + 1);  }}

Performance Concerns

Let's talk about performance for a moment. While pipes default to their most performant capabilities out-of-the-box, let's investigate a performance de-opt that you can choose to enable for fringe usecases.

See, by default, a pipe will not run multiple times given the same input:

<!-- Imagine each of these is a re-render of the component --><p>{{ 1 | doubleNum }}</p><p>{{ 1 | doubleNum }}</p><p>{{ 1 | doubleNum }}</p>

Will all return 2 without doing the math to recalulate this result.

However, the way it does this comparison is by strict equality: ===. This means that if you pass an object and then mutate the reference, it won't track the changes to an object.

So, while this code won't work by default:

@Pipe({ name: "getListProps" })class GetListPropsPipe implements PipeTransform {    transform<T extends object, K extends keyof T>(value: T[], key: K): T[K][] {        return value.map((item) => item[key]);    }}@Component({    selector: "app-root",    imports: [GetListPropsPipe, JsonPipe],    template: `    <div>      <p>{{ list | getListProps: "age" | json }}</p>      <button (click)="addTenToAges()">Change ages</button>    </div>  `,})class AppComponent {    list = [        {            name: "John",            age: 30,        },        {            name: "Jane",            age: 25,        },    ];    addTenToAges() {        this.list.forEach((item) => {            item.age = item.age + 10;        });    }}

We can add pure: false to the pipe and, voila, it works!

To learn more about object reference and comparison in JavaScript, check out our article that explains how memory addresses work in JS.

Using Services in Pipes

Some folks have asked me how they can use Angular's dependency injection in pipes.

Well, luckily for us it's no different than using the inject function in a @Component or @Directive:

@Injectable({ providedIn: "root" })class UserService {  name = "Corbin Crutchley";}@Pipe({ name: "greeting" })class GreetingPipe implements PipeTransform {  userService = inject(UserService);  transform(greeting: string) {    return `${greeting}, ${this.userService.name}`;  }}@Component({  selector: "app-root",  imports: [GreetingPipe],  template: `    <div>      <p>{{ "Hello" | greeting }}</p>    </div>  `,})class AppComponent {}

Subscribe to our newsletter!

Subscribe to our newsletter to get updates on new content we create, events we have coming up, and more! We'll make sure not to spam you and provide good insights to the content we have.