Table of Contents
- What Are Pipes in Angular?
- Built-in Pipes vs. Custom Pipes
- When to Use Custom Pipes
- How to Create Custom Pipes: Step-by-Step
- Advanced Custom Pipe Techniques
- Best Practices for Custom Pipes
- Common Pitfalls to Avoid
- Conclusion
- References
What Are Pipes in Angular?
Pipes in Angular are a way to transform data before displaying it in a template. They take input data, process it, and return a transformed output. Pipes are used with the | (pipe) operator in templates, making them concise and easy to integrate.
Syntax Example:
<!-- Using the built-in DatePipe to format a date -->
<p>{{ today | date:'medium' }}</p>
Here, date is the pipe name, and 'medium' is an optional parameter to specify the date format. Pipes can be chained (e.g., {{ value | pipe1 | pipe2 }}) or parameterized (e.g., {{ value | pipe:param1:param2 }}).
Built-in Pipes vs. Custom Pipes
Angular ships with several built-in pipes to handle common transformations:
| Built-in Pipe | Purpose | Example Usage |
|---|---|---|
DatePipe | Format dates | `{{ date |
UpperCasePipe | Convert text to uppercase | `{{ name |
LowerCasePipe | Convert text to lowercase | `{{ name |
CurrencyPipe | Format currency | `{{ price |
DecimalPipe | Format numbers with decimals | `{{ value |
JsonPipe | Convert objects to JSON strings | `{{ user |
Limitations of Built-in Pipes:
Built-in pipes work for generic use cases, but they can’t handle application-specific logic. For example:
- Formatting data based on custom business rules (e.g., “VIP” tags for users with a balance > $1000).
- Filtering lists with dynamic criteria (e.g., search terms entered by the user).
- Transforming data using external services (e.g., translating text via a custom API).
This is where custom pipes shine: they extend Angular’s capabilities to meet your app’s unique needs.
When to Use Custom Pipes
Custom pipes are ideal for reusable, template-focused data transformations. Use them when:
1. You Need Application-Specific Formatting
If built-in pipes don’t support your formatting requirements (e.g., converting a phone number to a specific pattern like (123) 456-7890), a custom pipe can encapsulate this logic.
2. You Want to Reuse Logic Across Components
Instead of duplicating transformation code in multiple components, a custom pipe centralizes the logic, making it reusable and easier to maintain.
3. You Need to Filter or Sort Data (With Caution)
Pipes can filter or sort lists, but use this sparingly. For small datasets, a filter pipe (e.g., {{ users | filter:searchTerm }}) is convenient. For large datasets, consider using component logic or state management (e.g., NgRx) to avoid performance issues.
4. You Need to Integrate External Services
Pipes can inject services (e.g., translation services, API clients) to transform data dynamically. For example, a TranslatePipe could fetch translations from a service.
When Not to Use Custom Pipes:
- Heavy Computations: Pipes run frequently (on every change detection cycle for impure pipes), so avoid CPU-intensive tasks (e.g., complex calculations).
- Stateful Logic: Pipes should ideally be stateless (no internal state). If you need to manage state, use a service instead.
How to Create Custom Pipes: Step-by-Step
Creating a custom pipe involves three key steps: generate the pipe, implement the transformation logic, and use it in templates. Let’s walk through an example.
Step 1: Generate a Pipe with Angular CLI
The Angular CLI simplifies pipe creation. Run:
ng generate pipe shared/pipes/capitalize # Creates a pipe in src/app/shared/pipes/
This generates:
- A pipe file (
capitalize.pipe.ts). - A test file (
capitalize.pipe.spec.ts). - Automatically declares the pipe in the nearest module (e.g.,
SharedModule).
Step 2: Understand the Pipe Structure
The generated pipe will look like this:
// capitalize.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'capitalize', // Name used in templates (e.g., {{ value | capitalize }})
pure: true // Default: pure pipe (runs only when input changes)
})
export class CapitalizePipe implements PipeTransform {
// Transform method: input → output
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
Key components:
@Pipedecorator: Defines the pipe’s name and purity (pure/impure).PipeTransforminterface: Requires implementing thetransformmethod, which contains the transformation logic.
Step 3: Implement the Transformation Logic
Let’s update the transform method to capitalize 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 {
// Handle null/undefined to avoid runtime errors
if (!value) return value;
// Capitalize first letter, lowercase the rest
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
}
}
Step 4: Use the Pipe in a Template
First, ensure the pipe is declared in a module accessible to your component (e.g., SharedModule). Then use it in a template:
<!-- user.component.html -->
<p>Welcome, {{ userName | capitalize }}!</p>
If userName is 'john doe', the output will be:
Welcome, John doe!
Example 2: Parameterized Pipe (Truncate Text)
Let’s create a truncate pipe that shortens text to a specified length and appends an ellipsis (...).
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
// Parameters: value (text), length (max length), ellipsis (suffix)
transform(value: string, length: number = 10, ellipsis: string = '...'): string {
if (!value) return value;
return value.length > length ? `${value.slice(0, length)}${ellipsis}` : value;
}
}
Usage in Template:
<!-- Truncate to 15 characters with default ellipsis -->
<p>{{ longDescription | truncate:15 }}</p>
<!-- Truncate to 20 characters with custom ellipsis -->
<p>{{ longDescription | truncate:20:'... Read More' }}</p>
Advanced Custom Pipe Techniques
1. Parameterized Pipes with Multiple Arguments
Pipes can accept multiple parameters by adding them to the transform method. For example, a formatPhone pipe that accepts a country code:
transform(phone: string, countryCode: string = 'US'): string {
switch (countryCode) {
case 'US': return `(${phone.slice(0, 3)}) ${phone.slice(3, 6)}-${phone.slice(6)}`;
case 'UK': return `+44 ${phone.slice(0, 4)} ${phone.slice(4, 8)} ${phone.slice(8)}`;
default: return phone;
}
}
Usage:
{{ '1234567890' | formatPhone:'US' }} <!-- Output: (123) 456-7890 -->
2. Chaining Pipes
Pipes can be chained to combine transformations. For example:
<!-- Capitalize then truncate -->
<p>{{ 'hello world' | capitalize | truncate:5 }}</p> <!-- Output: Hello... -->
3. Impure Pipes
By default, pipes are pure, meaning they only run when their input value changes (e.g., a string/number changes, or an object’s reference changes). Impure pipes run on every change detection cycle, making them useful for dynamic data that changes without a reference update (e.g., timers, real-time data).
To create an impure pipe, set pure: false in the @Pipe decorator:
// current-time.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'currentTime',
pure: false // Impure pipe: runs on every change detection cycle
})
export class CurrentTimePipe implements PipeTransform {
transform(): string {
return new Date().toLocaleTimeString(); // Updates every second
}
}
Usage:
<p>Current Time: {{ | currentTime }}</p> <!-- Updates every second -->
⚠️ Note: Impure pipes can hurt performance. Use them only when necessary (e.g., real-time data).
4. Dependency Injection in Pipes
Pipes can inject services to access external data or logic. For example, a TranslatePipe that uses a TranslationService to fetch translations:
// translate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { TranslationService } from '../services/translation.service';
@Pipe({ name: 'translate' })
export class TranslatePipe implements PipeTransform {
constructor(private translationService: TranslationService) {}
transform(key: string): string {
// Use the service to get the translated value
return this.translationService.getTranslation(key);
}
}
Best Practices for Custom Pipes
1. Keep Pipes Pure When Possible
Pure pipes are more performant because they run only when inputs change. Use impure pipes only for dynamic data that can’t be tracked via references.
2. Avoid Heavy Computations
Pipes run frequently (especially impure ones). Avoid tasks like large dataset filtering, complex math, or API calls. Offload heavy work to services or component logic.
3. Make Pipes Reusable and Stateless
Pipes should be stateless (no internal state) and generic enough to work across components. For example, a formatCurrency pipe should accept currency codes as parameters instead of hardcoding 'USD'.
4. Handle Edge Cases (Null/Undefined)
Always validate inputs to avoid runtime errors:
transform(value: string): string {
if (!value) return ''; // Handle null/undefined
// ... rest of logic
}
5. Test Pipes Thoroughly
Pipes are easy to test because they’re pure functions. Write unit tests for edge cases (empty strings, null values, invalid inputs).
6. Document Your Pipes
Add comments or JSDoc to explain the pipe’s purpose, parameters, and output:
/**
* Capitalizes the first letter of a string and lowercases the rest.
* @param value - Input string to transform.
* @returns Capitalized string (e.g., "john" → "John").
*/
transform(value: string): string { /* ... */ }
Common Pitfalls to Avoid
1. Overusing Impure Pipes
Impure pipes run on every change detection cycle (e.g., every keystroke, timer tick). This can cause lag in large apps.
2. Filtering Large Arrays with Pipes
A pure pipe filtering an array will not update if the array’s content changes (e.g., adding/removing items) because the array’s reference remains the same. An impure pipe will work but may be slow for large datasets. Instead, filter data in the component or use async pipes with observables.
3. Tight Coupling with Components
Avoid referencing component logic or template variables in pipes. Pipes should depend only on their inputs and injected services.
4. Ignoring Null/Undefined
Failing to handle null/undefined inputs will cause errors when the pipe is used with optional data (e.g., {{ user?.name | capitalize }}).
Conclusion
Custom pipes are a powerful way to extend Angular’s data transformation capabilities. They promote reusability, simplify templates, and encapsulate application-specific logic. By following best practices—like keeping pipes pure, handling edge cases, and avoiding heavy computations—you can create efficient, maintainable pipes that enhance your app’s functionality.
Whether you’re formatting dates, filtering lists, or integrating external services, custom pipes help keep your code clean and your templates readable.