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
:
Name | Description |
---|---|
AsyncPipe | Handles Promises and RxJS Observables. |
CurrencyPipe | Handles the conversion from a number to a locale'd currency string. |
DatePipe | Handles the conversion from a Date to a locale'd date string. |
DecimalPipe | Handles the conversion from a number to a locale'd decimal string. |
I18nPluralPipe | Handles pluralizing a string in a locale'd manner. |
I18nSelectPipe | Handle the mapping from a value to a locale |
JsonPipe | Handles data via JSON.stringify . |
KeyValuePipe | Handles key value pair transform of objects. |
LowerCasePipe | Handles lowercasing of a string. |
PercentPipe | Handles the conversion from a number to a locale'd percent string |
SlicePipe | Handles subsets of an array using slice |
TitleCasePipe | Handles title-casing of a string. |
UpperCasePipe | Handles 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 {}