A · Fundamentals → Module 02
Module 02 · Questions 7–12

Directives & Pipes

The two ways Angular extends HTML: directives change elements, pipes change values.

Q7

What is a directive, and what are the types?

In one line

A class that adds behaviour to an element. Three kinds — and a component is really just a directive with a template.

TypeDoesExamples
StructuralAdds or removes elements from the DOM*ngIf, *ngFor, *ngSwitch
AttributeChanges appearance or behaviourngClass, ngStyle, ngModel
ComponentA directive with its own templateEverything you write

The * marks a structural directive — it's shorthand for wrapping the element in an <ng-template>. That's why you can only have one per element.

Q8

*ngIf and *ngFor

<p *ngIf="orders.length === 0">No orders</p>

<div *ngIf="user; else loading">{{ user.name }}</div>
<ng-template #loading>Loading…</ng-template>

<li *ngFor="let o of orders; let i = index; trackBy: trackById">
  {{ i + 1 }}. {{ o.customer }}
</li>
AngularReact
*ngIf="cond"{cond && …}
*ngFor="let x of xs"xs.map(x => …)
trackBykey

*ngFor also gives you index, first, last, even and odd.

⚠ *ngIf removes the element

It isn't hidden — it's gone from the DOM, and the component inside it is destroyed and recreated each time. If you only want it invisible (to keep state, or for a cheap toggle), use [hidden] or a CSS class.

Angular 17+: @if / @else and @for are built into the template language. @for requires a track expression.

Q9

What is trackBy, and why does *ngFor need it?

The user is typing in row 3 when the list refreshes. What happens?
<li *ngFor="let o of orders">
  <input [(ngModel)]="o.note">
</li>

// every 30 seconds:
this.orders = await this.api.getOrders();   // a NEW array, same data
Every row is destroyed and rebuilt. The user loses focus mid-word, the caret jumps, and anything typed but not yet saved is gone. All 200 rows re-render even though nothing changed.

Angular compares items by object identity. A new array from the API means new objects, so as far as Angular is concerned every row is a different row.

<li *ngFor="let o of orders; trackBy: trackById">

trackById(index: number, order: Order) { return order.id; }

Now Angular matches rows by id, keeps the existing DOM, and only touches what actually changed. It's exactly React's key.

Track by id, never by index

Tracking by index tells Angular "position 3 is still position 3", which is wrong the moment anything is inserted, removed or reordered — and reintroduces the bug you were fixing.

Q10ngClass and ngStyle
<div [ngClass]="{ active: isActive, error: hasError }"></div>
<div [ngStyle]="{ color: textColour, 'font-size.px': size }"></div>

<!-- for a single class or style, these are simpler and faster -->
<div [class.active]="isActive"></div>
<div [style.color]="textColour"></div>

Both take an object, an array or a string. Prefer [class.x] when there's only one condition — less to evaluate on every change detection cycle.

Q11

How do you write a custom directive?

@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
  @Input() appHighlight = 'yellow';

  constructor(private el: ElementRef, private r: Renderer2) {}

  @HostListener('mouseenter') onEnter() {
    this.r.setStyle(this.el.nativeElement, 'background', this.appHighlight);
  }

  @HostListener('mouseleave') onLeave() {
    this.r.removeStyle(this.el.nativeElement, 'background');
  }
}
<p appHighlight="lightblue">Hover me</p>
Selector in brackets'[appHighlight]' — it applies as an attribute
ElementRefThe host element
Renderer2Safer than nativeElement — works with server-side rendering
@HostListenerSubscribe to an event on the host

When you'd write one: a permission directive that hides elements by role, an autofocus directive, click-outside, infinite scroll, input masking.

Q12

Pipes, and pure vs impure

In one line

A pipe transforms a value in the template without changing the underlying data.

{{ order.date  | date:'dd/MM/yyyy' }}
{{ order.total | currency:'INR' }}
{{ user.name   | uppercase }}
{{ data        | json }}
{{ orders$     | async }}          <!-- subscribes and unsubscribes for you -->
@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
  transform(value: string, max = 20): string {
    return value.length <= max ? value : value.slice(0, max) + '…';
  }
}

// {{ description | truncate:50 }}
Pure (default)Impure
Re-runs whenThe input reference changesEvery change detection cycle
CostCheapExpensive
Use forAlmost everythingOnly when you must react to mutation
⚠ The pure-pipe surprise

A pure pipe over an array won't re-run when you push to that array — the reference didn't change. People then mark the pipe impure, which makes it run on every cycle and quietly wrecks performance. The right fix is to replace the array (this.items = [...this.items, x]), not to change the pipe.

async is the pipe worth knowing best — it subscribes, renders each value, and unsubscribes on destroy, which removes a whole class of memory leak (Q44).