Angular Pipes: Dates, Currency, Turkish Locale and Writing a Custom Pipe
Built-in pipes like date, currency, number and titlecase, parameters and chaining, Turkish formatting with registerLocaleData, writing a custom pipe, and how pure vs impure affects performance.
Version note: The code and commands in this post are for Angular 12.
Same Data, Different Look
You have a Date object in your class and you want it on screen as "12 Mayıs 2021". You also have the number 1450, which should show up as "₺1.450,00". The first idea is to write helper methods like formatDate() and formatPrice() in the class. Three components later, you notice you are copy-pasting the same methods around.
Angular has a tool for this: the pipe. A pipe is a small function in the template that takes a value and prints a transformed version of it. The original data does not change; only how it is displayed does.
Think of a filter on your kitchen tap. The water in the mains stays the same, but what reaches your glass has gone through the filter. Swap the filter and the water in your glass changes, while the mains stay untouched.
First Pipes: Playing With Letters
Say a component has these two properties:
export class CustomerCardComponent {
customer = 'ayşe yılmaz';
city = 'istanbul';
}
In the template, you attach a pipe to the right of a value with a vertical bar:
<p>{{ customer | titlecase }}</p> <!-- Ayşe Yılmaz -->
<p>{{ customer | uppercase }}</p> <!-- AYŞE YILMAZ -->
<p>{{ city | uppercase }}</p> <!-- ISTANBUL -->
<p>{{ city | titlecase }}</p> <!-- Istanbul -->
The | character tells Angular "pass the value on the left through the pipe on the right." titlecase capitalizes the first letter of each word, uppercase capitalizes everything, and lowercase lowercases everything.
Look at the last two lines. They say "ISTANBUL" and "Istanbul", without the dotted capital İ that Turkish uses. Under the hood these pipes call JavaScript's toUpperCase(), which knows nothing about language; it always turns a lowercase i into a dotless I. If you work with Turkish text, keep this detail in mind. We will fix it with our own pipe at the end of the post.
Pipes With Parameters: Dates, Numbers, Money
Some pipes take settings. You pass a parameter after the pipe name with a colon. Let's take an order summary:
export class OrderSummaryComponent {
orderDate = new Date(2021, 4, 12, 14, 30);
total = 1450;
discount = 0.15;
rating = 4.6666;
}
<p>{{ orderDate | date }}</p> <!-- May 12, 2021 -->
<p>{{ orderDate | date:'dd.MM.yyyy HH:mm' }}</p> <!-- 12.05.2021 14:30 -->
<p>{{ total | number }}</p> <!-- 1,450 -->
<p>{{ rating | number:'1.1-2' }}</p> <!-- 4.67 -->
<p>{{ discount | percent }}</p> <!-- 15% -->
<p>{{ total | currency:'TRY' }}</p> <!-- TRY1,450.00 -->
<p>{{ total | currency:'TRY':'symbol-narrow' }}</p> <!-- ₺1,450.00 -->
Without a parameter, date picks a default format. You can build your own format from pieces like dd, MM, yyyy, HH and mm; MM is the month and mm is minutes, so don't mix them up.
The '1.1-2' in the number pipe is made of three numbers: the minimum number of integer digits, the minimum number of decimal digits and the maximum number of decimal digits. So 4.6666 is rounded to at least one and at most two decimals and becomes 4.67.
currency takes the currency code as its first parameter. The second one decides how the symbol is shown. The reason it prints "TRY" instead of a symbol is that the app is currently running with English (US) settings. You probably noticed the commas and dots look wrong for Turkey, too. We will get to that in a moment.
Chaining Pipes
You can feed one pipe's output into another. It reads left to right:
<p>{{ city | slice:0:3 | uppercase }}</p> <!-- IST -->
<p>{{ orderDate | date:'MMMM' | lowercase }}</p> <!-- may -->
<li *ngFor="let item of items | slice:0:5">{{ item }}</li>
<pre>{{ order | json }}</pre>
slice cuts a piece out of a string or an array, exactly like JavaScript's slice method. In the first line we take the first three letters of the city and then uppercase them. Combined with the *ngFor we saw earlier, it is the shortest way to say "show the first five items of the list."
The json pipe turns an object into readable JSON text. It is not meant for users; it is a quick answer to "what is inside this object?" while you develop. Put it inside a <pre> and it shows up indented.
Turkish Locale: registerLocaleData
To show dates with Turkish month names and numbers with a dot for thousands and a comma for decimals, as in Turkey, we need to give Angular the Turkish locale data. Open app.module.ts:
import { LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { registerLocaleData } from '@angular/common';
import localeTr from '@angular/common/locales/tr';
import { AppComponent } from './app.component';
registerLocaleData(localeTr);
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [{ provide: LOCALE_ID, useValue: 'tr' }],
bootstrap: [AppComponent]
})
export class AppModule { }
There are two steps. registerLocaleData(localeTr) loads Turkish month names, day names and number formats into Angular. The LOCALE_ID line tells the app "this is your default language." Why that line sits inside providers is something we will cover in detail when we get to services in upcoming posts.
If you set LOCALE_ID but forget to register the data, the app fails at startup with a "Missing locale data" error. You need both. Now look at the same template again:
<p>{{ orderDate | date }}</p> <!-- 12 May 2021 -->
<p>{{ orderDate | date:'d MMMM y, EEEE' }}</p> <!-- 12 Mayıs 2021, Çarşamba -->
<p>{{ total | number }}</p> <!-- 1.450 -->
<p>{{ rating | number:'1.1-2' }}</p> <!-- 4,67 -->
<p>{{ discount | percent }}</p> <!-- %15 -->
<p>{{ total | currency:'TRY' }}</p> <!-- ₺1.450,00 -->
Month and day names are now Turkish, the separators have swapped places, and even the percent sign follows the Turkish rule and moves in front of the number. The "May" in the first line is not a bug; the Turkish abbreviation for Mayıs really is "May".
One more trap: switching the locale to Turkish does not change the default currency. Write {{ total | currency }} without a code and you get "$1.450,00". Either pass 'TRY' every time or add { provide: DEFAULT_CURRENCY_CODE, useValue: 'TRY' } to the providers list; DEFAULT_CURRENCY_CODE also comes from @angular/core.
Writing Your Own Pipe
Product descriptions are long, and on the card we want to show the first forty characters followed by an ellipsis. Let's have the CLI generate a pipe:
ng generate pipe truncate
The short form is ng g p truncate. Unlike with components, the CLI does not create a folder; it puts truncate.pipe.ts and a test file under src/app and adds the pipe to the declarations list in app.module.ts. Pipes, just like components, must be registered in a module. We fill in the generated skeleton like this:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate'
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 20, suffix = '...'): string {
if (value.length <= limit) {
return value;
}
return value.slice(0, limit) + suffix;
}
}
The name in the @Pipe decorator is the name you use in the template. The PipeTransform interface asks for a single method: transform. Its first parameter is always the value to the left of the pipe. The rest are the parameters you pass with colons in the template. Since we gave them default values, both are optional.
<p>{{ product.description | truncate:40 }}</p>
<p>{{ product.description | truncate:40:' →' }}</p>
The first line cuts everything after forty characters and adds an ellipsis; the second uses an arrow instead. With strict mode on, passing a string instead of a number to truncate in the template is a compile error; types are checked in templates too.
Pure and Impure: When Does a Pipe Run Again?
Let's write a pipe that filters products that are in stock:
@Pipe({ name: 'inStock' })
export class InStockPipe implements PipeTransform {
transform(products: Product[]): Product[] {
console.log('inStock çalıştı');
return products.filter(p => p.stock > 0);
}
}
We use it in the template as *ngFor="let p of products | inStock". Then a button runs this.products.push(newProduct), and the new product does not appear in the list.
The reason is that pipes are pure by default. A pure pipe runs again only when its input changes. For a number or a string, "changed" means the value changed. For arrays and objects, it means the reference changed. push modifies the same array, the reference stays the same, and Angular does not call the pipe again.
There are two fixes. The first is to create a new array instead of modifying the old one: this.products = [...this.products, newProduct]. The reference changes and the pipe runs. The second is to write pure: false in the decorator and make the pipe impure. Then it runs on every check round; once you see "inStock çalıştı" lines flooding the console, you will understand why you should be wary of it.
A pure pipe is like a calculator's memory: give it the same input and it does not recompute, it remembers the result. Stay pure whenever you can and make producing new references a habit. Some of Angular's own pipes are impure, the best-known being async. We will cover it in detail together with observables in upcoming posts.
A Small Experiment
Your turn to fix the Istanbul problem from the start of the post. Generate a pipe with ng g p trUpper. Inside transform, uppercase the value with value.toLocaleUpperCase('tr-TR'). Put {{ 'istanbul' | uppercase }} and {{ 'istanbul' | trUpper }} side by side in a template and see the difference.
Then prepare an array of products, giving each one a name, price and addedAt field. In the list, show the name with trUpper, the price with currency:'TRY' and the date with date:'d MMMM y'. Look at it first without the Turkish locale, then with registerLocaleData added.
These pipes work nicely inside a single component's template. But when several components need to see the product list, or another component needs to know about the item that was just added to the cart, where do we keep that data? Next up, we talk about services and Angular's dependency injection system.