Angular: Zero to Senior · Lesson 3 of 11

Directives & Pipes

Directives extend HTML with new behavior. Pipes transform displayed values. Together they make your templates powerful and readable without cluttering your component class.


Structural Directives

Structural directives change the DOM layout — they add, remove, or replace elements.

@if (Angular 17+) / *ngIf

The modern Angular 17+ control flow syntax:

HTML
@if (user) {
  <p>Welcome, {{ user.name }}</p>
} @else {
  <p>Please log in</p>
}

The classic *ngIf still works (needs CommonModule):

HTML
<p *ngIf="user; else loginPrompt">Welcome, {{ user.name }}</p>
<ng-template #loginPrompt>
  <p>Please log in</p>
</ng-template>

@for (Angular 17+) / *ngFor

HTML
@for (product of products; track product.id) {
  <div class="card">
    <h3>{{ product.name }}</h3>
    <p>{{ product.price | currency }}</p>
  </div>
} @empty {
  <p>No products found.</p>
}

Classic *ngFor with common variables:

HTML
<li *ngFor="let item of items; let i = index; let last = last"
    [class.last]="last">
  {{ i + 1 }}. {{ item.name }}
</li>

Available loop variables: index, first, last, even, odd, count.

Always use track / trackBy to help Angular identify items by a unique key — avoids re-rendering the whole list when data changes:

TYPESCRIPT
// Class method for trackBy
trackById(index: number, item: Product): number {
  return item.id;
}
HTML
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>

@switch / *ngSwitch

HTML
@switch (status) {
  @case ('active')   { <span class="green">Active</span> }
  @case ('inactive') { <span class="red">Inactive</span> }
  @default           { <span>Unknown</span> }
}

Attribute Directives

Attribute directives change the appearance or behavior of an existing element.

ngClass

Conditionally apply CSS classes:

HTML
<!-- Object form  most flexible -->
<div [ngClass]="{
  'active':   isActive,
  'disabled': isDisabled,
  'highlight': score > 80
}">...</div>

<!-- Array form -->
<div [ngClass]="['base-class', isActive ? 'active' : 'inactive']">...</div>

<!-- String form -->
<div [ngClass]="'primary bold'">...</div>

For simple toggles, prefer the plain class binding:

HTML
<div [class.active]="isActive">...</div>

ngStyle

Conditionally apply inline styles:

HTML
<p [ngStyle]="{ color: textColor, 'font-size': fontSize + 'px' }">Text</p>

For simple cases, use the plain style binding:

HTML
<div [style.background-color]="bgColor">...</div>
<div [style.width.px]="widthValue">...</div>

Custom Attribute Directives

Create directives that add behavior to any element:

Bash
ng g directive directives/highlight
TYPESCRIPT
import { Directive, ElementRef, HostListener, Input, OnInit } from '@angular/core';

@Directive({
  selector: '[appHighlight]',  // applied as: <p appHighlight>
  standalone: true,
})
export class HighlightDirective implements OnInit {
  @Input() appHighlight = 'yellow';  // default highlight color
  @Input() defaultColor = 'transparent';

  constructor(private el: ElementRef) {}

  ngOnInit(): void {
    this.el.nativeElement.style.backgroundColor = this.defaultColor;
  }

  @HostListener('mouseenter')
  onMouseEnter(): void {
    this.el.nativeElement.style.backgroundColor = this.appHighlight;
  }

  @HostListener('mouseleave')
  onMouseLeave(): void {
    this.el.nativeElement.style.backgroundColor = this.defaultColor;
  }
}

Usage:

HTML
<p appHighlight="lightblue">Hover to highlight</p>
<p [appHighlight]="userColor" [defaultColor]="'#f0f0f0'">Custom color</p>

Pipes

Pipes transform values in the template: {{ value | pipeName }}.

Built-in Pipes

HTML
<!-- DatePipe -->
{{ today | date }}                    <!-- Apr 16, 2026 -->
{{ today | date:'yyyy-MM-dd' }}       <!-- 2026-04-16 -->
{{ today | date:'shortTime' }}        <!-- 3:45 PM -->

<!-- CurrencyPipe -->
{{ price | currency }}                <!-- $1,234.56 -->
{{ price | currency:'GBP':'symbol' }} <!-- £1,234.56 -->

<!-- DecimalPipe -->
{{ 3.14159 | number:'1.2-3' }}        <!-- 3.142 (min 1 integer, 2-3 decimals) -->

<!-- PercentPipe -->
{{ 0.85 | percent }}                  <!-- 85% -->
{{ 0.85 | percent:'1.1-2' }}          <!-- 85.0% -->

<!-- UpperCase / LowerCase / TitleCase -->
{{ 'hello world' | uppercase }}       <!-- HELLO WORLD -->
{{ 'Hello World' | lowercase }}       <!-- hello world -->
{{ 'hello world' | titlecase }}       <!-- Hello World -->

<!-- SlicePipe  works on arrays and strings -->
{{ [1,2,3,4,5] | slice:1:3 }}         <!-- [2, 3] -->
{{ 'Angular' | slice:0:3 }}           <!-- Ang -->

<!-- AsyncPipe  unwraps Observable/Promise -->
{{ user$ | async }}

Chaining Pipes

HTML
{{ name | uppercase | slice:0:10 }}
{{ today | date:'fullDate' | uppercase }}

Custom Pipes

Bash
ng g pipe pipes/truncate
TYPESCRIPT
import { Pipe, PipeTransform } from '@angular/core';

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

Usage:

HTML
{{ description | truncate }}           <!-- limit=100, trail='...' -->
{{ description | truncate:50 }}        <!-- limit=50 -->
{{ description | truncate:50:'' }}    <!-- custom trail -->

Import in your component:

TYPESCRIPT
@Component({
  standalone: true,
  imports: [TruncatePipe],
  // ...
})

The AsyncPipe — Essential for Observables

The async pipe subscribes to an Observable or Promise and automatically unsubscribes when the component is destroyed:

TYPESCRIPT
@Component({
  selector: 'app-users',
  standalone: true,
  imports: [CommonModule, AsyncPipe],
  template: `
    @if (users$ | async; as users) {
      @for (user of users; track user.id) {
        <p>{{ user.name }}</p>
      }
    } @else {
      <p>Loading...</p>
    }
  `,
})
export class UsersComponent {
  users$ = this.userService.getAll();
  constructor(private userService: UserService) {}
}

Using async pipe is almost always better than manually subscribing in ngOnInit — it handles unsubscription automatically and works cleanly with OnPush change detection.


Pure vs Impure Pipes

By default pipes are pure — they only re-run when the input reference changes. This is a performance optimization.

Impure pipes run on every change detection cycle — use sparingly:

TYPESCRIPT
@Pipe({
  name: 'filterList',
  standalone: true,
  pure: false,  // runs on every change detection — expensive!
})
export class FilterListPipe implements PipeTransform {
  transform(items: any[], searchTerm: string): any[] {
    return items.filter(i => i.name.includes(searchTerm));
  }
}

For filtering/sorting lists, it's usually better to compute the result in the component class (using a getter or signal) than to use an impure pipe.


Quick Reference

Structural (new):   @if / @else, @for track, @switch / @case
Structural (old):   *ngIf, *ngFor trackBy, *ngSwitch / *ngSwitchCase
Attribute:          [ngClass]="{cls: bool}", [ngStyle]="{}",
                    [class.name]="bool", [style.prop]="val"
Custom directive:   @Directive({ selector: '[appName]' })
                    HostListener for events, ElementRef for DOM access
Built-in pipes:     date, currency, number, percent,
                    uppercase, lowercase, titlecase, slice, async
Custom pipe:        @Pipe({ name: 'myPipe' }) implements PipeTransform
Async pipe:         {{ obs$ | async }} — auto-subscribes + unsubscribes
Pure vs impure:     Pure (default) = ref-change only. Impure = every tick.