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

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="isAuthenticated"> Welcome, user!
*ngIf="isLoading; else content"> Loading...
#content> Content loaded
  • *ngFor="let business of businesses; let i = index; trackBy: trackByBusinessId"> {{ i + 1 }}. {{ business.name }}
  • [ngSwitch]="userRole">

    *ngSwitchCase="'admin'">Admin Panel

    *ngSwitchCase="'user'">User Dashboard

    *ngSwitchCase="'manager'">Manager View

    *ngSwitchDefault>Guest View

    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:

    
    {{ currentDate | date:'short' }}
    {{ currentDate | date:'fullDate' }}
    {{ currentDate | date:'MM/dd/yyyy' }}
    {{ currentDate | date:'medium' }}
    
    
    {{ price | currency:'USD':'symbol':'1.2-2' }}
    {{ price | currency:'EUR':'symbol':'1.2-2' }}
    {{ price | currency:'USD':'$' }}
    
    
    {{ text | uppercase }}
    {{ text | lowercase }}
    {{ text | titlecase }}
    
    
    {{ number | number:'1.2-2' }}
    {{ number | number:'3.1-5' }}
    
    
    {{ ratio | percent:'1.2-2' }}
    {{ ratio | percent }}
    
    
    
    {{ data | json }}


    {{ items | slice:0:5 }}
    {{ text | slice:0:20 }}


    *ngFor=“let item of object | keyvalue”>
    {{ item.key }}: {{ item.value }}

    Enter fullscreen mode

    Exit fullscreen mode



    Chaining Pipes

    {{ currentDate | date:'fullDate' | uppercase }}
    {{ price | currency:'USD' | slice:1 }}
    
    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.



    Source link

    Leave a Reply

    Your email address will not be published. Required fields are marked *