Angular Directives & Pipes
Directives and pipes are powerful features in Angular. Directives add behavior to elements, while pipes transform data in templates.
Built-in Directives
Structural Directives
<!-- *ngIf - conditional rendering -->
<div *ngIf="isVisible">Visible content</div>
<div *ngIf="user; else loading">Hello, {{ user.name }}</div>
<ng-template #loading>Loading...</ng-template>
<!-- *ngIf with as for variable assignment -->
<div *ngIf="user$ | async as user">
{{ user.name }}
</div>
<!-- *ngFor - loop over collections -->
<ul>
<li *ngFor="let item of items; let i = index; let first = first; let last = last; let even = even">
{{ i + 1 }}. {{ item.name }}
<span *ngIf="first">(First)</span>
<span *ngIf="last">(Last)</span>
<span *ngIf="even">(Even)</span>
</li>
</ul>
<!-- *ngFor with trackBy for performance -->
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
<!-- *ngSwitch - switch statement -->
<div [ngSwitch]="status">
<p *ngSwitchCase="'loading'">Loading...</p>
<p *ngSwitchCase="'success'">Success!</p>
<p *ngSwitchCase="'error'">Error occurred</p>
<p *ngSwitchDefault>Unknown status</p>
</div>Attribute Directives
<!-- ngClass - dynamic classes -->
<div [ngClass]="'active'">Single class</div>
<div [ngClass]="['class1', 'class2']">Multiple classes</div>
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">Conditional classes</div>
<!-- ngStyle - dynamic styles -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">Styled text</div>
<div [ngStyle]="styleObject">Object styles</div>
<!-- ngModel - two-way binding -->
<input [(ngModel)]="name" />
<p>Hello, {{ name }}</p>Custom Attribute Directives
import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';
// Highlight directive
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
@Input() defaultColor = 'transparent';
constructor(
private el: ElementRef,
private renderer: Renderer2
) { }
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(this.defaultColor);
}
private highlight(color: string) {
this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);
}
}
// Usage
<p appHighlight="lightblue">Hover over me!</p>
<p [appHighlight]="color" [defaultColor]="'white'">Dynamic color</p>
// Tooltip directive
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective {
@Input() appTooltip = '';
private tooltipElement: HTMLElement | null = null;
constructor(
private el: ElementRef,
private renderer: Renderer2
) { }
@HostListener('mouseenter') onMouseEnter() {
this.showTooltip();
}
@HostListener('mouseleave') onMouseLeave() {
this.hideTooltip();
}
private showTooltip() {
this.tooltipElement = this.renderer.createElement('span');
const text = this.renderer.createText(this.appTooltip);
this.renderer.appendChild(this.tooltipElement, text);
this.renderer.appendChild(document.body, this.tooltipElement);
this.renderer.addClass(this.tooltipElement, 'tooltip');
// Position tooltip
const hostPos = this.el.nativeElement.getBoundingClientRect();
const top = hostPos.bottom + 10;
const left = hostPos.left;
this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
}
private hideTooltip() {
if (this.tooltipElement) {
this.renderer.removeChild(document.body, this.tooltipElement);
this.tooltipElement = null;
}
}
}Custom Structural Directives
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
// Unless directive (opposite of *ngIf)
@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) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
// Usage
<p *appUnless="isHidden">Visible when isHidden is false</p>
// Repeat directive
@Directive({
selector: '[appRepeat]',
standalone: true
})
export class RepeatDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) { }
@Input() set appRepeat(times: number) {
this.viewContainer.clear();
for (let i = 0; i < times; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: i,
index: i
});
}
}
}
// Usage
<p *appRepeat="3; let i = index">Item {{ i }}</p>Built-in Pipes
<!-- String pipes -->
<p>{{ 'hello world' | uppercase }}</p> <!-- HELLO WORLD -->
<p>{{ 'HELLO WORLD' | lowercase }}</p> <!-- hello world -->
<p>{{ 'hello world' | titlecase }}</p> <!-- Hello World -->
<!-- Number pipes -->
<p>{{ 12345.6789 | number }}</p> <!-- 12,345.679 -->
<p>{{ 12345.6789 | number:'1.2-4' }}</p> <!-- 12,345.6789 -->
<p>{{ 0.259 | percent }}</p> <!-- 26% -->
<p>{{ 0.259 | percent:'1.2-2' }}</p> <!-- 25.90% -->
<p>{{ 1234.56 | currency }}</p> <!-- $1,234.56 -->
<p>{{ 1234.56 | currency:'EUR' }}</p> <!-- €1,234.56 -->
<p>{{ 1234.56 | currency:'USD':'symbol':'1.0-0' }}</p> <!-- $1,235 -->
<!-- Date pipes -->
<p>{{ today | date }}</p> <!-- Dec 15, 2023 -->
<p>{{ today | date:'short' }}</p> <!-- 12/15/23, 3:30 PM -->
<p>{{ today | date:'fullDate' }}</p> <!-- Friday, December 15, 2023 -->
<p>{{ today | date:'yyyy-MM-dd' }}</p> <!-- 2023-12-15 -->
<p>{{ today | date:'hh:mm:ss a' }}</p> <!-- 03:30:45 PM -->
<!-- JSON pipe -->
<pre>{{ user | json }}</pre>
<!-- Async pipe - unwraps observables/promises -->
<div *ngIf="user$ | async as user">
{{ user.name }}
</div>
<!-- Slice pipe -->
<p>{{ [1,2,3,4,5] | slice:1:3 }}</p> <!-- [2,3] -->
<p>{{ 'Hello World' | slice:0:5 }}</p> <!-- Hello -->
<!-- KeyValue pipe -->
<div *ngFor="let item of object | keyvalue">
{{ item.key }}: {{ item.value }}
</div>Custom Pipes
import { Pipe, PipeTransform } from '@angular/core';
// Exponential pipe
@Pipe({
name: 'exponential',
standalone: true
})
export class ExponentialPipe implements PipeTransform {
transform(value: number, exponent: number = 1): number {
return Math.pow(value, exponent);
}
}
// Usage: {{ 2 | exponential:3 }} // 8
// Truncate pipe
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 50, trail: string = '...'): string {
return value.length > limit ? value.substring(0, limit) + trail : value;
}
}
// Usage: {{ longText | truncate:20:'...' }}
// Filter pipe (impure pipe - use with caution)
@Pipe({
name: 'filter',
standalone: true,
pure: false // Impure pipe - runs on every change detection
})
export class FilterPipe implements PipeTransform {
transform(items: any[], searchText: string, property: string): any[] {
if (!items || !searchText) {
return items;
}
return items.filter(item =>
item[property].toLowerCase().includes(searchText.toLowerCase())
);
}
}
// Usage: <div *ngFor="let user of users | filter:searchText:'name'">
// Time ago pipe
@Pipe({
name: 'timeAgo',
standalone: true
})
export class TimeAgoPipe implements PipeTransform {
transform(value: Date | string): string {
const date = new Date(value);
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const intervals: { [key: string]: number } = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
for (const [name, count] of Object.entries(intervals)) {
const interval = Math.floor(seconds / count);
if (interval >= 1) {
return interval === 1
? `${interval} ${name} ago`
: `${interval} ${name}s ago`;
}
}
return 'just now';
}
}
// Safe HTML pipe
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Pipe({
name: 'safeHtml',
standalone: true
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(value: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(value);
}
}
// Usage: <div [innerHTML]="htmlContent | safeHtml"></div>Pure vs Impure Pipes
**Pure pipes** (default): Only re-run when input reference changes. More performant. **Impure pipes**: Run on every change detection cycle. Use sparingly as they can impact performance.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free