Angular Directives and Pipes: Complete Guide
Angular Directives and Pipes extend HTML with powerful functionality. Learn how to use built-in directives and pipes, create custom directives for reusable behavior, and build custom pipes for data transformation in Angular applications.
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
Common structural directives:
<!-- *ngIf -->
<div *ngIf="isAuthenticated">
<p>Welcome, user!</p>
</div>
<!-- *ngFor -->
<ul>
<li *ngFor="let business of businesses; let i = index; trackBy: trackByBusinessId">
{{ business.name }}
</li>
</ul>
<!-- *ngSwitch -->
<div [ngSwitch]="userRole">
<p *ngSwitchCase="'admin'">Admin Panel</p>
<p *ngSwitchCase="'user'">User Dashboard</p>
<p *ngSwitchDefault>Guest View</p>
</div>Custom Structural Directive
Create a custom structural directive:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@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>Custom Attribute Directive
Create a custom attribute directive:
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>Built-in Pipes
Common built-in pipes:
<!-- Date Pipe -->
<p>{{ currentDate | date:'short' }}</p>
<p>{{ currentDate | date:'fullDate' }}</p>
<!-- Currency Pipe -->
<p>{{ price | currency:'USD':'symbol':'1.2-2' }}</p>
<!-- Uppercase/Lowercase -->
<p>{{ text | uppercase }}</p>
<p>{{ text | lowercase }}</p>
<!-- Decimal Pipe -->
<p>{{ number | number:'1.2-2' }}</p>
<!-- Percent Pipe -->
<p>{{ ratio | percent:'1.2-2' }}</p>
<!-- JSON Pipe -->
<pre>{{ data | json }}</pre>
<!-- Slice Pipe -->
<p>{{ items | slice:0:5 }}</p>Custom Pipe
Create a custom pipe:
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>
// Custom filter pipe
@Pipe({
name: 'filter',
pure: false
})
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>Async Pipe
Handle asynchronous data:
// 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>Best Practices
- Use structural directives for conditional rendering and loops
- Create custom directives for reusable DOM manipulation
- Use pipes for data transformation, not business logic
- Keep pipes pure when possible for better performance
- Use async pipe to handle Observables and Promises
- Avoid complex logic in templates; move to pipes or components
- Use trackBy function with *ngFor for better performance
- Chain pipes when needed: {{ value | pipe1 | pipe2 }}
- Document custom directives and pipes clearly
- Test custom directives and pipes independently
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.