DEV Community

Cover image for Angular Directives and Pipes: Complete Guide | Custom Directives, Pipes & Transformations
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

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

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Chaining Pipes

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

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)