Skip to main content

Module Overview

Estimated Time: 3-4 hours | Difficulty: Intermediate | Prerequisites: Module 2
Directives add behavior to DOM elements, and pipes transform data for display. Together, they make your templates powerful and expressive. What You’ll Learn:
  • Structural directives (*ngIf, *ngFor, *ngSwitch)
  • Modern control flow syntax (@if, @for, @switch)
  • Attribute directives (ngClass, ngStyle)
  • Creating custom directives
  • Built-in pipes and custom pipe creation
  • Pure vs impure pipes

Structural Directives

Structural directives change the DOM layout by adding, removing, or manipulating elements.

*ngIf (Legacy) vs @if (Modern)

@Component({
  selector: 'app-user-status',
  standalone: true,
  imports: [NgIf],  // Only needed for *ngIf
  template: `
    <!-- Legacy *ngIf syntax -->
    <div *ngIf="user">
      <p>Welcome, {{ user.name }}!</p>
    </div>
    <div *ngIf="!user">
      <p>Please log in.</p>
    </div>
    
    <!-- With else -->
    <div *ngIf="user; else loginPrompt">
      Welcome, {{ user.name }}!
    </div>
    <ng-template #loginPrompt>
      <p>Please log in.</p>
    </ng-template>
    
    <!-- Modern @if syntax (Angular 17+) - Recommended! -->
    @if (user) {
      <p>Welcome, {{ user.name }}!</p>
    } @else {
      <p>Please log in.</p>
    }
    
    <!-- @if with else-if -->
    @if (user.role === 'admin') {
      <p>Admin Dashboard</p>
    } @else if (user.role === 'user') {
      <p>User Dashboard</p>
    } @else {
      <p>Guest View</p>
    }
  `
})
export class UserStatusComponent {
  user?: { name: string; role: string };
}

*ngFor (Legacy) vs @for (Modern)

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [NgFor],  // Only needed for *ngFor
  template: `
    <!-- Legacy *ngFor syntax -->
    <ul>
      <li *ngFor="let user of users; let i = index; let first = first; 
                  let last = last; let even = even; let odd = odd;
                  trackBy: trackByUserId">
        {{ i + 1 }}. {{ user.name }}
        <span *ngIf="first">(First)</span>
        <span *ngIf="last">(Last)</span>
      </li>
    </ul>
    
    <!-- Modern @for syntax (Angular 17+) - Recommended! -->
    <ul>
      @for (user of users; track user.id; let i = $index, 
            first = $first, last = $last) {
        <li>
          {{ i + 1 }}. {{ user.name }}
          @if (first) { <span>(First)</span> }
          @if (last) { <span>(Last)</span> }
        </li>
      } @empty {
        <li>No users found.</li>
      }
    </ul>
  `
})
export class UserListComponent {
  users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Charlie' }
  ];
  
  // Legacy trackBy function
  trackByUserId(index: number, user: { id: number }) {
    return user.id;
  }
}
Always use track! Without tracking, Angular rebuilds the entire list on changes. With track user.id, it only updates changed items.

*ngSwitch vs @switch

@Component({
  selector: 'app-status-badge',
  standalone: true,
  imports: [NgSwitch, NgSwitchCase, NgSwitchDefault],
  template: `
    <!-- Legacy ngSwitch -->
    <div [ngSwitch]="status">
      <span *ngSwitchCase="'active'" class="badge green">Active</span>
      <span *ngSwitchCase="'pending'" class="badge yellow">Pending</span>
      <span *ngSwitchCase="'inactive'" class="badge red">Inactive</span>
      <span *ngSwitchDefault class="badge gray">Unknown</span>
    </div>
    
    <!-- Modern @switch (Angular 17+) -->
    @switch (status) {
      @case ('active') {
        <span class="badge green">Active</span>
      }
      @case ('pending') {
        <span class="badge yellow">Pending</span>
      }
      @case ('inactive') {
        <span class="badge red">Inactive</span>
      }
      @default {
        <span class="badge gray">Unknown</span>
      }
    }
  `
})
export class StatusBadgeComponent {
  status: 'active' | 'pending' | 'inactive' | 'unknown' = 'active';
}

Attribute Directives

Attribute directives change the appearance or behavior of an element.

ngClass

@Component({
  selector: 'app-button',
  standalone: true,
  imports: [NgClass],
  template: `
    <!-- String syntax -->
    <button [ngClass]="'btn btn-primary'">Button</button>
    
    <!-- Object syntax -->
    <button [ngClass]="{
      'btn': true,
      'btn-primary': isPrimary,
      'btn-large': size === 'large',
      'disabled': isDisabled
    }">
      Button
    </button>
    
    <!-- Array syntax -->
    <button [ngClass]="['btn', buttonClass]">Button</button>
    
    <!-- Method returning class object -->
    <button [ngClass]="getButtonClasses()">Button</button>
  `
})
export class ButtonComponent {
  isPrimary = true;
  size = 'large';
  isDisabled = false;
  buttonClass = 'btn-primary';
  
  getButtonClasses() {
    return {
      'btn': true,
      'btn-primary': this.isPrimary,
      'btn-disabled': this.isDisabled
    };
  }
}

ngStyle

@Component({
  selector: 'app-styled-box',
  standalone: true,
  imports: [NgStyle],
  template: `
    <!-- Object syntax -->
    <div [ngStyle]="{
      'background-color': bgColor,
      'width.px': width,
      'height.px': height,
      'font-size.rem': 1.5
    }">
      Styled Box
    </div>
    
    <!-- Method returning style object -->
    <div [ngStyle]="getStyles()">Dynamic Styles</div>
  `
})
export class StyledBoxComponent {
  bgColor = '#DD0031';
  width = 200;
  height = 100;
  
  getStyles() {
    return {
      backgroundColor: this.bgColor,
      width: `${this.width}px`,
      height: `${this.height}px`
    };
  }
}

Custom Directives

Attribute Directive

// highlight.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input() appHighlight = 'yellow';  // Highlight color
  @Input() defaultColor = 'transparent';
  
  constructor(private el: ElementRef<HTMLElement>) {}
  
  @HostListener('mouseenter')
  onMouseEnter() {
    this.highlight(this.appHighlight || 'yellow');
  }
  
  @HostListener('mouseleave')
  onMouseLeave() {
    this.highlight(this.defaultColor);
  }
  
  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}
<!-- Usage -->
<p appHighlight>Highlight me on hover (yellow)</p>
<p [appHighlight]="'lightblue'">Highlight me (light blue)</p>
<p [appHighlight]="'pink'" [defaultColor]="'lightgray'">
  Pink highlight, gray default
</p>

Structural Directive

// unless.directive.ts
import { 
  Directive, 
  Input, 
  TemplateRef, 
  ViewContainerRef 
} from '@angular/core';

@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) {
      // Show the template
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      // Hide the template
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}
<!-- Usage: opposite of *ngIf -->
<p *appUnless="isLoggedIn">Please log in to continue.</p>

Using HostBinding and HostListener

// tooltip.directive.ts
import { 
  Directive, 
  Input, 
  HostBinding, 
  HostListener 
} from '@angular/core';

@Directive({
  selector: '[appTooltip]',
  standalone: true
})
export class TooltipDirective {
  @Input() appTooltip = '';
  
  @HostBinding('class.has-tooltip') hasTooltip = true;
  @HostBinding('attr.data-tooltip') 
  get tooltipText() { return this.appTooltip; }
  
  @HostListener('mouseenter')
  onMouseEnter() {
    // Show tooltip logic
  }
  
  @HostListener('mouseleave')
  onMouseLeave() {
    // Hide tooltip logic
  }
}

Pipes

Pipes transform data for display in templates.

Built-in Pipes

@Component({
  selector: 'app-pipes-demo',
  standalone: true,
  imports: [
    DatePipe, 
    CurrencyPipe, 
    DecimalPipe, 
    PercentPipe,
    UpperCasePipe, 
    LowerCasePipe, 
    TitleCasePipe,
    SlicePipe, 
    JsonPipe, 
    AsyncPipe,
    KeyValuePipe
  ],
  template: `
    <!-- Date Pipe -->
    <p>Today: {{ today | date }}</p>
    <p>Full: {{ today | date:'fullDate' }}</p>
    <p>Custom: {{ today | date:'yyyy-MM-dd HH:mm' }}</p>
    <p>Relative: {{ today | date:'short' }}</p>
    
    <!-- Currency Pipe -->
    <p>Price: {{ price | currency }}</p>
    <p>EUR: {{ price | currency:'EUR' }}</p>
    <p>Custom: {{ price | currency:'USD':'symbol':'1.0-0' }}</p>
    
    <!-- Number Pipes -->
    <p>Number: {{ pi | number:'1.2-4' }}</p>
    <p>Percent: {{ ratio | percent:'1.1-1' }}</p>
    
    <!-- String Pipes -->
    <p>Upper: {{ name | uppercase }}</p>
    <p>Lower: {{ name | lowercase }}</p>
    <p>Title: {{ 'hello world' | titlecase }}</p>
    
    <!-- Slice Pipe -->
    <p>Slice: {{ name | slice:0:5 }}</p>
    <p>Array: {{ items | slice:0:3 }}</p>
    
    <!-- JSON Pipe (debugging) -->
    <pre>{{ user | json }}</pre>
    
    <!-- Async Pipe (auto subscribes!) -->
    @if (users$ | async; as users) {
      <ul>
        @for (user of users; track user.id) {
          <li>{{ user.name }}</li>
        }
      </ul>
    }
    
    <!-- KeyValue Pipe -->
    @for (item of object | keyvalue; track item.key) {
      <p>{{ item.key }}: {{ item.value }}</p>
    }
  `
})
export class PipesDemoComponent {
  today = new Date();
  price = 42.5;
  pi = 3.14159265;
  ratio = 0.856;
  name = 'Angular Developer';
  items = [1, 2, 3, 4, 5];
  user = { name: 'John', age: 30 };
  users$ = of([{ id: 1, name: 'Alice' }]);
  object = { a: 1, b: 2, c: 3 };
}

Chaining Pipes

<!-- Pipes can be chained -->
<p>{{ today | date:'fullDate' | uppercase }}</p>
<p>{{ name | slice:0:10 | uppercase }}</p>

Custom Pipe

// time-ago.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

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

Filter Pipe

// filter.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'filter',
  standalone: true
})
export class FilterPipe implements PipeTransform {
  transform<T>(items: T[], field: keyof T, value: any): T[] {
    if (!items || !field || value === undefined) {
      return items;
    }
    return items.filter(item => item[field] === value);
  }
}
<!-- Usage -->
@for (user of users | filter:'role':'admin'; track user.id) {
  <p>{{ user.name }} - Admin</p>
}

Pure vs Impure Pipes

// Pure pipe (default) - only runs when input reference changes
@Pipe({
  name: 'purePipe',
  standalone: true,
  pure: true  // default
})
export class PurePipe implements PipeTransform {
  transform(value: any[]): any[] {
    console.log('Pure pipe executed');
    return value.filter(v => v.active);
  }
}

// Impure pipe - runs on every change detection cycle
@Pipe({
  name: 'impurePipe',
  standalone: true,
  pure: false  // impure
})
export class ImpurePipe implements PipeTransform {
  transform(value: any[]): any[] {
    console.log('Impure pipe executed');  // Called frequently!
    return value.filter(v => v.active);
  }
}
Avoid impure pipes! They run on every change detection cycle, which can cause performance issues. Instead, use pure pipes and ensure you create new array/object references when data changes.

Deferrable Views (@defer)

Angular 17+ introduces deferrable views for lazy loading content:
@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: `
    <h1>Dashboard</h1>
    
    <!-- Lazy load heavy component -->
    @defer {
      <app-heavy-chart></app-heavy-chart>
    } @placeholder {
      <p>Chart will load shortly...</p>
    } @loading (minimum 500ms) {
      <app-spinner></app-spinner>
    } @error {
      <p>Failed to load chart</p>
    }
    
    <!-- Defer with trigger -->
    @defer (on viewport) {
      <app-comments></app-comments>
    } @placeholder {
      <div style="height: 200px">Comments will load when scrolled into view</div>
    }
    
    <!-- Defer on interaction -->
    @defer (on interaction) {
      <app-video-player></app-video-player>
    } @placeholder {
      <button>Click to load video player</button>
    }
    
    <!-- Defer on hover -->
    @defer (on hover) {
      <app-preview-card></app-preview-card>
    } @placeholder {
      <p>Hover to load preview</p>
    }
    
    <!-- Defer with timer -->
    @defer (on timer(2s)) {
      <app-delayed-content></app-delayed-content>
    }
    
    <!-- Defer when condition is true -->
    @defer (when isDataLoaded) {
      <app-data-display></app-data-display>
    }
    
    <!-- Prefetch strategy -->
    @defer (on viewport; prefetch on idle) {
      <app-large-component></app-large-component>
    }
  `
})
export class DashboardComponent {
  isDataLoaded = false;
}

Practice Exercise

Exercise: Create a Directive and Pipe

  1. Create a DebounceClickDirective that prevents rapid clicks
  2. Create a TruncatePipe that shortens text with ellipsis
Requirements:
  • Directive should have configurable delay (default 300ms)
  • Pipe should accept max length parameter
// debounce-click.directive.ts
@Directive({
  selector: '[appDebounceClick]',
  standalone: true
})
export class DebounceClickDirective {
  @Input() debounceTime = 300;
  @Output() debounceClick = new EventEmitter();
  
  private clicks$ = new Subject<MouseEvent>();
  
  constructor() {
    this.clicks$.pipe(
      debounceTime(this.debounceTime)
    ).subscribe(e => this.debounceClick.emit(e));
  }
  
  @HostListener('click', ['$event'])
  onClick(event: MouseEvent) {
    event.preventDefault();
    event.stopPropagation();
    this.clicks$.next(event);
  }
}

// truncate.pipe.ts
@Pipe({
  name: 'truncate',
  standalone: true
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, maxLength = 50, suffix = '...'): string {
    if (!value) return '';
    if (value.length <= maxLength) return value;
    return value.substring(0, maxLength).trim() + suffix;
  }
}

// Usage
`<button appDebounceClick (debounceClick)="save()">Save</button>`
`<p>{{ longText | truncate:100:'...' }}</p>`

Summary

1

Structural Directives

Use @if, @for, @switch (modern) or *ngIf, *ngFor, *ngSwitch (legacy) to control DOM structure
2

Attribute Directives

ngClass and ngStyle for dynamic styling, create custom with @Directive
3

Pipes

Transform data for display with built-in or custom pipes
4

@defer

Lazy load content with viewport, interaction, or timer triggers

Next Steps

Next: Services & Dependency Injection

Learn about injectable services and Angular’s powerful DI system