Join our FREE personalized newsletter for news, trends, and insights that matter to everyone in America

Newsletter
New

Angular Directives And Pipes: Complete Guide | Custom Directives, Pipes & Transformations

Card image cap

When I first started with Angular, I thought directives and pipes were just nice-to-have features. Then I found myself writing the same conditional rendering logic in multiple components, and the same data transformation code in multiple templates. That's when I realized that custom directives and pipes are powerful tools for creating reusable, maintainable code.

Directives extend HTML with custom behavior. Structural directives (like *ngIf and *ngFor) change the DOM structure, while attribute directives (like [ngClass] and [ngStyle]) modify element appearance or behavior. Pipes transform data for display in templates—formatting dates, currencies, and text. Both are essential for building clean, maintainable Angular templates.

???? Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.

What are Angular Directives and Pipes?

Angular Directives provide:

  • Structural Directives - Change DOM structure (*ngIf, *ngFor, *ngSwitch)
  • Attribute Directives - Modify element appearance/behavior ([ngClass], [ngStyle])
  • Custom Directives - Reusable behavior for specific use cases

Angular Pipes provide:

  • Built-in Pipes - Date, currency, uppercase, lowercase, json, etc.
  • Custom Pipes - Data transformations specific to your application
  • Async Pipe - Handle Observables and Promises automatically

Built-in Structural Directives

Angular provides powerful structural directives out of the box:

<!-- *ngIf - Conditional rendering -->  
<div *ngIf="isAuthenticated">  
  <p>Welcome, user!</p>  
</div>  
  
<!-- *ngIf with else -->  
<div *ngIf="isLoading; else content">  
  <p>Loading...</p>  
</div>  
<ng-template #content>  
  <p>Content loaded</p>  
</ng-template>  
  
<!-- *ngFor - Loop through arrays -->  
<ul>  
  <li *ngFor="let business of businesses; let i = index; trackBy: trackByBusinessId">  
    {{ i + 1 }}. {{ business.name }}  
  </li>  
</ul>  
  
<!-- *ngSwitch - Multiple conditions -->  
<div [ngSwitch]="userRole">  
  <p *ngSwitchCase="'admin'">Admin Panel</p>  
  <p *ngSwitchCase="'user'">User Dashboard</p>  
  <p *ngSwitchCase="'manager'">Manager View</p>  
  <p *ngSwitchDefault>Guest View</p>  
</div>  

TrackBy Function for Performance

export class BusinessListComponent {  
  businesses: Business[];  
  
  trackByBusinessId(index: number, business: Business): number {  
    return business.id;  
  }  
}  
  
// Template  
<div *ngFor="let business of businesses; trackBy: trackByBusinessId">  
  {{ business.name }}  
</div>  

Custom Structural Directive

Create custom structural directives for reusable conditional rendering:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';  
import { AuthService } from '../auth/auth.service';  
  
@Directive({  
  selector: '[appHasPermission]'  
})  
export class HasPermissionDirective {  
  private hasView = false;  
  
  constructor(  
    private templateRef: TemplateRef<any>,  
    private viewContainer: ViewContainerRef,  
    private authService: AuthService  
  ) {}  
  
  @Input() set appHasPermission(permission: string) {  
    const hasPermission = this.authService.hasPermission(permission);  
  
    if (hasPermission && !this.hasView) {  
      this.viewContainer.createEmbeddedView(this.templateRef);  
      this.hasView = true;  
    } else if (!hasPermission && this.hasView) {  
      this.viewContainer.clear();  
      this.hasView = false;  
    }  
  }  
}  
  
// Usage  
<div *appHasPermission="'admin'">  
  Admin content  
</div>  
  
<div *appHasPermission="'business:read'">  
  Business details  
</div>  

Advanced Structural Directive

@Directive({  
  selector: '[appUnless]'  
})  
export class UnlessDirective {  
  private hasView = false;  
  
  constructor(  
    private templateRef: TemplateRef<any>,  
    private viewContainer: ViewContainerRef  
  ) {}  
  
  @Input() set appUnless(condition: boolean) {  
    if (!condition && !this.hasView) {  
      this.viewContainer.createEmbeddedView(this.templateRef);  
      this.hasView = true;  
    } else if (condition && this.hasView) {  
      this.viewContainer.clear();  
      this.hasView = false;  
    }  
  }  
}  
  
// Usage (opposite of *ngIf)  
<div *appUnless="isHidden">  
  This content shows when isHidden is false  
</div>  

Custom Attribute Directive

Create custom attribute directives for reusable behavior:

import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';  
  
@Directive({  
  selector: '[appHighlight]'  
})  
export class HighlightDirective {  
  @Input() appHighlight = 'yellow';  
  
  constructor(  
    private el: ElementRef,  
    private renderer: Renderer2  
  ) {}  
  
  @HostListener('mouseenter') onMouseEnter() {  
    this.highlight(this.appHighlight);  
  }  
  
  @HostListener('mouseleave') onMouseLeave() {  
    this.highlight(null);  
  }  
  
  private highlight(color: string | null) {  
    this.renderer.setStyle(this.el.nativeElement, 'background-color', color);  
  }  
}  
  
// Usage  
<p appHighlight="lightblue">Hover over me</p>  

Directive with @HostBinding

@Directive({  
  selector: '[appFocus]'  
})  
export class FocusDirective {  
  @Input() appFocus: boolean;  
  
  @HostBinding('class.focused') get isFocused() {  
    return this.appFocus;  
  }  
  
  @HostListener('click') onClick() {  
    this.appFocus = true;  
  }  
}  

Directive with Multiple Inputs

@Directive({  
  selector: '[appTooltip]'  
})  
export class TooltipDirective {  
  @Input() appTooltip: string;  
  @Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';  
  
  @HostListener('mouseenter') onMouseEnter() {  
    this.showTooltip();  
  }  
  
  @HostListener('mouseleave') onMouseLeave() {  
    this.hideTooltip();  
  }  
  
  private showTooltip(): void {  
    // Tooltip logic  
  }  
  
  private hideTooltip(): void {  
    // Hide tooltip logic  
  }  
}  
  
// Usage  
<span appTooltip="Help text" tooltipPosition="bottom">Hover me</span>  

Built-in Pipes

Angular provides many built-in pipes for common transformations:

<!-- Date Pipe -->  
<p>{{ currentDate | date:'short' }}</p>  
<p>{{ currentDate | date:'fullDate' }}</p>  
<p>{{ currentDate | date:'MM/dd/yyyy' }}</p>  
<p>{{ currentDate | date:'medium' }}</p>  
  
<!-- Currency Pipe -->  
<p>{{ price | currency:'USD':'symbol':'1.2-2' }}</p>  
<p>{{ price | currency:'EUR':'symbol':'1.2-2' }}</p>  
<p>{{ price | currency:'USD':'$' }}</p>  
  
<!-- Uppercase/Lowercase -->  
<p>{{ text | uppercase }}</p>  
<p>{{ text | lowercase }}</p>  
<p>{{ text | titlecase }}</p>  
  
<!-- Decimal Pipe -->  
<p>{{ number | number:'1.2-2' }}</p>  
<p>{{ number | number:'3.1-5' }}</p>  
  
<!-- Percent Pipe -->  
<p>{{ ratio | percent:'1.2-2' }}</p>  
<p>{{ ratio | percent }}</p>  
  
<!-- JSON Pipe (for debugging) -->  
<pre>{{ data | json }}</pre>  
  
<!-- Slice Pipe -->  
<p>{{ items | slice:0:5 }}</p>  
<p>{{ text | slice:0:20 }}</p>  
  
<!-- KeyValue Pipe -->  
<div *ngFor="let item of object | keyvalue">  
  {{ item.key }}: {{ item.value }}  
</div>  

Chaining Pipes

<p>{{ currentDate | date:'fullDate' | uppercase }}</p>  
<p>{{ price | currency:'USD' | slice:1 }}</p>  

Custom Pipe

Create custom pipes for specific data transformations:

import { Pipe, PipeTransform } from '@angular/core';  
  
@Pipe({  
  name: 'truncate',  
  pure: true  
})  
export class TruncatePipe implements PipeTransform {  
  transform(value: string, limit: number = 50, trail: string = '...'): string {  
    if (!value) return '';  
    if (value.length <= limit) return value;  
    return value.substring(0, limit) + trail;  
  }  
}  
  
// Usage  
<p>{{ longText | truncate:100 }}</p>  
<p>{{ description | truncate:50:'...' }}</p>  

Filter Pipe (Impure)

@Pipe({  
  name: 'filter',  
  pure: false // Impure pipe - runs on every change detection  
})  
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())  
    );  
  }  
}  
  
// Usage  
<div *ngFor="let item of items | filter:searchTerm:'name'">  
  {{ item.name }}  
</div>  

Pure vs Impure Pipes

Pure Pipes (default):

  • Only run when input reference changes
  • Better performance
  • Use for simple transformations

Impure Pipes:

  • Run on every change detection cycle
  • Use when you need to detect changes in nested objects/arrays
  • Can impact performance
// Pure pipe (default)  
@Pipe({  
  name: 'truncate',  
  pure: true // Only runs when input changes  
})  
  
// Impure pipe  
@Pipe({  
  name: 'filter',  
  pure: false // Runs on every change detection  
})  

Custom Currency Pipe

@Pipe({  
  name: 'customCurrency',  
  pure: true  
})  
export class CustomCurrencyPipe implements PipeTransform {  
  transform(value: number, currency: string = 'USD'): string {  
    if (value == null) return '';  
  
    const formatter = new Intl.NumberFormat('en-US', {  
      style: 'currency',  
      currency: currency  
    });  
  
    return formatter.format(value);  
  }  
}  
  
// Usage  
<p>{{ price | customCurrency:'EUR' }}</p>  

Async Pipe

Handle asynchronous data with the async pipe:

// Component  
export class BusinessListComponent {  
  businesses$: Observable<any[]>;  
  
  constructor(private businessService: BusinessService) {  
    this.businesses$ = this.businessService.GetBusinesses({});  
  }  
}  
  
// Template  
<div *ngIf="businesses$ | async as businesses">  
  <div *ngFor="let business of businesses">  
    {{ business.name }}  
  </div>  
</div>  
  
// With loading state  
<ng-container *ngIf="businesses$ | async as businesses; else loading">  
  <div *ngFor="let business of businesses">  
    {{ business.name }}  
  </div>  
</ng-container>  
<ng-template #loading>  
  <p>Loading...</p>  
</ng-template>  
  
// With error handling  
<ng-container *ngIf="businesses$ | async as businesses; else loading">  
  <div *ngFor="let business of businesses">  
    {{ business.name }}  
  </div>  
</ng-container>  
<ng-template #loading>  
  <p>Loading businesses...</p>  
</ng-template>  

Multiple Async Pipes

export class DashboardComponent {  
  businesses$: Observable<Business[]>;  
  categories$: Observable<Category[]>;  
  
  constructor(  
    private businessService: BusinessService,  
    private categoryService: CategoryService  
  ) {  
    this.businesses$ = this.businessService.GetBusinesses({});  
    this.categories$ = this.categoryService.GetCategories();  
  }  
}  
  
// Template  
<ng-container *ngIf="businesses$ | async as businesses">  
  <ng-container *ngIf="categories$ | async as categories">  
    <div *ngFor="let business of businesses">  
      <p>{{ business.name }}</p>  
      <p>Category: {{ getCategoryName(business.categoryId, categories) }}</p>  
    </div>  
  </ng-container>  
</ng-container>  

Best Practices

  1. Use structural directives for conditional rendering - *ngIf, *ngFor, *ngSwitch
  2. Create custom directives for reusable DOM manipulation - Avoid code duplication
  3. Use pipes for data transformation - Not for business logic
  4. Keep pipes pure when possible - Better performance
  5. Use async pipe - Automatically handles Observables and Promises
  6. Avoid complex logic in templates - Move to pipes or components
  7. Use trackBy function with *ngFor - Better performance with large lists
  8. Chain pipes when needed - {{ value | pipe1 | pipe2 }}
  9. Document custom directives and pipes - Clear usage instructions
  10. Test custom directives and pipes independently - Unit test them separately

Performance Tips

// ✅ Good - Pure pipe  
@Pipe({ name: 'truncate', pure: true })  
  
// ❌ Avoid - Impure pipe unless necessary  
@Pipe({ name: 'filter', pure: false })  
  
// ✅ Good - TrackBy function  
*ngFor="let item of items; trackBy: trackById"  
  
// ❌ Avoid - No trackBy  
*ngFor="let item of items"  

Common Patterns

Permission-Based Directive

@Directive({  
  selector: '[appRequirePermission]'  
})  
export class RequirePermissionDirective {  
  @Input() appRequirePermission: string;  
  
  constructor(  
    private templateRef: TemplateRef<any>,  
    private viewContainer: ViewContainerRef,  
    private authService: AuthService  
  ) {}  
  
  ngOnInit(): void {  
    if (this.authService.hasPermission(this.appRequirePermission)) {  
      this.viewContainer.createEmbeddedView(this.templateRef);  
    } else {  
      this.viewContainer.clear();  
    }  
  }  
}  

Format Phone Number Pipe

@Pipe({  
  name: 'phone',  
  pure: true  
})  
export class PhonePipe implements PipeTransform {  
  transform(value: string): string {  
    if (!value) return '';  
  
    const cleaned = value.replace(/\D/g, '');  
    if (cleaned.length === 10) {  
      return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;  
    }  
    return value;  
  }  
}  
  
// Usage  
<p>{{ phoneNumber | phone }}</p>  

Resources and Further Reading

Conclusion

Angular Directives and Pipes are powerful features that extend HTML capabilities and transform data for display. By understanding built-in directives and pipes, and creating custom ones, you can build more maintainable and reusable Angular applications.

Key Takeaways:

  • Structural directives - Change DOM structure (*ngIf, *ngFor, *ngSwitch)
  • Attribute directives - Modify element appearance/behavior
  • Custom directives - Reusable behavior for specific use cases
  • Built-in pipes - Date, currency, uppercase, lowercase, json, etc.
  • Custom pipes - Data transformations specific to your application
  • Async pipe - Handle Observables and Promises automatically
  • Pure vs Impure - Performance considerations for pipes
  • TrackBy function - Better performance with *ngFor

Whether you're building a simple data display or a complex interactive application, Angular Directives and Pipes provide the foundation you need. They handle DOM manipulation and data transformation while keeping your templates clean and maintainable.

What's your experience with Angular Directives and Pipes? Share your tips and tricks in the comments below! ????

???? Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.

If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.