Skip to main content

Angular Pipes

What are Pipes?

Pipes transform data in templates. They take data as input and return a transformed output for display.

Built-in Pipes

DatePipe

Format dates in various ways.

import { Component } from '@angular/core';
import { DatePipe } from '@angular/common';

@Component({
selector: 'app-date-demo',
standalone: true,
imports: [DatePipe],
template: `
<p>{{ today | date }}</p>
<!-- Output: Jan 15, 2024 -->

<p>{{ today | date : 'short' }}</p>
<!-- Output: 1/15/24, 3:45 PM -->

<p>{{ today | date : 'medium' }}</p>
<!-- Output: Jan 15, 2024, 3:45:30 PM -->

<p>{{ today | date : 'long' }}</p>
<!-- Output: January 15, 2024 at 3:45:30 PM GMT+0 -->

<p>{{ today | date : 'fullDate' }}</p>
<!-- Output: Monday, January 15, 2024 -->

<p>{{ today | date : 'yyyy-MM-dd' }}</p>
<!-- Output: 2024-01-15 -->

<p>{{ today | date : 'h:mm a' }}</p>
<!-- Output: 3:45 PM -->
`,
})
export class DateDemoComponent {
today = new Date();
}

CurrencyPipe

Format numbers as currency.

import { Component } from '@angular/core';
import { CurrencyPipe } from '@angular/common';

@Component({
selector: 'app-price',
standalone: true,
imports: [CurrencyPipe],
template: `
<p>{{ price | currency }}</p>
<!-- Output: $99.99 -->

<p>{{ price | currency : 'EUR' }}</p>
<!-- Output: €99.99 -->

<p>{{ price | currency : 'GBP' : 'symbol' : '1.0-0' }}</p>
<!-- Output: £100 -->

<p>{{ price | currency : 'USD' : 'code' }}</p>
<!-- Output: USD99.99 -->
`,
})
export class PriceComponent {
price = 99.99;
}

DecimalPipe

Format numbers with decimal places.

import { Component } from '@angular/core';
import { DecimalPipe } from '@angular/common';

@Component({
selector: 'app-number',
standalone: true,
imports: [DecimalPipe],
template: `
<p>{{ pi | number }}</p>
<!-- Output: 3.142 -->

<p>{{ pi | number : '1.0-0' }}</p>
<!-- Output: 3 -->

<p>{{ pi | number : '3.1-5' }}</p>
<!-- Output: 003.14159 -->

<p>{{ largeNumber | number }}</p>
<!-- Output: 1,234,567.89 -->
`,
})
export class NumberComponent {
pi = 3.14159265359;
largeNumber = 1234567.89;
}

PercentPipe

Format numbers as percentages.

import { Component } from '@angular/core';
import { PercentPipe } from '@angular/common';

@Component({
selector: 'app-percent',
standalone: true,
imports: [PercentPipe],
template: `
<p>{{ 0.75 | percent }}</p>
<!-- Output: 75% -->

<p>{{ 0.5 | percent : '2.2-2' }}</p>
<!-- Output: 50.00% -->

<p>Success rate: {{ successRate | percent }}</p>
<!-- Output: Success rate: 89% -->
`,
})
export class PercentComponent {
successRate = 0.89;
}

UpperCasePipe, LowerCasePipe, TitleCasePipe

Transform string case.

import { Component } from '@angular/core';
import { UpperCasePipe, LowerCasePipe, TitleCasePipe } from '@angular/common';

@Component({
selector: 'app-text',
standalone: true,
imports: [UpperCasePipe, LowerCasePipe, TitleCasePipe],
template: `
<p>{{ text | uppercase }}</p>
<!-- Output: HELLO WORLD -->

<p>{{ text | lowercase }}</p>
<!-- Output: hello world -->

<p>{{ text | titlecase }}</p>
<!-- Output: Hello World -->
`,
})
export class TextComponent {
text = 'hello world';
}

SlicePipe

Extract a subset of an array or string.

import { Component } from '@angular/core';
import { SlicePipe } from '@angular/common';

@Component({
selector: 'app-slice',
standalone: true,
imports: [SlicePipe],
template: `
<!-- Array slicing -->
<p>{{ items | slice : 0 : 3 }}</p>
<!-- Output: Item 1,Item 2,Item 3 -->

<!-- String slicing -->
<p>{{ text | slice : 0 : 10 }}...</p>
<!-- Output: Hello Worl... -->

<!-- Negative indices -->
<p>{{ items | slice : -2 }}</p>
<!-- Output: Item 4,Item 5 -->
`,
})
export class SliceComponent {
items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
text = 'Hello World! This is a long text.';
}

JsonPipe

Display objects as JSON (useful for debugging).

import { Component } from '@angular/core';
import { JsonPipe } from '@angular/common';

@Component({
selector: 'app-debug',
standalone: true,
imports: [JsonPipe],
template: `
<pre>{{ user | json }}</pre>
<!-- Output:
{
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
-->
`,
})
export class DebugComponent {
user = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
}

AsyncPipe

Automatically subscribe and unsubscribe from Observables.

import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Observable, interval } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
selector: 'app-async',
standalone: true,
imports: [AsyncPipe],
template: `
<p>Time: {{ time$ | async }}</p>
<p>User: {{ user$ | async }}</p>
`,
})
export class AsyncComponent {
time$ = interval(1000).pipe(map(() => new Date()));

user$ = new Observable(observer => {
setTimeout(() => {
observer.next({ name: 'Alice', email: 'alice@example.com' });
}, 1000);
});
}

Custom Pipes

Create your own pipes for specific transformations.

Basic Custom Pipe

// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'truncate',
standalone: true,
})
export class TruncatePipe implements PipeTransform {
transform(
value: string,
limit: number = 20,
ellipsis: string = '...'
): string {
if (!value) return '';
if (value.length <= limit) return value;
return value.substring(0, limit) + ellipsis;
}
}

// Usage
@Component({
template: `
<p>{{ longText | truncate : 50 }}</p>
<p>{{ longText | truncate : 30 : '---' }}</p>
`,
})
export class MyComponent {
longText = 'This is a very long text that needs to be truncated';
}

Time Ago Pipe

// time-ago.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'timeAgo',
standalone: true
})
export class TimeAgoPipe implements PipeTransform {
transform(value: Date | string): string {
const date = value instanceof Date ? value : new Date(value);
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);

if (seconds < 60) return 'just now';

const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60
};

for (const [name, secondsInInterval] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInInterval);
if (interval >= 1) {
return interval === 1
? `1 ${name} ago`
: `${interval} ${name}s ago`;
}
}

return 'just now';
}
}

// Usage
<p>Posted {{ post.createdAt | timeAgo }}</p>
<!-- Output: Posted 2 hours ago -->

Filter Pipe

// filter.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'filter',
standalone: true,
})
export class FilterPipe implements PipeTransform {
transform\<T\>(items: T[], searchText: string, property: keyof T): T[] {
if (!items || !searchText) return items;

searchText = searchText.toLowerCase();
return items.filter(item => {
const value = String(item[property]).toLowerCase();
return value.includes(searchText);
});
}
}

// Usage
@Component({
template: `
<input [(ngModel)]="searchQuery" placeholder="Search..." />
<ul>
@for (user of users | filter:searchQuery:'name'; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`,
})
export class UserListComponent {
searchQuery = '';
users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
];
}

Sort Pipe

// sort.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'sort',
standalone: true
})
export class SortPipe implements PipeTransform {
transform\<T\>(items: T[], property: keyof T, order: 'asc' | 'desc' = 'asc'): T[] {
if (!items || !property) return items;

return [...items].sort((a, b) => {
const aValue = a[property];
const bValue = b[property];

if (aValue < bValue) return order === 'asc' ? -1 : 1;
if (aValue > bValue) return order === 'asc' ? 1 : -1;
return 0;
});
}
}

// Usage
<select [(ngModel)]="sortOrder">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>

<ul>
@for (product of products | sort:'price':sortOrder; track product.id) {
<li>{{ product.name }} - ${{ product.price }}</li>
}
</ul>

Highlight Pipe

// highlight.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Pipe({
name: 'highlight',
standalone: true
})
export class HighlightPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}

transform(value: string, search: string): SafeHtml {
if (!search || !value) return value;

const regex = new RegExp(search, 'gi');
const highlighted = value.replace(
regex,
match => `<mark>${match}</mark>`
);

return this.sanitizer.bypassSecurityTrustHtml(highlighted);
}
}

// Usage
<p [innerHTML]="text | highlight:searchTerm"></p>

Pure vs Impure Pipes

Pure Pipes (Default)

Execute only when input value changes.

@Pipe({
name: 'truncate',
standalone: true,
pure: true, // Default
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number): string {
console.log('Pipe executed');
return value.substring(0, limit);
}
}

Impure Pipes

Execute on every change detection cycle.

@Pipe({
name: 'filterImpure',
standalone: true,
pure: false // Executes every change detection
})
export class FilterImpurePipe implements PipeTransform {
transform(items: any[], filter: any): any[] {
console.log('Filter executed');
return items.filter(item => /* filter logic */);
}
}

⚠️ Warning: Impure pipes can hurt performance!

Chaining Pipes

Combine multiple pipes.

@Component({
template: `
<!-- Chain multiple pipes -->
<p>{{ text | uppercase | slice : 0 : 10 }}</p>

<!-- Date formatting chain -->
<p>{{ date | date : 'medium' | uppercase }}</p>

<!-- Custom pipes chain -->
<p>{{ longText | truncate : 50 | uppercase }}</p>
`,
})
export class ChainComponent {
text = 'hello world';
date = new Date();
longText = 'This is a very long text';
}

Pipes with Parameters

@Pipe({
name: 'exponential',
standalone: true
})
export class ExponentialPipe implements PipeTransform {
transform(value: number, exponent: number = 1, digits: number = 2): string {
const result = Math.pow(value, exponent);
return result.toFixed(digits);
}
}

// Usage
<p>{{ 2 | exponential:3:0 }}</p>
<!-- Output: 8 -->

<p>{{ 5 | exponential:2:3 }}</p>
<!-- Output: 25.000 -->

Testing Pipes

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

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

beforeEach(() => {
pipe = new TruncatePipe();
});

it('should create', () => {
expect(pipe).toBeTruthy();
});

it('should truncate long text', () => {
const result = pipe.transform('Hello World', 5);
expect(result).toBe('Hello...');
});

it('should not truncate short text', () => {
const result = pipe.transform('Hi', 10);
expect(result).toBe('Hi');
});

it('should use custom ellipsis', () => {
const result = pipe.transform('Hello World', 5, '---');
expect(result).toBe('Hello---');
});
});

Best Practices

✅ Do

// Keep pipes pure when possible
@Pipe({ name: 'myPipe', pure: true })

// Use descriptive names
@Pipe({ name: 'formatPhoneNumber' })

// Handle edge cases
transform(value: string): string {
if (!value) return '';
// transformation logic
}

// Use TypeScript types
transform(value: number, decimals: number = 2): string {
return value.toFixed(decimals);
}

❌ Don't

// Don't make pipes impure unnecessarily
@Pipe({ name: 'filter', pure: false }) // ❌ Can hurt performance

// Don't do complex calculations in pipes
transform(items: any[]): any[] {
// ❌ Heavy computation on every change detection
return items.map(/* complex operation */);
}

// Don't mutate input data
transform(items: any[]): any[] {
items.sort(); // ❌ Mutates original array
return items;
}

// ✅ Better - create new array
transform(items: any[]): any[] {
return [...items].sort();
}

Common Use Cases

1. Safe HTML Pipe

import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Pipe({
name: 'safeHtml',
standalone: true,
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}

transform(html: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(html);
}
}

2. File Size Pipe

@Pipe({
name: 'fileSize',
standalone: true,
})
export class FileSizePipe implements PipeTransform {
transform(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';

const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));

return (
parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]
);
}
}

// Usage: {{ 1536 | fileSize }} → "1.5 KB"

3. Default Value Pipe

@Pipe({
name: 'default',
standalone: true,
})
export class DefaultPipe implements PipeTransform {
transform(value: any, defaultValue: any = 'N/A'): any {
return value ?? defaultValue;
}
}

// Usage: {{ user.phone | default:'No phone' }}

Interview Questions

Q: What's the difference between pure and impure pipes? A: Pure pipes execute only when input values change. Impure pipes execute on every change detection cycle, which can impact performance.

Q: When should you use the async pipe? A: Use async pipe with Observables or Promises to automatically handle subscription and unsubscription, preventing memory leaks.

Q: How do you pass parameters to pipes? A: Use colon syntax: {{ value | pipeName:param1:param2 }}

Q: Why should you avoid impure pipes? A: Impure pipes run on every change detection, which can cause performance issues, especially with complex transformations.

Next Steps