Custom Pipes
While Angular's built-in pipes cover common formatting tasks, real applications frequently need domain-specific transformations. Custom pipes let you encapsulate that logic once and reuse it across any template in your app.
A custom pipe is simply a TypeScript class decorated with @Pipe that implements the PipeTransform interface.
Anatomy of a Custom Pipe
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'pipeName', // used in templates: {{ value | pipeName }}
standalone: true, // Angular 14+ — no NgModule needed
pure: true, // default; set false only when necessary
})
export class PipeNamePipe implements PipeTransform {
transform(value: InputType, ...args: any[]): OutputType {
// transformation logic
return transformedValue;
}
}The transform method receives:
value— the left-hand side of the pipe operator- Additional arguments passed with
:in the template
Your First Custom Pipe: Truncate
A common need is truncating long strings to a maximum length with an ellipsis. No built-in pipe does this:
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true,
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 100, trail: string = '...'): string {
if (!value) return '';
if (value.length <= limit) return value;
return value.substring(0, limit) + trail;
}
}<!-- Basic usage -->
{{ article.body | truncate }}
<!-- "The quick brown fox jumps over the lazy dog..." -->
<!-- Custom limit -->
{{ article.body | truncate: 50 }}
<!-- Custom ellipsis -->
{{ article.body | truncate: 30 : ' [read more]' }}
<!-- In a card grid -->
@for (article of articles; track article.id) {
<div class="card">
<h3>{{ article.title | truncate: 60 }}</h3>
<p>{{ article.body | truncate: 120 }}</p>
</div>
}Pipe with Multiple Parameters
Let's build a highlight pipe that wraps search-term matches in a span:
// 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(text: string, search: string, cssClass: string = 'highlight'): SafeHtml {
if (!search || !text) return text;
const pattern = new RegExp(`(${search})`, 'gi');
const highlighted = text.replace(
pattern,
`<span class="${cssClass}">$1</span>`
);
return this.sanitizer.bypassSecurityTrustHtml(highlighted);
}
}<!-- Bind to innerHTML because the pipe returns HTML --> <p [innerHTML]="result.title | highlight: searchTerm"></p> <p [innerHTML]="result.body | highlight: searchTerm : 'match'"></p>
bypassSecurityTrustHtml when the input comes from a trusted source. For user-generated content, sanitize the text first before highlighting.Impure Custom Pipes
By default all pipes are pure. Set pure: false when your pipe needs to react to changes inside mutable objects or arrays:
// filter.pipe.ts — filters an array in-place
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filter',
standalone: true,
pure: false, // re-runs on every change detection cycle
})
export class FilterPipe implements PipeTransform {
transform<T>(items: T[], predicate: (item: T) => boolean): T[] {
if (!items || !predicate) return items;
return items.filter(predicate);
}
}// Usage in component
export class ProductListComponent {
products = [
{ name: 'Laptop', inStock: true },
{ name: 'Mouse', inStock: false },
{ name: 'Keyboard', inStock: true },
];
// Predicate function passed to the pipe
isInStock = (p: { inStock: boolean }) => p.inStock;
}@for (product of products | filter: isInStock; track product.name) {
<div>{{ product.name }}</div>
}
<!-- Shows only: Laptop, Keyboard -->Pipe with Dependency Injection
Custom pipes can inject Angular services, just like components and directives:
// file-size.pipe.ts — formats bytes into human-readable sizes
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'fileSize',
standalone: true,
})
export class FileSizePipe implements PipeTransform {
private readonly units = ['B', 'KB', 'MB', 'GB', 'TB'];
transform(bytes: number, precision: number = 1): string {
if (bytes === 0) return '0 B';
if (isNaN(bytes) || !isFinite(bytes)) return '-';
const exp = Math.floor(Math.log(bytes) / Math.log(1024));
const unit = this.units[Math.min(exp, this.units.length - 1)];
const value = bytes / Math.pow(1024, exp);
return `${value.toFixed(precision)} ${unit}`;
}
}// translation.pipe.ts — uses a translation service
import { Pipe, PipeTransform, inject } from '@angular/core';
import { TranslationService } from './translation.service';
@Pipe({
name: 'translate',
standalone: true,
pure: false, // re-run when language changes
})
export class TranslatePipe implements PipeTransform {
private translationService = inject(TranslationService);
transform(key: string, params?: Record<string, string>): string {
return this.translationService.get(key, params);
}
}Registering Custom Pipes
Standalone Components (Angular 14+)
Add the pipe to the component's imports array:
@Component({
selector: 'app-articles',
standalone: true,
imports: [TruncatePipe, HighlightPipe, FileSizePipe],
template: `
<p>{{ article.body | truncate: 100 }}</p>
<p>{{ file.size | fileSize }}</p>
`,
})
export class ArticlesComponent {}Module-Based Apps
Declare the pipe in an NgModule and export it for use elsewhere:
@NgModule({
declarations: [TruncatePipe, HighlightPipe, FileSizePipe],
exports: [TruncatePipe, HighlightPipe, FileSizePipe],
})
export class SharedPipesModule {}Generating Pipes with the Angular CLI
# Generate a pipe in a pipes/ directory ng generate pipe pipes/truncate # or shorthand ng g pipe pipes/truncate # With standalone flag (default in Angular 17+) ng g pipe pipes/truncate --standalone
CREATE src/app/pipes/truncate.pipe.spec.ts CREATE src/app/pipes/truncate.pipe.ts
Testing Custom Pipes
Because pipes are pure functions, they are the easiest Angular construct to unit test:
// truncate.pipe.spec.ts
import { TruncatePipe } from './truncate.pipe';
describe('TruncatePipe', () => {
let pipe: TruncatePipe;
beforeEach(() => {
pipe = new TruncatePipe();
});
it('should return the original string when under the limit', () => {
expect(pipe.transform('Hello', 10)).toBe('Hello');
});
it('should truncate when over the limit', () => {
expect(pipe.transform('Hello World', 5)).toBe('Hello...');
});
it('should use custom ellipsis', () => {
expect(pipe.transform('Hello World', 5, ' [more]')).toBe('Hello [more]');
});
it('should handle empty input', () => {
expect(pipe.transform('', 10)).toBe('');
});
});Best Practices
Keep pipes pure whenever possible — pure pipes are cached and efficient
Give pipes a single responsibility — one pipe, one transformation
Use descriptive names that read naturally in templates: value | truncate, value | fileSize
Handle null/undefined inputs gracefully — always add null checks in transform()
Avoid side effects in pipes — pipes should be deterministic given the same inputs
For pipes that need service injection, use inject() (Angular 14+) over constructor injection for cleaner code
Summary
Custom pipes let you package any reusable template transformation into a clean, testable unit:
- Decorate a class with
@Pipe({ name: 'myPipe' }) - Implement
PipeTransformwith atransform(value, ...args)method - Register in the component's
imports(standalone) or module'sdeclarations - Use in templates:
{{ value | myPipe: arg1 : arg2 }}
Pure pipes are ideal for stateless transformations. Only reach for pure: false when you genuinely need to track mutable data changes.