Directives & Pipes
The two ways Angular extends HTML: directives change elements, pipes change values.
What is a directive, and what are the types?
A class that adds behaviour to an element. Three kinds — and a component is really just a directive with a template.
| Type | Does | Examples |
|---|---|---|
| Structural | Adds or removes elements from the DOM | *ngIf, *ngFor, *ngSwitch |
| Attribute | Changes appearance or behaviour | ngClass, ngStyle, ngModel |
| Component | A directive with its own template | Everything 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.
*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>
| Angular | React |
|---|---|
*ngIf="cond" | {cond && …} |
*ngFor="let x of xs" | xs.map(x => …) |
trackBy | key |
*ngFor also gives you index, first, last,
even and odd.
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.
What is trackBy, and why does *ngFor need it?
<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
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.
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.
<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.
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 |
ElementRef | The host element |
Renderer2 | Safer than nativeElement — works with server-side rendering |
@HostListener | Subscribe 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.
Pipes, and pure vs impure
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 when | The input reference changes | Every change detection cycle |
| Cost | Cheap | Expensive |
| Use for | Almost everything | Only when you must react to mutation |
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).