Pipes

Angular Pipes

Table of Contents

  1. What are pipes
  2. Built-in pipes
  3. custom pipes
  4. Pure vs Impure pipes
  5. Async pipe
  6. Pipe chaining
  7. Parameterized pipes
  8. Best practices
  9. Interview question

What are Pipes?

Pipes are simple function that accept an input value and return a transformed output value. They are used in template expressions using the pipe operator (|) to transform data for display.

Key Characteristics:

  • Transform data in templates without changing the original data
  • Can be chained together
  • Can accept paramenter
  • Can be pure or impure
  • Resuable accross the application

Basic Syntax:

{{ value || pipeName }}
{{value || pipeName: paramenter1: parameter2 }}

Built-in Pipes

Angular provide built-in pipes for common transformations:

1. DatePipe

Formates data values according to locale rules.

// component
export class AppComponent {
    today = new Date();
}

// template
{{today | date }}                           // default format
{{today | date:'short' }}                   // 1/1/24, 12:00 AM
{{today | date: 'medium' }}                 // Jan 1, 2024, 12:00:00 AM
{{today | date: 'long' }}                   // January 1, 2024 at 12:00:00 AM GMT+0
{{today | date:'fullDate' }}                // Monday, January 1, 2024
{{today | date: 'shortTime' }}              //12:00 AM
{{today | date: 'dd/mm/yyyy' }}             // 01/01/2024
{{today | date: 'dd mmm yyyy HH:mm:ss' }}   // 01 Jan 2024 00:00:00 

2. CurrencyPipe

Transform numbers to currency strings.

//template
{{1234.56 | currency }}                   //$1234.56 (default USD)
{{1234.56 | currency:'EUR' }}           // €1234.56
{{1234.56 | currency:'INR' }}           // ₹1234.56
{{1234.56 | currency:'GBP':'code' }}           // GBP1234.56
{{1234.56 | currency:'USD': 'symbol':'1.0-0' }} //$1235 (no decimal)

3. DecimalPipe

Formats numbers with decimal points.

//template
{{3.14159 | number }}                   //3.142 (default)
{{3.14159 | number:'1.0-0' }}           //3 (no decimal)
{{3.14159 | number:'3.1-5' }}           //003.14159
{{1234567.89 | number:'1.2-2' }}           // 1234567.89

4. PercentPipe

Transform numbers to percentage strings.

//tamplate 
{{0.25 |percent }}       //25%
{{0.5 |percent:'1.2-2' }}       //50.00%
{{1.5 |percent }}       //150%

5. UpperCasePipe / LowerCasePipe / TitleCasePipe

Transform text case.

//tamplate 
{{'hellow world' | uppercase }}   // HELLOW WORLD
{{'hellow world' | lowercase }}   // hellow world
{{'hellow world' | titlecase }}   // Hellow World

6. SlicePipe

creates a subset of array or strings.

// component
export class AppComponent {
    items =['Apple', 'Banana','Cherry', 'Date', 'Elderberry'];
    text = 'Hellow World';
}

// Template
{{item | slice: 1:3 }}     //Banana, Cherry
{{text | slice: 0:5}}      // Hello

7. JsonPipe

Converts object to JSON strings (useful for debugging).

//Component
export class AppComponent {
    user = {name: 'John', age: 30, city: 'New York'};
}

// Template
<pre>{{user | json }}</pre>
// Output : {"name": "John", "age": 30, "city": "New York"}

8. KeyValuePipe

Transforms Objects or Maps into an array of key-value pairs.

//Component
export class AppComponent {
    object = {name: 'John', age: 30, city: 'New York'};
}

//Template
<div *ngFor="let item of object | keyvalue">
{{item.key}}: {{item:value }}
</div>

Custom Pipes

Creating a Custom Pipe

Step 1: Generate pipe

ng generate pipe pipes/custom-pipe-name
# or
ng g p pipes/custom-pipe-name

Step 2: Implement pipe logic

Example 1: Reverse String Pipe

import { pipe, PipeTransform } from '@angular/core';

@pipe({
    name: 'reverse',
    standalsone: true
})
export class ReversePipe implements PipeTransform {
    transform(value: string): string {
        if(!value) return value;
        return value.split('').reverse().join('');
    }
}

Usage:

{{'Angular' | reverse }} <!-- Output: ralugnA -->

Example 2: Word count pipe

import { pipe, PipeTransform } from '@angular/core';

@pipe({
    name: 'wordCount',
    standalsone: true
})
export class WordCountPipe implements PipeTransfrom {
    transfrom(value: string): number {
        if(!value) return value;
        return value.trim().split(/\s+/).length;
    }
}

Usage:

{{'Hellow world from Angular' | wordCount }} <!-- Output: 4 -->

Example 3: Filter Pipe (with Parameters)

import { pipe, PipeTransform } from '@angular/core';

@pipe({
    name: 'filter',
    standalsone: true
})
export class FilterPipe implements PipeTransfrom {
    transform(items: any[], searchText: string, field?: string): any[] {
        if(!items || !searchText) return items;

        searchText = searchText.toLowerCase();

        return items.filter( item => {
            if(field){
                return item[field]?.toLowerCase().includes(searchText);
            }
            return JSON.stringify(item).toLowerCase()includes(searchText);
        });
    }
}

Usase:

<input [(ngModel)]="searchTerm" placeholder="search...">
<div *ngFor="let user of users | filter: searchTerm:'name'">
    {{user.name}}
</div>

Example 4: Truncate Pipe

import { pipe, PipeTransform } from '@angular/core';

@pipe({
    name: 'truncate',
    standalsone: true
})
export class TruncatePipe implements PipeTransform {
    transfrom(value: string, limit: number = 50, ellipsis: string ='...') : string {
        if(!value) return value;

        if(value.length<= limit){
            return value;
        }
        return value.substring(0, limit) + ellipsis;
    }
}

Usage:

{{longText | truncate: 100:'...'}}

Exaple:5 Time Ago Pipe

import { pipe, PipeTransform } from '@angular/core';

@pipe({
    name: 'timeAgo',
    standalsone: true
})
export class TimeAgoPipe implements PipeTransform {
    transform(value: Date | string): string {
        if(!value) return '';

        const date = value instanceof Date? value: new Date(value);
        const now = new Date();
        const seconds = Math.floor((now.getTime() - date.getTime())/1000);

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

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

Usage:

{{post.createAt | timeago }} <!-- Output: 2 hours ago -->

Pure VS Impure Pipes

Pure Pipes (Default)

Characteristics:

  • Called only when Angular detects a pure change to the input value
  • Pure changes: primitive values (String, Number, Boolean) or object refrences
  • More performant as they're called less frequently
  • Default behavior (pure: true)
@pipe({
    name: 'purePipe',
    pure: true // Default
})
export class PurePipe implements PipeTransform {
    transform(value: any): any {
        console.log('Pure pipe called');
        return value;
    }
}

Example:

// component 
export class AppComponent {
    items = ['Apple', 'Banana', 'Cherry'];

    addItem() {
        // This WON'T trigger pure pipe (same reference)
        this.items.push('Date');

        // This will trigger pure pipe (new reference)
        this.items = [...this.items, 'Date'];
    }
}

Impure Pipe

Characteristics:

  • Called on every change detection cycle
  • Detects changes within objects or arrays
  • Less performant but more responsive to data changes
  • Must explicitly set pure: false
@pipe({
    name: 'impureFilter',
    pure: false // Impure pipe
})
export class ImpureFilterPipe implements PipeTransform {
    transform(items: any[], searchText: string): any[]{
        console.log('Impure pipe called');
        if(!items || !searchText){
            return items;
        }
        return items.filter(item => item.toLowerCase().includes(searchText.toLowerCase())
        );
    }
}

When to use Impure Pipes:

  • Filtering/sorting dynamic arrays
  • Real-time data transformations
  • When you need to detect changes within object/arrays

Performance Consideration:

// ❌ Bad: Impure pipe for simple transformation
@pipe({ name: 'badPipe', pure: false})

//✅ Good: Pure pipe with immutable data
@pipe({ name: 'goodPipe', pure: true})
// Update data immutably: this.items = [...this.items, newItem]

Async Pipe

The Async Pipe is a spacial built-in pipe that subscribes to observables or Promises and returns the latest value.

Key Benfits:

  1. Atomatic Subscription Managment: Automatically subscribes and Unsbscribes
  2. Memory leak Prevention: Prevents memory leaks
  3. Cleaner code: No need for manual subscription in component

Observable Example:

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

@Component({
    selector: 'app-async-demo',
    template: `
    <h3?>Current Time: {{time$ | async | date: 'medium' }}</h3>
    <h3?>User Name: {{user$ | async | json }}</h3>
    `
})
export class AsyncDemoComponent {
    // Observable that emits every second
    time$: Observable<Date> = interval(1000).pipe(
        map(() => new Date())
    );

    // Observable from HTTP call
    user$: Observable<user> = this.http.get<User>('/api/User');

    constructor(private http: HttpClient){}
}

Promise Example:

// component
export class AppComponent {
    dataPromise: Promise<string>;

    constructor(){
        this.dataPromise = this.fatchData();
    }
    fatchData(): Promise<string> {
        return new Promise((resolve) => {
            setTimeout(() => resolve('Data loaded!'), 2000);
        });
    }
}

// Template
<div>{{dataPromise | async }}</div>

Multiple async Pipes (Same Observable):

//❌ Bad: Multiple subscriptions
<div>{{user$ | async }}</div>
<div>{{user$ | async }}</div>
<div>{{user$ | async }}</div>

//✅ Good Using as Keyword (Angular 13+)
<div *ngIf="user$ | async as user">
    <h3>{{user.name}}</h3>
    <p> {{user.email}}</p>
    <p> {{user.age}}</p>
</div>

//✅ Good Using ng-container
<ng-container *ngIf=="user$ | async as user">
    <h3>{{user.name}}</h3>
    <p> {{user.email}}</p>
</ng-container>

Error handling with async pipe

import { catchError, of } from 'rxjs';

export class AppComponent {
    data$ = this.http.get('/api/data').pipe(
        catchError(error => {
            console.error('Error:', error);
            return of(null); // Return default value
        })
    );
}

// Template with error handeling
<div *ngIf="data$ | async as data; else loading">
    {{data | json }}
</div>
<ng-tamplate #loading>Loading....</ng-template>

Pipe Chaining

Pipe can be chained together, with the output of one pipe becoming input of the next.

// component
export class AppComponent {
    birthday = new Date(1990, 5, 15);
    price = 1234.56;
    message= 'hellow world from Angular';
}

// Tamplate
{{birthday | date:'fullDate' | uppercase}}
//Output: FRIDAY, JUNE 15, 1990

{{price | currency:'USD' | lowercase }}
//Output: $1,234.56

{{message | slice:0:11 | titlecase }}
//Output: Hellow World

{{user$ | async | json }}
// First resolve observable, then convert to json

Complex Chaining Example:

// Component
export class AppComponent {
    product$ = this.http.get<Product[]>('/api/products');
}

// Tamplate
<div *ngFor="let product for products$ | async | slice:0:5">
    <h3> {{product.name | titlecase}} </h3>
    <p> {{product.discription | truncate:100 }}</p>
    <p> {{product.price | currency:'USD' }}</p>
    <p> {{product.createAt | date:'short' | uppercase }} </p>
</div>

Parameterized Pipes

Pipes can accept parameters to customize their behavior.

Single Paramenter:

{{ value | pipeName: parameter }}
{{'hello' | truncate:5}}

Multiple Parameters:

{{value | pipeName:param1:param2:param3 }}
{{today | date: 'dd/mm/yyyy': 'UTC':'+0530'}}

Custom Pipe with Multiple Paramenters:

import { Pipe, PipeTransform } from '@angular/core';

@pipe({
    name: 'highlight',
    standalone: true
})
export class HighlightPipe implements PipeTransform {
    transform(
        value: string,
        searchTerm: string,
        backGroundColor: string = 'yellow',
        textColor: string ='black'
    ): string {
        if(!value ||!searchTerm) return value;

        const regex = new RegExp(searchTerm, 'gi');
        return value.replace( regex, match => 
            `<span style="backgroun-color: ${backgroundColor}; color: ${textColor}">
            ${match}
            </span>`
        );
    }
}

usage:

<div [innerHtml]="text | highlight: 'Angular':'yellow':'red'"></div>

Bast Practices

1. Keep Pipe pure when possible

//✅ Good: Pure pipe with immutable data
@pipe({name: 'filter', pure: true});

// Update data immutable
this.items = [...this.items, newItem];

2. Avoid Complex logic in Pipes

//❌ Bad: Complex business logic
@pipe({ name: 'complexCalculation' })
export class ComplexPipe implements PipeTransform {
    trasfrom(value: number): number {
        // Multiple API calls, complex calculations
        return complexResult;
    }
}

// ✅ Good: Simple transfromation
@pipe({ name: 'formatCurrency' })
export class FormateCurrencyPipe implements PipeTransform {
    transform( value: number, currency: string='USD'): string {
        return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: currency
        }).fromate(value);
    }
}

3. USe Async Pipe for Observables

//❌ Bad: Manual subscription
export class AppComponent implements OnInit, OnDestroy {
    data: any;
    subscription: Subscription;

    ngOnInit(){
        this.subscription = this.service.getData().subscription(
            data => this.data = data
        );
    }

    ngDestroy(){
        this.subscription.unsubscribe();
    }
}

//✅ Good : Async pipe
export class AppComponent {
    data$ = this.service.getData();
}
// Tamplate: {{data$ | async }}

4. Minimize Async Pipe Duplication

//❌ Bad: Multiple subscriptions
<div> {{user$ | async }}</div>
<div> {{user$ | async }}</div>

//✅ Good: Single subscription with alias
<ng-container *ngIf="user$ | async as user">
    <h3>{{user.name}}</h3>
    <p> {{user.email}}</p>
    <p> {{user.age}}</p>
</ng-container>

5. USe Descriptive Pipe Names

//❌ Bad
@pipe({name: 'p1'})

//✅ Good 
@pipe({name: 'formatephoneNumber'})

6. Handle Null/Undefined Values

@Pipe({name: 'safePipe'})
export class SafePipe implements PipeTransform {
    transform( value: any): any {
        if(value === null || value === undefined){
            return ''; // or default value
        } 
        return value;
    }
}

7. **Consider Performance for Impure Pipes **

// USe impure pipe sparingly
@pipe({
    name: 'expensiveFilter',
    pure: false //Only when necessary
})
export class ExpensiveFilterPipe implements PipeTransform {
    transform(items: any[], filter: string): any[]{
        // This will run on every change detection
        return items.filter(item => item.includes(filter));
    }
}

Interview Question & Answer

Basic level Questions

Q1: What are pipes in Angular?

Answer: Pipes are simple functions used in template expressions to transform data for display. They take an input value and return a transformed output without changing the original data. Pipes are denoted by the pipe operator (|) in templates.

Example:

{{'hello' | uppercase }} //Output: HELLO
{{price | currency: 'USD'}} // Output: $99.99

Q2. Name some built-in pipes in Angular.

Angular Angular provides sevral built-in pipes:

  • DatePipe: Formates dates
  • CurrencyPipe: Formates currency value
  • DecimalPipe: fprmates number with decimals
  • PercentagePipe: formates percentage
  • UpperCasePipe/LowerCasePipe/TitleCasePipe: Taxt case transformation
  • SlicePipe: Extracts subset of arrays or string
  • JSONPipe: Converts object to JSON strings
  • AsyncPipe: Handle observable & Promises
  • KeyValuePipe: Transforms objects to key-value pairs

Q3: How do you create custom pipe in Angular?

Answer To create a custom pipe:

  1. Generate using CLI:
    ng g p pipes/my-custom-pipe
  1. Implement the PipeTrasform interface:
import { pipe, PipeTransform } from '@angular/core';

@pipe({
    name: 'myCustomPipe',
    standalone: true
})
export class MyCustomPipe implements PipeTransform {
    trasform(value: any, ...args: any[]): any{
        // Transformation Logic
        return TransformValue;
    }
}
  1. Use in Template:
{{someValue | myCustomPipe }}

Q4: What is difference between Pure and Impure Pipes?

Answer

Pure Pipe (Default):

  • Called only when input value or refrence changes
  • More performant
  • Default behaviour (pure: true)
  • Example: Changes to primitives or new object refrences

Impure Pipes:

  • Called on every change detection cycle
  • Less performent but detects changes with object/array
  • must explicitly set pure: false
  • Use when you need to detect internal changes in objects/arrays
// Pure pipe
@pipe({name: 'purePipe', pure: true})
// ImPure pipe
@pipe({name: 'impurePipe', pure: false})

Q5: What is Async pipe and why is it useful?

Answer: The asyncPipe subscribes to observables or Promised and returns the latest emitted value. It automatically handles subscription and unsubscription, preventing memory leaks.

Benefits:

  • Automatic subscription managment
  • Prevents memory leaks
  • Cleaner code ( no munal subscribe/unsubscribe)
  • Works with both Observable and Promises

Example:

//Component
data$ = this.http.get('/api/data');

//Template
<div>{{data$ | async | json }} </div>

Intermediate Level Questions

Q6: How can you pass parameter to a pipe?

Answer: Parameters are passed using colon (:) after the pipe name:

// Single parameter
{{ value | pipeName: parameter }}

//Mutiple parameter
{{value | pipeName: param1: param2: param3 }}

// Example 
{{today | date: 'dd/mm/yyyy'}}
{{ price | currency: 'EUR': 'symbol':'1.2-2 '}}

Custom pipe with parameters

@pipe({ name: 'power'})
export class PowerPipe implements PipeTransform {
    transform(value: number, exponent: number =1): number {
        return Math.pow(value, exponent);
    }
}

// Usage: {{2 | power: 3}} // Output: 8

Q7: Can you chain multiple pipes together? Provide an example.

Answer: Yes, pipes can be chained. The Output of one pipe become the input of the next pipe. They are executed from left to right.

Example:

//Component
birthdaty = new Date(1990, 5, 15);

//Tamplate
{{birthday | date:'full' | uppercase }}
//Output: FRIDAY, JUNE 15, 1990

{{user$ | async | json | slice:0:100 }}
// First resolve observable, converts to json , then slices

Q8: What happens if you use multiple async pipes with the same observable?

Answer: Each async pipe cretes a seperate subscription to the observable, which can lead to:

  • Multiple Http requests (if it's an HTTP observables)
  • Performance issues
  • Inconsistent data

Solution 1: Use as keyword

<ng-container *ngIf="user$ | async as user">
    <div>{{user.name}}</div>
    <div>{{user.email}}</div>
</ng-container>

Solution 2: Use shareReplay operator

user$ = this.http.get('/api/user').pipe(shareReplay(1));

Q9: How would you implement a filter pipe? Why might it be problematic?

Answer:

Implementation:

@pipe({
    name:'filter',
    pure: false // Must be impure to detect array changes
})
export class FilterPipe implements PipeTransform {
    transform(items: any[], searchText: string): any[]{
        if(!items || !searchText){
            return items;
        }
        return items.filter(item =>
            item.toLowerCase().includes(searchText.tolowerCase())
        );
    }
}

Problems:

  1. Performance: - Impure pipes run on every change detection cycle
  2. Frequent Call: - Can be called hundreds of items per second
  3. Angular Bast Practice: Angular discourage filter in templates

Better Alternative:

// In Component
filteredItems : any[] = [];

filterItems(searchText: string){
    this.filteredItems = this.items.filter(items => 
        item.tolowerCase().includes(searchText.tolowerCase())
    );
}

Q10: Explain the transform menthod signature in PipeTransforma interface.

Answer: The transform method is the core of any pipes:

transform(value: any, ...args: any[]): any

Parameters: value:: The input value to transform (required) ...arg: Rest parameter for additional arguments (optional)

Example:

@pipe({ name: 'formatName'})
export class FormateNamePipe implements PipeTransform {
    transfrom ( 
        value: string, // required input
        formate?: 'upper'|'lower' | 'title', //optional param1 
        prefix? : string //optional param2
    ): string {
        if(!value) return value;

        let result = value;
        if(format === 'upper') result = value.toUpperCase();
        else if(format === 'lower') result = value.tolowerCase();
        else if(format ==='title') result = value.charAt(0).toupperCase()+value.slice(1);

        return prefix? `${prefix} ${result}`: result;
    }
}
// Usage
{{name | formatName: 'upper':'Mr.' }}

Advance Level Questions

Q11: How do pure pipe dectect changes? Explain the mechanism.

Answer: Pure pipes use Angular's change detection to determaine if the input has changed:

For Primitives (string, Number, Boolean):

  • Use strict equality (===)
  • New value trigers the pipe

For Object and Arrays:

  • Compares object refrences, not content
  • Only triggers when reference changes (new object/array)

Example:

export class AppComponent {
    items = [1,2,3];

    //❌ Won't trigger pure pipe (same reference)
    addItem(){
        this.items.push(4);
    }
    //✅ will trigger pure pipe (new refrence)
    addItemImmutably(){
        this.items = [...this.items, 4];
    }
}

Change Detection Flow

1. Angualr runs change detection
2. Checks if input reference changed (for objects/arrays)
3. If changed -> calls pipe's transform method 
4. if unchanged -> returns cached result

Q12: what are the performance implementations of using impure pipes?

Answer: Performance Impact:

  1. Called Frequently: Runs on every change detection cycle (potentially hundreds of itmes per second)
  2. CPU Intensive: Can cause performance degrafation with complex transformations
  3. UI Lag: My cause jank or stuttering in the UI

Example Scenario:

@pipe({ name: 'expensiveFilter', pure: false})
expoer class ExpensiveFilter implements PipeTransforms {
    transform(items: any[], filter: string): any[]{
        console.log('Pipe Colled') // will log constantly
        // Expensive operation runs repeatedly
        return items.filter(item =>
            this.complexCalculation(item, filter)
        );
    }
}

Optimization Strategies:

  1. Use Pure pipes: Update data immutably
this.items = [...this.items, newItem];
  1. Move logic to component:
// In Component
filterItem$ = combineLatest([
    this.items$,
    this.searchTerm$
]).pipe(
    map(([items, term]) => items.filter(item => item.includes(term)))
);
  1. Memoization: Cache results
private cache = new Map();

transform(item: any[], filter: string): any[]{
    const key = JSON.stringify({items, filter});
    if(this.cache.has(key)){
        return this.cache.get(key);
    }
    const result = items.filter(item => item.include(filter));
    this.cache.set(key, result);
    return result;
}

Q13: How would you implement a custom async pipe?

Answer:

important { Pipe, PipeTransform, ChangeDecetorRef, OnDestroy } from '@angular/core';
import {Observable, subscription } from 'rxjs';

@pipe({
    name: 'customAsync',
    pure:false,
    stanalone: true
})
export class CustomAsyncPipe implements PipeTransform, OnDestroy {
    private subscription: Subscription | null = null;
    private latestValue: any = null;
    private observable: Observable<any> |null = null;

    constructor( private cdr: ChangeDecetorRef){}

    transform(observable: Observable<any>): any {
        if(!this.observable){
            // First time or different observable
            this.observable = observable;
            this.subscribe(observable);
        } else if(observable !==this.observable){
            //Observable changed
            this.dispose();
            this.observable=observable;
            this.subscribe(observable);
        }
        return this.latestValue;
    }

    private subscribe(observable: Observable<any>){
        this.subscription = observable.subscribe({
            next: (value) => {
                this.latestValue = value;
                this.cdr.markForCheck(); // Trigger change detection
            },
            error: (error) => {
                console.log('Custom async pipe error: ', error);
            }
        });
    }

    private dispose(){
        if(subscription) {
            this.subscription.unsubscribe();
            this.subscription = null;
        }
        this.latestValue = null;
    }
    ngDestroy(){ 
        this.dispose();
    }
}

Key Concepts:

  • Must be impure (pure: false) to detect observable emmissions
  • Needs ChangeDetectorRef to trigger view updates
  • Implements OnDestroy for Cleanup
  • Handles observable changes

Q14: How would you test a custom pipe?

answer:


Q15: What is the difference between using a pipe and a method call in a template?

Answer:

Pipe

{{ value | myPipe }}

Method call:

{{ transformValue(value) }}

Key Difference:

AspectPipeMethod call
CachingPure Pipe cache resultsCalled every change detection
PerformanceBatter (for pure pipe)worse (called repeatedly)
ReusabilityHighly reusableComponent-specific
TestingEasy to test in isolationTested with component
ReadabilityMore declarativemore imerative
Change DetectionOptimizedAlways called

Performance Example:

// Component
export class AppComponent {
    counter = 0; 

    // This will be called on EVERY change detection
    transform(value: string): string {
        console.log('Method called: ', ++this.counter);
        return valure.toUpperCase();
    }
}

// Template 
<div>{{transform(name)}}</div>
<!--Console: method called: 1,2,3,4, ... (continuesly)-->

//with Pipe
<div>{{name | upperCase}}</div>
<!--Called only when 'name' changes-->

Best Practice:

  • ✅ Use Pipe for data trasformation in template
  • ❌ Avoid method calls in templates for transformations
  • ✅ Use component menthods only for event handlers

Q16: How would you implement a memoization strategy for an expensive pipe?

Answer:


Q17: How do pipes work with OnPush change detection strategy?

Answer:

OnPush Change Detection: Component with OnPush strategy only check for changes when:

  1. Input reference change
  2. Event is triggered
  3. Observable emits (via async pipe)
  4. Manual change detection

Pipe with onPush:

@component({
    selector: 'app-user',
    template: `
    <div>{{user.name | uppercase }}</div>
    <div?> {{userData$ | async | json }}</div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
    @input(): user: User; // Pure pipe works when reference changes
    userData$ = this.service.getUser(); // async pipe triggers detection
}

Key Points:

  1. Pure pipes + OnPush = perfect match
// perent component
updateUser(){
    //✅ Creates new reference, triggers onPush
    this.user = {...this.user, name: 'New Name'};
}

// Child with OnPush
{{use.name | uppercase }} //will update
  1. ImPure pipes + OnPush =Issues
@component ({
    template: `{{items | impureFilter: searchTerm }}`,
    changeDetection: ChangeDetectionStrategy.OnPush  
})
export class MyComponent {
    items = [1,2,3];
    searchTerm = '';

    //❌ won't trigger OnPush even though pipe is impure
    updateSearch(term: string){
        this.searchTerm = term;
    }
}
  1. Async Pipe + OnPush = Automatic Updates
@component({
    template: `{{data$ | async}}`,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
    data$ = this.service.getData();
    //Async pipe automatically calls markForCheck()
}

Solution for Impure Pipe:

import { ChangeDectorRef } from '@angular/core';

@component({
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
    constructor( private cdr: ChangeDectorRef){}

    updateData(){
        // Manually trigger change detection
        this.cdr.markForChange();
    }
}

Q18: Explain lifecycle of a pipe in Angular.

Angular:

Pipe Lifecycle:

@pipe({name: 'lifecycle'})
export class LifecyclePipe implements PipeTransform, OnDestroy {
    private callCount =0;

    constructor(){
        console.log('1. Pipe constructed');
    }
    transform(value: any): any {
         console.log(`2. Transform colled (${++this.callCount} times)`);
         return value;
    }
    ngOnDestroy(){
         console.log('3. Pipe destroyed');
    }
}

Lifecylcle stages:

  1. Construction
    • Pipe intance created when first used
    • One intance per pipe usege in template
    • Shared accross change detection cycle
  2. Tramsform Method calls:
    • Pure Pipe: Called when input/reference changes
    • Impure Pipe: Called on every change detection
    • Can be called multiple times per detection cycle
  3. Destruction
    • When component is destroyed
    • Implements OnDestroy for cleanup
    • Unsubcribe observables, clear caches

Example with multiple ueases:

@component({
    template: `
        <div?> {{value1 | lifecycle }} </div> <!--New instance-->
        <div?> {{value2 | lifecycle }} </div> <!--New instance-->
        <div?> {{value1 | lifecycle }} </div> <!--same a first-->
    `
})
export class AppComponent{ 
    value1 = 'Hellow';
    value2 = 'World';
}
// Creates 2 pipe instances (one for eanch uniqe usage location)

Async Pipe Lifecycle:

@pipe({name: 'customAsync', pure: false})
export class CustomeAsyncPipe implements PipeTransform, OnDestroy {
    transform(observable: Observable<any>): any {
        // Subscribe on first call
        if(!this.subscription){
            this.subscription = observable.subscribe(/*....*/);
        }
        return this.latestValue;
    }
}
ngOnDestroy(){
    //Cleanup on Destroy
    if(this.subscription){
        this,subscription.unsubscribe();
    }
}

Q19: How would you handle errors in a pipe?

Answer:

1. Try-Catch Block

@pipe({ name: 'safeTransform'})
export class SafeTransformPipe implements PipeTransform {
    transform(value: any): any {
        try{
            // Risky transformation
            return JSON.parse(value);
        }catch(error){
            console.log('Pipe error:', error);
            return null; // Return default value
        }
    }
}

2. Null/Undefined Checks:

@pipe({ name: 'safeName' })
export class SafeNamePipe implements PipeTransfrom {
    transform(value: any ): any {
        if(value === null || value === undefined){
            return 'N/A';
        }
        if(typeof value !== 'string'){
            console.warn('Expected string, got:', typeof value);
            return String(value);
        }
        return value.toUpperCase();
    }
}

3. Error service Integration:

@pipe({ name: 'withErrorHandling' })
export class withErrorHandlingPipe implements PipeTransform {
    constructor(private errorService: ErrorService){}

    transform(value: any, fallback: any = null): any {
        try{
            // Complex transformation
            return this.complexTransform(value);
        }catch (error) {
            // log to error service
            this.errorService.logError({
                message: 'pipe transform failed',
                error,
                value
            });
            return fallback;
        }
    }
    private complexTransform(value: any): any {
        // Your transform logic
        return value;
    }
}

4. Async Pipe Error Handling

@component ({
    template:
    `
    <div *ngIf=*data$ | async as data; else loading>
    {{data | json}}
    </div>
    <ng-template #loading>
        <div *ngIf= "error$ | async as error">
            Error: {{error}}
        </div>
        <div *ngIf="!(error$ | async )">
            Loading...
        </div>
    </ng-template>
    `
})
export class AppComponent{
    error$ = new Subject<string>();

    data$ = this.http.get('/api/data').pipe(
        catchError(error => {
            this.error$.next(error.massage);
            return of(null);
        })
    );
}

5. Custom safe pipe Decotator:

export function SafePipe(fallbackValue: any = null){
    return function (
        target: any,
        propertyKey: string,
        descriptor:PropertyDescriptor
    ){
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]){
            try{
                return originalMethod.apply(this, args);
            }catch(error){
                Console.log(`Error in pipe ${target.constructor.name}: `, error);
                return fallback;
            }
        };
        return descriptor;
    };
}

// Usage 
@Pipe ({name: 'protected'})
export class ProtectedPipe implements PipeTransform {
    @SafePipe ('Error occured')
    transform(value: any): any {
        // Risky operation
        return value.dangerousMethod();
    }
}

Q20: what are some real-world use casees for custom pipes? Provide examples.

Answer:

1. Phone Number formating:

@pipe({name: 'phoneNumber' })
export class PhoneNumberPipe implements PipeTransform {
    transform(value: string, country: string: ='US'): String {
        if(!value) return value;

        const cleaned = value.replace(/\D/g, '');
        
        if(country ==='US' && Cleaned.length ===10){
            return `(${cleaned.slice(0,3)}) ${cleaned.slice(3,6)}-${cleand.slice(6)}`;
        }
        return value;
    }
}

// Usege: {{'1234567890' | phoneNumber }}
// Output: (123) 456-7890

File size formating:

@Pipe({name: 'fileSize' })
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)) + ''+size[i]
        );
    }
}
// Usege: {{1536 | filesize}}
// Output : 1.5 KB

3. Relative Time (Time Ago)

@pipe({name: 'timeAgo'})
export class TimeAgoPipe implements PipeTransform {
    transform(value: Date | string): string {
        const data = value instanceof Date? value: new Date(value);
        const seconds = Math.floor((+new Date() - +date) / 1000);

        const intervals: { [key: string]: number} = {
            year: 31536000,
            month: 2592000,
            week: 86400,
            day: 86400,
            hour: 3600,
            minute: 60,
            second: 1
        };

        for(const [name, secondInterval] or Object.entries(intervals)) {
            const interval = Math.floor(seconds / secondsInInterval);
            if(interval>=1){
                return interval ===1
                ? `${interval} ${name} ago`
                : `${interval} ${name}s ago`;
            }
        }
        return 'just now';
    }
}
// Usege: {{post.createdAt | timeAgo }}
// Outpput: 2 hours ago

4. Safe HTML

import{ DomSanitizer, safeHtml } from '@angular/platform-browser';

@pipe({ name: 'safeHtml'})
export class SafeHtmlPipe implements PipeTransform {
    constructor(private sanitizer: DomSanitizer){}
    
    transform(value: string): SafeHTML {
        return this.sanitizer.bypassSecurityTrustHtml(value);
    }
}

// Usage: <div [innerHtml]= "htmlContent | safeHtml"></div>

5. Search Highlight

@pipe({ name: 'highlight'})
export class HighlightPipe implements PipeTransform {
    trasform(value: string, searchTerm: string): string {
        if(!searchTerm) return value;

        const regex = new Regex(searchTerm, 'gi');
        return value.replace(
            regex, 
            match=> `<mark>${match}</mark>`
        );
    }
}
// Usage: <div [innerHtml]="text | highlight: searchTerm"></div>

6. Pluralize:

@pipe({ name: 'pluralize'})
export class PluralizePipe implements PipeTransform {
    transform(count: number, singular: string, plural?: string): string {
        const pluralForm = plural || singular +'s'
        return count ===1 ? `${count} ${singular}`: `${count}: ${pluralForm}`;
    }
}

// Usage: {{itemCount || pluralize: 'item'}}
// Output: 1 item or 5 items

7. Mask Sensitive Data:

@pipe({ name: 'mask'})
export class MaskPipe implements PipeTransform {
    transform( value: string, visibleChars: number =4, maskChar: string = '*'): string {
        if(!value) return value;

        const visible = value.slice(-visibleChars);
        const masked = maskChar.repeat(value.length- visibleChars);
        return masked + visible;
    }
}

// Usage: {{createdAt || mask:4}}
// Output: ***************1234

Summary

Key Takeways:

  1. Pipes transform data in tamplate without modifying the source
  2. Pure pipes are more performent and should be preferred
  3. Async pipe handles subscriptions automatically
  4. Custom pipe sould be simple, focused, and resuable
  5. Avoid filtering/sorting in templates, do it in components
  6. Test Pipe in isolation for batter maintainability
  7. Memoization can optimize expencsive pipes
  8. Error handiling prevents pipe failures form breaking UI

Common mistake to Avoid:

  • ❌ Using impure pipe for expensive operation
  • ❌ Multiple async pipes on the same observable
  • ❌ Complex business logic in pipe
  • ❌ Mutating input values in pipe
  • ❌ Not handling null/ undefined values
  • ❌ Filtering large arrays with impure pipes

Best practices

  • ✅ Keep pipes pure when possible
  • ✅ Use asyc pipe for observables
  • ✅ Handle error gracefully
  • ✅ Write unit test for custom pipe -✅ Use descriptive pipe name
  • ✅ Document pipe parameter -✅ Consiter performance implications

Comments

Popular posts from this blog