Angular provides many built-in pipes to handle common data transformations such as date formatting, number conversion, and currency display.
However, there are times when you need custom logic that is specific to your application — for example, transforming text in a special way, filtering arrays, or formatting domain-specific data.
In such cases, Angular allows you to create your own custom pipes.
Custom pipes are simple, reusable, and declarative tools for transforming data directly inside your templates.
This post will cover everything about creating and using custom pipes in Angular, including:
- What are custom pipes?
- The structure of a pipe class
- Creating a simple custom pipe
- Using the pipe in templates
- Working with pipe parameters
- Handling edge cases and null values
- Creating pure and impure pipes
- Performance considerations
- Chaining custom pipes
- Testing custom pipes
- Common use cases
- Real-world examples
- Best practices for custom pipes
- Complete working demo
Let’s get started.
1. What Are Custom Pipes?
A custom pipe is a class that implements Angular’s PipeTransform
interface and is decorated with the @Pipe
decorator.
The @Pipe
decorator tells Angular:
- The name of the pipe (how it will be used in templates)
- Whether it’s a pure or impure pipe
Pipes are used in Angular templates to transform values like this:
{{ value | customPipeName }}
A pipe’s transform()
method defines the logic for that transformation.
For example, if you wanted to capitalize the first letter of any word, you could create a pipe named capitalize
.
2. The Structure of a Pipe Class
Every pipe in Angular follows a simple structure:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'pipeName' })
export class PipeNamePipe implements PipeTransform {
transform(value: any, ...args: any[]): any {
// transformation logic
return transformedValue;
}
}
Let’s break this down:
- @Pipe({ name: ‘pipeName’ }) — defines the name used in the template.
- implements PipeTransform — ensures that the class implements a
transform()
method. - transform(value, …args) — called whenever Angular updates the template.
3. Creating a Simple Custom Pipe
Let’s start with a simple example — a pipe that capitalizes the first letter of a string.
capitalize.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'capitalize' })
export class CapitalizePipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
Explanation
- We import
Pipe
andPipeTransform
from@angular/core
. - The pipe’s name is set to
'capitalize'
. - The
transform()
method checks for empty values to prevent errors. - The first character is converted to uppercase, and the rest of the string remains the same.
4. Using the Custom Pipe in a Component Template
Once the pipe is created, you can use it in your component templates.
Example
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
template: `
<p>{{ 'angular' | capitalize }}</p>
<p>{{ 'custom pipe' | capitalize }}</p>
`
})
export class DemoComponent {}
Output
Angular
Custom pipe
5. Registering the Pipe in a Module
Before you can use your custom pipe, you must declare it in an Angular module.
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { CapitalizePipe } from './capitalize.pipe';
@NgModule({
declarations: [
AppComponent,
CapitalizePipe
],
imports: [BrowserModule],
bootstrap: [AppComponent]
})
export class AppModule {}
Now, your CapitalizePipe
is available for use across all templates within this module.
6. Adding Parameters to a Custom Pipe
Pipes can also accept arguments to make them flexible.
Let’s extend the capitalize pipe to handle full capitalization or title case depending on an argument.
enhanced-capitalize.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'capitalize' })
export class CapitalizePipe implements PipeTransform {
transform(value: string, mode: 'first' | 'all' | 'title' = 'first'): string {
if (!value) return '';
switch (mode) {
case 'all':
return value.toUpperCase();
case 'title':
return value
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
default:
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
}
Template Usage
<p>{{ 'angular' | capitalize }}</p>
<p>{{ 'angular' | capitalize:'all' }}</p>
<p>{{ 'angular custom pipe' | capitalize:'title' }}</p>
Output
Angular
ANGULAR
Angular Custom Pipe
This makes your pipe more reusable across different situations.
7. Handling Null or Undefined Values
Always handle cases where input values might be null, undefined, or empty.
Without such handling, your pipe may throw runtime errors.
Example
transform(value: string): string {
if (!value || typeof value !== 'string') return '';
return value.charAt(0).toUpperCase() + value.slice(1);
}
This ensures your pipe is safe to use with unknown or missing values.
8. Creating a Pure vs Impure Pipe
By default, Angular pipes are pure, meaning they only run when the input value changes.
This improves performance because Angular doesn’t re-run the transformation on every change detection cycle.
Sometimes, however, you need an impure pipe — for example, if your pipe depends on data that changes without the reference changing (like modifying an array in place).
Example of a Pure Pipe (Default)
@Pipe({ name: 'pureExample' })
export class PureExamplePipe implements PipeTransform {
transform(value: any[]): number {
console.log('Pure pipe executed');
return value.length;
}
}
Example of an Impure Pipe
@Pipe({ name: 'impureExample', pure: false })
export class ImpureExamplePipe implements PipeTransform {
transform(value: any[]): number {
console.log('Impure pipe executed');
return value.length;
}
}
Impure pipes run on every change detection, so use them only when necessary.
9. Performance Considerations
Pipes are designed to be lightweight and efficient.
However, using impure pipes or performing heavy computations inside a pipe can degrade performance.
Best practices:
- Keep pipe logic simple.
- Avoid impure pipes when possible.
- Use caching or memoization if the transformation is expensive.
- Use async pipes instead of manually subscribing to observables.
10. Chaining Custom Pipes
You can chain multiple pipes together for complex transformations.
Example
<p>{{ 'hello world' | capitalize:'title' | uppercase }}</p>
Here:
- The
capitalize
pipe converts it toHello World
. - The built-in
uppercase
pipe converts it toHELLO WORLD
.
Pipes are executed left to right, making transformations clean and readable.
11. Example: Creating a Truncate Pipe
Let’s create a custom pipe that shortens long text strings and adds ellipsis.
truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 20, ellipsis: string = '...'): string {
if (!value) return '';
return value.length > limit ? value.substring(0, limit) + ellipsis : value;
}
}
Template
<p>{{ 'This is a very long sentence that needs truncation.' | truncate:25 }}</p>
Output
This is a very long sent...
This pipe is extremely useful in dashboards, cards, and mobile layouts where text must fit within limited space.
12. Example: Creating a Filter Pipe for Arrays
Let’s create a custom pipe to filter an array of objects based on a search term.
filter.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
transform(items: any[], searchText: string, field: string): any[] {
if (!items || !searchText) return items;
return items.filter(item =>
item[field].toLowerCase().includes(searchText.toLowerCase())
);
}
}
Template Usage
<input [(ngModel)]="searchTerm" placeholder="Search name" />
<ul>
<li *ngFor="let user of users | filter:searchTerm:'name'">{{ user.name }}</li>
</ul>
Component
export class UserListComponent {
searchTerm = '';
users = [
{ name: 'Alice' },
{ name: 'Bob' },
{ name: 'Charlie' },
{ name: 'David' }
];
}
Now typing “bo” will only show Bob.
13. Real-World Example: Formatting a Phone Number
Many apps need to display phone numbers in a specific format.
Let’s build a pipe that converts a 10-digit number into a formatted phone string.
phone.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'phoneFormat' })
export class PhoneFormatPipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
const cleaned = value.replace(/\D/g, '');
if (cleaned.length !== 10) return value;
return (${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}
;
}
}
Template
<p>{{ '1234567890' | phoneFormat }}</p>
Output
(123) 456-7890
14. Example: Converting Temperature Units
You can also create pipes for domain-specific data transformations.
temperature.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'temperature' })
export class TemperaturePipe implements PipeTransform {
transform(value: number, unit: 'C' | 'F' = 'C'): string {
if (value == null) return '';
if (unit === 'F') {
return ((value * 9) / 5 + 32).toFixed(1) + ' °F';
}
return value.toFixed(1) + ' °C';
}
}
Template
<p>{{ 30 | temperature }}</p>
<p>{{ 30 | temperature:'F' }}</p>
Output
30.0 °C
86.0 °F
15. Testing a Custom Pipe
Custom pipes should be tested to ensure they behave correctly.
Angular makes pipe testing simple.
capitalize.pipe.spec.ts
import { CapitalizePipe } from './capitalize.pipe';
describe('CapitalizePipe', () => {
const pipe = new CapitalizePipe();
it('transforms "angular" to "Angular"', () => {
expect(pipe.transform('angular')).toBe('Angular');
});
it('returns empty string for null input', () => {
expect(pipe.transform(null as any)).toBe('');
});
});
Running this test will verify that your pipe works correctly for all input types.
16. Best Practices for Custom Pipes
- Keep pipes simple and fast — avoid complex computations.
- Always handle null or undefined values.
- Use pure pipes whenever possible for better performance.
- Re-use pipes across components instead of duplicating logic.
- Avoid modifying input data — pipes should be pure transformations.
- Add unit tests to validate logic.
- Use descriptive names for pipes (e.g.,
truncate
,capitalize
,filter
). - Prefer built-in pipes if they achieve the same goal.
17. Full Example: Custom Pipes in a Product Catalog
Here’s a real-world example combining multiple custom pipes.
product.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'discountedPrice' })
export class DiscountedPricePipe implements PipeTransform {
transform(price: number, discount: number = 0): number {
if (!price || discount <= 0) return price;
return price - price * (discount / 100);
}
}
product-list.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html'
})
export class ProductListComponent {
products = [
{ name: 'Laptop', price: 1000, discount: 10 },
{ name: 'Phone', price: 600, discount: 5 },
{ name: 'Tablet', price: 400, discount: 0 }
];
}
product-list.component.html
<h3>Product List</h3>
<table>
<tr>
<th>Product</th>
<th>Original Price</th>
<th>Discounted Price</th>
</tr>
<tr *ngFor="let product of products">
<td>{{ product.name | capitalize }}</td>
<td>{{ product.price | currency:'USD' }}</td>
<td>{{ product.price | discountedPrice:product.discount | currency:'USD' }}</td>
</tr>
</table>
Output
Product | Original Price | Discounted Price |
---|---|---|
Laptop | $1,000.00 | $900.00 |
Phone | $600.00 | $570.00 |
Tablet | $400.00 | $400.00 |
18. Complete Demo Application Structure
src/
├── app/
│ ├── capitalize.pipe.ts
│ ├── truncate.pipe.ts
│ ├── filter.pipe.ts
│ ├── phone.pipe.ts
│ ├── temperature.pipe.ts
│ ├── discounted-price.pipe.ts
│ ├── app.module.ts
│ ├── product-list/
│ │ ├── product-list.component.ts
│ │ └── product-list.component.html
│ └── app.component.ts
Each pipe handles a different transformation, making your app modular and clean.
Leave a Reply