On this page
Custom directives and pipes
Directives in Angular
Directives extend the behavior of HTML elements. There are two main types:
- Attribute directives — Modify the appearance or behavior of an element
- Structural directives — Modify the DOM structure (add/remove elements)
Attribute directives
They create reusable behavior that is applied to existing elements:
import { Directive, ElementRef, inject, input } from '@angular/core';
@Directive({
selector: '[appTooltip]',
host: {
'(mouseenter)': 'show()',
'(mouseleave)': 'hide()',
'[attr.aria-label]': 'appTooltip()',
},
})
export class TooltipDirective {
readonly appTooltip = input.required<string>();
show(): void { /* tooltip logic */ }
hide(): void { /* hide tooltip */ }
}Usage in the template:
<button [appTooltip]="'Save changes'">Save</button>Structural directives
They modify the DOM structure. They use TemplateRef and ViewContainerRef:
import { Directive, TemplateRef, ViewContainerRef, inject, input, effect } from '@angular/core';
@Directive({ selector: '[appRepeat]' })
export class RepeatDirective {
private readonly template = inject(TemplateRef);
private readonly container = inject(ViewContainerRef);
readonly appRepeat = input.required<number>();
constructor() {
effect(() => {
this.container.clear();
for (let i = 0; i < this.appRepeat(); i++) {
this.container.createEmbeddedView(this.template);
}
});
}
}Pipes: transforming data in templates
Pipes transform data for presentation. They are used with the | operator in templates.
Built-in Angular pipes
| Pipe | Usage | Example |
|---|---|---|
date |
Format dates | {{ date | date:'short' }} |
currency |
Format currency | {{ price | currency:'USD' }} |
uppercase |
Uppercase | {{ text | uppercase }} |
lowercase |
Lowercase | {{ text | lowercase }} |
titlecase |
Capitalize | {{ text | titlecase }} |
number |
Format numbers | {{ value | number:'1.2-2' }} |
percent |
Percentage | {{ ratio | percent }} |
json |
Serialize to JSON | {{ object | json }} |
slice |
Extract portion | {{ list | slice:0:5 }} |
async |
Resolve Observable/Promise | {{ obs$ | async }} |
Creating a custom pipe
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'initials' })
export class InitialsPipe implements PipeTransform {
transform(fullName: string): string {
if (!fullName) return '';
return fullName
.split(' ')
.map(word => word[0])
.join('')
.toUpperCase();
}
}Usage:
<span class="avatar">{{ user.name | initials }}</span>
<!-- "Carlos Morales" -> "CM" -->Pipes with parameters
@Pipe({ name: 'filterBy' })
export class FilterByPipe implements PipeTransform {
transform<T>(list: T[], field: keyof T, value: T[keyof T]): T[] {
return list.filter(item => item[field] === value);
}
}@for (active of users | filterBy:'status':'active'; track active.id) {
<span>{{ active.name }}</span>
}Importing directives and pipes
In Angular 21, import directives and pipes directly in the component:
@Component({
imports: [HighlightDirective, RelativeTimePipe],
template: `
<p appHighlight>{{ date | relativeTime }}</p>
`,
})Practice
- Create an attribute directive: Implement an
appTooltipdirective that shows floating text on hover over an element. Use thehostproperty of the decorator instead of@HostListener. - Build a custom pipe: Implement a
timeAgopipe that receives a date and returns a string like "5 min ago", "2 h ago", or "3 days ago". - Create a pipe with parameters: Implement a
highlightpipe that receives a text and a search term, and returns the text with the term wrapped in a<mark>tag.
In the next lesson, we will learn about Server-Side Rendering and pre-rendering to improve SEO and performance.
Pure vs impure pipes
Pure pipes (pure: true, the default) only recalculate when the input reference changes. Impure pipes (pure: false) recalculate on every change detection cycle. Prefer pure pipes.
host over HostListener
In Angular 21, use the host property of the @Directive decorator instead of @HostListener and @HostBinding. It is more declarative and works better with the AOT compiler.
import {
Directive, Pipe, PipeTransform,
ElementRef, TemplateRef, ViewContainerRef,
inject, input, effect,
} from '@angular/core';
import { AuthService } from './auth.service';
// --- Attribute directive: highlight ---
@Directive({
selector: '[appHighlight]',
host: {
'(mouseenter)': 'activate()',
'(mouseleave)': 'deactivate()',
'[style.transition]': '"background-color 0.3s ease"',
},
})
export class HighlightDirective {
private readonly el = inject(ElementRef);
readonly appHighlight = input('var(--brand-amber)');
activate(): void {
this.el.nativeElement.style.backgroundColor = this.appHighlight();
}
deactivate(): void {
this.el.nativeElement.style.backgroundColor = '';
}
}
// --- Structural directive: role-based ---
@Directive({
selector: '[appIfRole]',
})
export class IfRoleDirective {
private readonly templateRef = inject(TemplateRef);
private readonly viewContainer = inject(ViewContainerRef);
readonly appIfRole = input.required<string>();
constructor() {
const auth = inject(AuthService);
effect(() => {
const requiredRole = this.appIfRole();
const user = auth.user();
if (user?.role === requiredRole) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
});
}
}
// --- Pipe: relative time ---
@Pipe({
name: 'relativeTime',
pure: true,
})
export class RelativeTimePipe implements PipeTransform {
transform(date: string | Date): string {
const now = Date.now();
const past = new Date(date).getTime();
const diff = now - past;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes} min ago`;
if (hours < 24) return `${hours} h ago`;
if (days < 30) return `${days} days ago`;
return new Date(date).toLocaleDateString('en');
}
}
// --- Pipe: truncate text ---
@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
transform(text: string, length = 100): string {
if (!text || text.length <= length) return text;
return text.substring(0, length).trimEnd() + '...';
}
}
Sign in to track your progress