Angular
09 / 10

Directives & Pipes

Angular Directives & Pipes

Directives and pipes are powerful features in Angular. Directives add behavior to elements, while pipes transform data in templates.

Built-in Directives

Structural Directives

<!-- *ngIf - conditional rendering -->
<div *ngIf="isVisible">Visible content</div>
<div *ngIf="user; else loading">Hello, {{ user.name }}</div>
<ng-template #loading>Loading...</ng-template>

<!-- *ngIf with as for variable assignment -->
<div *ngIf="user$ | async as user">
  {{ user.name }}
</div>

<!-- *ngFor - loop over collections -->
<ul>
  <li *ngFor="let item of items; let i = index; let first = first; let last = last; let even = even">
    {{ i + 1 }}. {{ item.name }}
    <span *ngIf="first">(First)</span>
    <span *ngIf="last">(Last)</span>
    <span *ngIf="even">(Even)</span>
  </li>
</ul>

<!-- *ngFor with trackBy for performance -->
<div *ngFor="let item of items; trackBy: trackByFn">
  {{ item.name }}
</div>

<!-- *ngSwitch - switch statement -->
<div [ngSwitch]="status">
  <p *ngSwitchCase="'loading'">Loading...</p>
  <p *ngSwitchCase="'success'">Success!</p>
  <p *ngSwitchCase="'error'">Error occurred</p>
  <p *ngSwitchDefault>Unknown status</p>
</div>

Attribute Directives

<!-- ngClass - dynamic classes -->
<div [ngClass]="'active'">Single class</div>
<div [ngClass]="['class1', 'class2']">Multiple classes</div>
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">Conditional classes</div>

<!-- ngStyle - dynamic styles -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">Styled text</div>
<div [ngStyle]="styleObject">Object styles</div>

<!-- ngModel - two-way binding -->
<input [(ngModel)]="name" />
<p>Hello, {{ name }}</p>

Custom Attribute Directives

import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';

// Highlight directive
@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input() appHighlight = 'yellow';
  @Input() defaultColor = 'transparent';
  
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) { }
  
  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight);
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(this.defaultColor);
  }
  
  private highlight(color: string) {
    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);
  }
}

// Usage
<p appHighlight="lightblue">Hover over me!</p>
<p [appHighlight]="color" [defaultColor]="'white'">Dynamic color</p>

// Tooltip directive
@Directive({
  selector: '[appTooltip]',
  standalone: true
})
export class TooltipDirective {
  @Input() appTooltip = '';
  private tooltipElement: HTMLElement | null = null;
  
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) { }
  
  @HostListener('mouseenter') onMouseEnter() {
    this.showTooltip();
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.hideTooltip();
  }
  
  private showTooltip() {
    this.tooltipElement = this.renderer.createElement('span');
    const text = this.renderer.createText(this.appTooltip);
    this.renderer.appendChild(this.tooltipElement, text);
    this.renderer.appendChild(document.body, this.tooltipElement);
    this.renderer.addClass(this.tooltipElement, 'tooltip');
    
    // Position tooltip
    const hostPos = this.el.nativeElement.getBoundingClientRect();
    const top = hostPos.bottom + 10;
    const left = hostPos.left;
    
    this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
    this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
  }
  
  private hideTooltip() {
    if (this.tooltipElement) {
      this.renderer.removeChild(document.body, this.tooltipElement);
      this.tooltipElement = null;
    }
  }
}

Custom Structural Directives

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

// Unless directive (opposite of *ngIf)
@Directive({
  selector: '[appUnless]',
  standalone: true
})
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
<p *appUnless="isHidden">Visible when isHidden is false</p>

// Repeat directive
@Directive({
  selector: '[appRepeat]',
  standalone: true
})
export class RepeatDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) { }
  
  @Input() set appRepeat(times: number) {
    this.viewContainer.clear();
    for (let i = 0; i < times; i++) {
      this.viewContainer.createEmbeddedView(this.templateRef, {
        $implicit: i,
        index: i
      });
    }
  }
}

// Usage
<p *appRepeat="3; let i = index">Item {{ i }}</p>

Built-in Pipes

<!-- String pipes -->
<p>{{ 'hello world' | uppercase }}</p>  <!-- HELLO WORLD -->
<p>{{ 'HELLO WORLD' | lowercase }}</p>  <!-- hello world -->
<p>{{ 'hello world' | titlecase }}</p>  <!-- Hello World -->

<!-- Number pipes -->
<p>{{ 12345.6789 | number }}</p>                    <!-- 12,345.679 -->
<p>{{ 12345.6789 | number:'1.2-4' }}</p>           <!-- 12,345.6789 -->
<p>{{ 0.259 | percent }}</p>                       <!-- 26% -->
<p>{{ 0.259 | percent:'1.2-2' }}</p>              <!-- 25.90% -->
<p>{{ 1234.56 | currency }}</p>                    <!-- $1,234.56 -->
<p>{{ 1234.56 | currency:'EUR' }}</p>             <!-- €1,234.56 -->
<p>{{ 1234.56 | currency:'USD':'symbol':'1.0-0' }}</p>  <!-- $1,235 -->

<!-- Date pipes -->
<p>{{ today | date }}</p>                          <!-- Dec 15, 2023 -->
<p>{{ today | date:'short' }}</p>                  <!-- 12/15/23, 3:30 PM -->
<p>{{ today | date:'fullDate' }}</p>               <!-- Friday, December 15, 2023 -->
<p>{{ today | date:'yyyy-MM-dd' }}</p>             <!-- 2023-12-15 -->
<p>{{ today | date:'hh:mm:ss a' }}</p>             <!-- 03:30:45 PM -->

<!-- JSON pipe -->
<pre>{{ user | json }}</pre>

<!-- Async pipe - unwraps observables/promises -->
<div *ngIf="user$ | async as user">
  {{ user.name }}
</div>

<!-- Slice pipe -->
<p>{{ [1,2,3,4,5] | slice:1:3 }}</p>  <!-- [2,3] -->
<p>{{ 'Hello World' | slice:0:5 }}</p>  <!-- Hello -->

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

Custom Pipes

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

// Exponential pipe
@Pipe({
  name: 'exponential',
  standalone: true
})
export class ExponentialPipe implements PipeTransform {
  transform(value: number, exponent: number = 1): number {
    return Math.pow(value, exponent);
  }
}

// Usage: {{ 2 | exponential:3 }}  // 8

// Truncate pipe
@Pipe({
  name: 'truncate',
  standalone: true
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 50, trail: string = '...'): string {
    return value.length > limit ? value.substring(0, limit) + trail : value;
  }
}

// Usage: {{ longText | truncate:20:'...'  }}

// Filter pipe (impure pipe - use with caution)
@Pipe({
  name: 'filter',
  standalone: true,
  pure: false  // Impure pipe - runs on every change detection
})
export class FilterPipe implements PipeTransform {
  transform(items: any[], searchText: string, property: string): any[] {
    if (!items || !searchText) {
      return items;
    }
    
    return items.filter(item => 
      item[property].toLowerCase().includes(searchText.toLowerCase())
    );
  }
}

// Usage: <div *ngFor="let user of users | filter:searchText:'name'">

// Time ago pipe
@Pipe({
  name: 'timeAgo',
  standalone: true
})
export class TimeAgoPipe implements PipeTransform {
  transform(value: Date | string): string {
    const date = new Date(value);
    const now = new Date();
    const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
    
    const intervals: { [key: string]: number } = {
      year: 31536000,
      month: 2592000,
      week: 604800,
      day: 86400,
      hour: 3600,
      minute: 60,
      second: 1
    };
    
    for (const [name, count] of Object.entries(intervals)) {
      const interval = Math.floor(seconds / count);
      if (interval >= 1) {
        return interval === 1 
          ? `${interval} ${name} ago`
          : `${interval} ${name}s ago`;
      }
    }
    
    return 'just now';
  }
}

// Safe HTML pipe
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Pipe({
  name: 'safeHtml',
  standalone: true
})
export class SafeHtmlPipe implements PipeTransform {
  constructor(private sanitizer: DomSanitizer) {}
  
  transform(value: string): SafeHtml {
    return this.sanitizer.bypassSecurityTrustHtml(value);
  }
}

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

Pure vs Impure Pipes

**Pure pipes** (default): Only re-run when input reference changes. More performant. **Impure pipes**: Run on every change detection cycle. Use sparingly as they can impact performance.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free