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.
📖 Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
What are Angular Directives and Pipes?
Angular Directives provide:
-
Structural Directives - Change DOM structure (
*ngIf,*ngFor,*ngSwitch) -
Attribute Directives - Modify element appearance/behavior (
[ngClass],[ngStyle]) - Custom Directives - Reusable behavior for specific use cases
Angular Pipes provide:
- Built-in Pipes - Date, currency, uppercase, lowercase, json, etc.
- Custom Pipes - Data transformations specific to your application
- Async Pipe - Handle Observables and Promises automatically
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
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
@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
@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
@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
<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)
@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
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
@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
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
// ✅ 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
@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
@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>
Resources and Further Reading
- 📚 Full Angular Directives and Pipes Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- Angular Reactive Forms Guide - Form handling with directives
- Angular Component Communication Guide - Component interaction patterns
- Angular Directives Documentation - Official Angular docs
- Angular Pipes Documentation - Official pipes guide
- Angular Structural Directives - Structural directives guide
Conclusion
Angular Directives and Pipes are powerful features that extend HTML capabilities and transform data for display. By understanding built-in directives and pipes, and creating custom ones, you can build more maintainable and reusable Angular applications.
Key Takeaways:
-
Structural directives - Change DOM structure (
*ngIf,*ngFor,*ngSwitch) - Attribute directives - Modify element appearance/behavior
- Custom directives - Reusable behavior for specific use cases
- Built-in pipes - Date, currency, uppercase, lowercase, json, etc.
- Custom pipes - Data transformations specific to your application
- Async pipe - Handle Observables and Promises automatically
- Pure vs Impure - Performance considerations for pipes
-
TrackBy function - Better performance with
*ngFor
Whether you're building a simple data display or a complex interactive application, Angular Directives and Pipes provide the foundation you need. They handle DOM manipulation and data transformation while keeping your templates clean and maintainable.
What's your experience with Angular Directives and Pipes? Share your tips and tricks in the comments below! 🚀
💡 Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.
Top comments (0)