Angular Directives and Pipes – Complete Guide
Learn Angular directives (structural, attribute, custom) and pipes (built-in, custom, async). Complete guide for building reusable Angular components and data transformations.
When I first started with Angular, I thought directives and pipes were just nice-to-have features. Then I found myself writing the same conditional rendering logic in multiple components, and the same data transformation code in multiple templates. That's when I realized that custom directives and pipes are powerful tools for creating reusable, maintainable code.
Directives extend HTML with custom behavior. Structural directives (like *ngIf and *ngFor) change the DOM structure, while attribute directives (like [ngClass] and [ngStyle]) modify element appearance or behavior. Pipes transform data for display in templates—formatting dates, currencies, and text. Both are essential for building clean, maintainable Angular templates.
In this guide, I'll show you how I use directives and pipes in production Angular applications. We'll cover built-in directives and pipes (and when to use them), creating custom structural directives (for reusable conditional rendering), creating custom attribute directives (for reusable behavior), creating custom pipes (for data transformation), pure vs impure pipes (performance considerations), and best practices for organizing directives and pipes. I'll also share some patterns I've learned for building reusable directives and pipes that work well across different components.
Built-in Structural Directives
Angular provides powerful structural directives out of the box:
<!-- *ngIf - Conditional rendering -->
<div *ngIf="isAuthenticated">
<p>Welcome, user!</p>
</div>
<!-- *ngIf with else -->
<div *ngIf="isLoading; else content">
<p>Loading...</p>
</div>
<ng-template #content>
<p>Content loaded</p>
</ng-template>
<!-- *ngFor - Loop through arrays -->
<ul>
<li *ngFor="let business of businesses; let i = index; trackBy: trackByBusinessId">
{{ i + 1 }}. {{ business.name }}
</li>
</ul>
<!-- *ngSwitch - Multiple conditions -->
<div [ngSwitch]="userRole">
<p *ngSwitchCase="'admin'">Admin Panel</p>
<p *ngSwitchCase="'user'">User Dashboard</p>
<p *ngSwitchCase="'manager'">Manager View</p>
<p *ngSwitchDefault>Guest View</p>
</div>TrackBy Function for Performance
Use trackBy function with *ngFor to improve performance with large lists:
export class BusinessListComponent {
businesses: Business[];
trackByBusinessId(index: number, business: Business): number {
return business.id;
}
}
// Template
<div *ngFor="let business of businesses; trackBy: trackByBusinessId">
{{ business.name }}
</div>Custom Structural Directive
Create custom structural directives for reusable conditional rendering:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { AuthService } from '../auth/auth.service';
@Directive({
selector: '[appHasPermission]'
})
export class HasPermissionDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private authService: AuthService
) {}
@Input() set appHasPermission(permission: string) {
const hasPermission = this.authService.hasPermission(permission);
if (hasPermission && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (!hasPermission && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
// Usage
<div *appHasPermission="'admin'">
Admin content
</div>
<div *appHasPermission="'business:read'">
Business details
</div>Advanced Structural Directive
Create an "unless" directive (opposite of *ngIf):
@Directive({
selector: '[appUnless]'
})
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 (opposite of *ngIf)
<div *appUnless="isHidden">
This content shows when isHidden is false
</div>Custom Attribute Directive
Create custom attribute directives for reusable behavior:
import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string | null) {
this.renderer.setStyle(this.el.nativeElement, 'background-color', color);
}
}
// Usage
<p appHighlight="lightblue">Hover over me</p>Directive with @HostBinding
Use @HostBinding to bind to host element properties:
@Directive({
selector: '[appFocus]'
})
export class FocusDirective {
@Input() appFocus: boolean;
@HostBinding('class.focused') get isFocused() {
return this.appFocus;
}
@HostListener('click') onClick() {
this.appFocus = true;
}
}Directive with Multiple Inputs
Create directives that accept multiple input parameters:
@Directive({
selector: '[appTooltip]'
})
export class TooltipDirective {
@Input() appTooltip: string;
@Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
@HostListener('mouseenter') onMouseEnter() {
this.showTooltip();
}
@HostListener('mouseleave') onMouseLeave() {
this.hideTooltip();
}
private showTooltip(): void {
// Tooltip logic
}
private hideTooltip(): void {
// Hide tooltip logic
}
}
// Usage
<span appTooltip="Help text" tooltipPosition="bottom">Hover me</span>Built-in Pipes
Angular provides many built-in pipes for common transformations:
<!-- Date Pipe -->
<p>{{ currentDate | date:'short' }}</p>
<p>{{ currentDate | date:'fullDate' }}</p>
<p>{{ currentDate | date:'MM/dd/yyyy' }}</p>
<p>{{ currentDate | date:'medium' }}</p>
<!-- Currency Pipe -->
<p>{{ price | currency:'USD':'symbol':'1.2-2' }}</p>
<p>{{ price | currency:'EUR':'symbol':'1.2-2' }}</p>
<p>{{ price | currency:'USD':'$' }}</p>
<!-- Uppercase/Lowercase -->
<p>{{ text | uppercase }}</p>
<p>{{ text | lowercase }}</p>
<p>{{ text | titlecase }}</p>
<!-- Decimal Pipe -->
<p>{{ number | number:'1.2-2' }}</p>
<p>{{ number | number:'3.1-5' }}</p>
<!-- Percent Pipe -->
<p>{{ ratio | percent:'1.2-2' }}</p>
<p>{{ ratio | percent }}</p>
<!-- JSON Pipe (for debugging) -->
<pre>{{ data | json }}</pre>
<!-- Slice Pipe -->
<p>{{ items | slice:0:5 }}</p>
<p>{{ text | slice:0:20 }}</p>
<!-- KeyValue Pipe -->
<div *ngFor="let item of object | keyvalue">
{{ item.key }}: {{ item.value }}
</div>Chaining Pipes
Chain multiple pipes together for complex transformations:
<p>{{ currentDate | date:'fullDate' | uppercase }}</p>
<p>{{ price | currency:'USD' | slice:1 }}</p>Custom Pipe
Create custom pipes for specific data transformations:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
pure: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 50, trail: string = '...'): string {
if (!value) return '';
if (value.length <= limit) return value;
return value.substring(0, limit) + trail;
}
}
// Usage
<p>{{ longText | truncate:100 }}</p>
<p>{{ description | truncate:50:'...' }}</p>Filter Pipe (Impure)
Create impure pipes for filtering that need to run on every change detection:
@Pipe({
name: 'filter',
pure: false // Impure pipe - runs on every change detection
})
export class FilterPipe implements PipeTransform {
transform(items: any[], searchText: string, field: string): any[] {
if (!items || !searchText) return items;
return items.filter(item =>
item[field].toLowerCase().includes(searchText.toLowerCase())
);
}
}
// Usage
<div *ngFor="let item of items | filter:searchTerm:'name'">
{{ item.name }}
</div>Pure vs Impure Pipes
Understanding the difference between pure and impure pipes:
Pure Pipes (default):
- Only run when input reference changes
- Better performance
- Use for simple transformations
Impure Pipes:
- Run on every change detection cycle
- Use when you need to detect changes in nested objects/arrays
- Can impact performance
// Pure pipe (default)
@Pipe({
name: 'truncate',
pure: true // Only runs when input changes
})
// Impure pipe
@Pipe({
name: 'filter',
pure: false // Runs on every change detection
})Custom Currency Pipe
Create a custom currency pipe with advanced formatting:
@Pipe({
name: 'customCurrency',
pure: true
})
export class CustomCurrencyPipe implements PipeTransform {
transform(value: number, currency: string = 'USD'): string {
if (value == null) return '';
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency
});
return formatter.format(value);
}
}
// Usage
<p>{{ price | customCurrency:'EUR' }}</p>Async Pipe
Handle asynchronous data with the async pipe:
// Component
export class BusinessListComponent {
businesses$: Observable<any[]>;
constructor(private businessService: BusinessService) {
this.businesses$ = this.businessService.GetBusinesses({});
}
}
// Template
<div *ngIf="businesses$ | async as businesses">
<div *ngFor="let business of businesses">
{{ business.name }}
</div>
</div>
// With loading state
<ng-container *ngIf="businesses$ | async as businesses; else loading">
<div *ngFor="let business of businesses">
{{ business.name }}
</div>
</ng-container>
<ng-template #loading>
<p>Loading...</p>
</ng-template>
// With error handling
<ng-container *ngIf="businesses$ | async as businesses; else loading">
<div *ngFor="let business of businesses">
{{ business.name }}
</div>
</ng-container>
<ng-template #loading>
<p>Loading businesses...</p>
</ng-template>Multiple Async Pipes
Handle multiple observables in a single template:
export class DashboardComponent {
businesses$: Observable<Business[]>;
categories$: Observable<Category[]>;
constructor(
private businessService: BusinessService,
private categoryService: CategoryService
) {
this.businesses$ = this.businessService.GetBusinesses({});
this.categories$ = this.categoryService.GetCategories();
}
}
// Template
<ng-container *ngIf="businesses$ | async as businesses">
<ng-container *ngIf="categories$ | async as categories">
<div *ngFor="let business of businesses">
<p>{{ business.name }}</p>
<p>Category: {{ getCategoryName(business.categoryId, categories) }}</p>
</div>
</ng-container>
</ng-container>Best Practices
- Use structural directives for conditional rendering - `*ngIf`, `*ngFor`, `*ngSwitch`
- Create custom directives for reusable DOM manipulation - Avoid code duplication
- Use pipes for data transformation - Not for business logic
- Keep pipes pure when possible - Better performance
- Use async pipe - Automatically handles Observables and Promises
- Avoid complex logic in templates - Move to pipes or components
- Use trackBy function with *ngFor - Better performance with large lists
- Chain pipes when needed - {{ value | pipe1 | pipe2 }}
- Document custom directives and pipes - Clear usage instructions
- Test custom directives and pipes independently - Unit test them separately
Performance Tips
Follow these performance best practices:
// ✅ Good - Pure pipe
@Pipe({ name: 'truncate', pure: true })
// ❌ Avoid - Impure pipe unless necessary
@Pipe({ name: 'filter', pure: false })
// ✅ Good - TrackBy function
*ngFor="let item of items; trackBy: trackById"
// ❌ Avoid - No trackBy
*ngFor="let item of items"Common Patterns
Permission-Based Directive
Create a directive that shows/hides content based on user permissions:
@Directive({
selector: '[appRequirePermission]'
})
export class RequirePermissionDirective {
@Input() appRequirePermission: string;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private authService: AuthService
) {}
ngOnInit(): void {
if (this.authService.hasPermission(this.appRequirePermission)) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}Format Phone Number Pipe
Create a pipe to format phone numbers:
@Pipe({
name: 'phone',
pure: true
})
export class PhonePipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
const cleaned = value.replace(/D/g, '');
if (cleaned.length === 10) {
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
}
return value;
}
}
// Usage
<p>{{ phoneNumber | phone }}</p>Conclusion
Directives and Pipes are powerful Angular features that extend HTML capabilities. By understanding built-in directives and pipes, and creating custom ones, you can build more maintainable and reusable Angular applications.