AngularJSCustom Pipes

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

TS
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:

TS
// 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;
  }
}

HTML
<!-- 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:

TS
// 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);
  }
}

HTML
<!-- Bind to innerHTML because the pipe returns HTML -->
<p [innerHTML]="result.title | highlight: searchTerm"></p>
<p [innerHTML]="result.body | highlight: searchTerm : 'match'"></p>
Warning
Only use 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:

TS
// 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);
  }
}

TS
// 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;
}

HTML
@for (product of products | filter: isInStock; track product.name) {
  <div>{{ product.name }}</div>
}
<!-- Shows only: Laptop, Keyboard -->
Warning
Impure pipes run on every change detection cycle. For large lists this can be expensive. Prefer filtering in the component and binding to a computed property whenever possible.
Pipe with Dependency Injection

Custom pipes can inject Angular services, just like components and directives:

TS
// 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}`;
  }
}

TS
// 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:

TS
@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:

TS
@NgModule({
  declarations: [TruncatePipe, HighlightPipe, FileSizePipe],
  exports: [TruncatePipe, HighlightPipe, FileSizePipe],
})
export class SharedPipesModule {}
Generating Pipes with the Angular CLI

Bash
# 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:

TS
// 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

Tip
If a transformation is complex, write a helper function that the pipe delegates to. This makes the function easy to test independently of the Angular pipe infrastructure.
Summary

Custom pipes let you package any reusable template transformation into a clean, testable unit:

  1. Decorate a class with @Pipe({ name: 'myPipe' })
  2. Implement PipeTransform with a transform(value, ...args) method
  3. Register in the component's imports (standalone) or module's declarations
  4. 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.