Angular Directives & Pipes
Master built-in structural directives (ngIf, ngFor, ngSwitch), attribute directives (ngClass, ngStyle), custom directives, and pipes ā built-in and custom.
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:
@if (user) {
<p>Welcome, {{ user.name }}</p>
} @else {
<p>Please log in</p>
}The classic *ngIf still works (needs CommonModule):
<p *ngIf="user; else loginPrompt">Welcome, {{ user.name }}</p>
<ng-template #loginPrompt>
<p>Please log in</p>
</ng-template>@for (Angular 17+) / *ngFor
@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:
<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:
// Class method for trackBy
trackById(index: number, item: Product): number {
return item.id;
}<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>@switch / *ngSwitch
@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:
<!-- 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:
<div [class.active]="isActive">...</div>ngStyle
Conditionally apply inline styles:
<p [ngStyle]="{ color: textColor, 'font-size': fontSize + 'px' }">Text</p>For simple cases, use the plain style binding:
<div [style.background-color]="bgColor">...</div>
<div [style.width.px]="widthValue">...</div>Custom Attribute Directives
Create directives that add behavior to any element:
ng g directive directives/highlightimport { 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:
<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
<!-- 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
{{ name | uppercase | slice:0:10 }}
{{ today | date:'fullDate' | uppercase }}Custom Pipes
ng g pipe pipes/truncateimport { 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:
{{ description | truncate }} <!-- limit=100, trail='...' -->
{{ description | truncate:50 }} <!-- limit=50 -->
{{ description | truncate:50:'ā¦' }} <!-- custom trail -->Import in your component:
@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:
@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:
@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.Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.