Angular Directives¶
Directives are a fundamental feature of Angular that allow you to extend HTML with custom behavior. They are classes that add additional behavior to elements in your Angular applications. Angular provides several built-in directives and also allows you to create custom directives to encapsulate reusable DOM manipulation logic.
Understanding Directives¶
Angular directives are instructions in the DOM that tell Angular how to transform the DOM. There are three types of directives:
- Components: Directives with templates (covered in previous sections)
- Structural Directives: Change the DOM layout by adding/removing elements
- Attribute Directives: Change the appearance or behavior of elements
Directive Decorator¶
import { Directive, ElementRef, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) {}
}
Structural Directives¶
Structural directives are responsible for HTML layout. They shape or reshape the DOM's structure by adding, removing, or manipulating elements.
Key Characteristics¶
- Prefixed with an asterisk (*) in templates
- Can add or remove elements from the DOM
- Only one structural directive per element
Microsyntax¶
Structural directives use a special microsyntax for their expressions:
<!-- Full form -->
<ng-template [ngIf]="condition">
<div>Content</div>
</ng-template>
<!-- Shorthand with asterisk -->
<div *ngIf="condition">Content</div>
Built-in Structural Directives¶
*ngIf - Conditional Rendering¶
// component.ts
@Component({
selector: 'app-conditional',
template: `
<div class="conditional-demo">
<button (click)="toggleShow()">Toggle Content</button>
<button (click)="toggleUser()">Toggle User</button>
<!-- Basic ngIf -->
<div *ngIf="showContent" class="content">
<h3>This content is conditionally shown</h3>
</div>
<!-- ngIf with else -->
<div *ngIf="user; else noUser">
<h3>Welcome, {{ user.name }}!</h3>
<p>Email: {{ user.email }}</p>
</div>
<ng-template #noUser>
<p>Please log in to see user information.</p>
</ng-template>
<!-- ngIf with then and else -->
<div *ngIf="isLoading; then loading; else loaded"></div>
<ng-template #loading>
<div class="loading">Loading...</div>
</ng-template>
<ng-template #loaded>
<div class="loaded">Content has loaded!</div>
</ng-template>
<!-- ngIf with as (storing result) -->
<div *ngIf="getUser() as currentUser">
<h3>Current User: {{ currentUser.name }}</h3>
</div>
</div>
`
})
export class ConditionalComponent {
showContent = false;
isLoading = false;
user: { name: string; email: string } | null = null;
toggleShow(): void {
this.showContent = !this.showContent;
}
toggleUser(): void {
this.user = this.user ? null : { name: 'John Doe', email: 'john@example.com' };
}
getUser(): { name: string; email: string } | null {
return this.user;
}
}
*ngFor - List Rendering¶
@Component({
selector: 'app-list',
template: `
<div class="list-demo">
<!-- Basic ngFor -->
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
<!-- ngFor with index -->
<ul>
<li *ngFor="let item of items; let i = index">
{{ i + 1 }}. {{ item }}
</li>
</ul>
<!-- ngFor with trackBy for performance -->
<div class="user-list">
<div *ngFor="let user of users; trackBy: trackByUserId" class="user-card">
<h4>{{ user.name }}</h4>
<p>{{ user.email }}</p>
<button (click)="updateUser(user.id)">Update</button>
</div>
</div>
<!-- ngFor with multiple values -->
<div *ngFor="let item of items; let i = index; let first = first; let last = last; let even = even; let odd = odd"
[class.first]="first"
[class.last]="last"
[class.even]="even"
[class.odd]="odd">
{{ i }}: {{ item }}
</div>
<!-- ngFor with object properties -->
<div *ngFor="let entry of objectEntries">
<strong>{{ entry.key }}:</strong> {{ entry.value }}
</div>
</div>
`
})
export class ListComponent {
items = ['Apple', 'Banana', 'Cherry', 'Date'];
users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
];
userObject = {
name: 'John Doe',
age: 30,
city: 'New York',
occupation: 'Developer'
};
get objectEntries() {
return Object.entries(this.userObject).map(([key, value]) => ({ key, value }));
}
trackByUserId(index: number, user: any): number {
return user.id;
}
updateUser(userId: number): void {
const user = this.users.find(u => u.id === userId);
if (user) {
user.name = `${user.name} (Updated)`;
}
}
}
*ngSwitch - Multiple Condition Rendering¶
@Component({
selector: 'app-switch',
template: `
<div class="switch-demo">
<select [(ngModel)]="selectedView">
<option value="list">List View</option>
<option value="grid">Grid View</option>
<option value="table">Table View</option>
<option value="chart">Chart View</option>
</select>
<div [ngSwitch]="selectedView" class="view-container">
<div *ngSwitchCase="'list'" class="list-view">
<h3>List View</h3>
<ul>
<li *ngFor="let item of data">{{ item.name }}</li>
</ul>
</div>
<div *ngSwitchCase="'grid'" class="grid-view">
<h3>Grid View</h3>
<div class="grid">
<div *ngFor="let item of data" class="grid-item">
{{ item.name }}
</div>
</div>
</div>
<div *ngSwitchCase="'table'" class="table-view">
<h3>Table View</h3>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of data">
<td>{{ item.name }}</td>
<td>{{ item.value }}</td>
</tr>
</tbody>
</table>
</div>
<div *ngSwitchDefault class="default-view">
<h3>Chart View</h3>
<p>Chart visualization would go here</p>
</div>
</div>
</div>
`
})
export class SwitchComponent {
selectedView = 'list';
data = [
{ name: 'Item 1', value: 100 },
{ name: 'Item 2', value: 200 },
{ name: 'Item 3', value: 300 }
];
}
Built-in Attribute Directives¶
ngClass - Dynamic CSS Classes¶
@Component({
selector: 'app-styling',
template: `
<div class="styling-demo">
<!-- Object syntax -->
<div [ngClass]="{
'highlight': isHighlighted,
'error': hasError,
'disabled': isDisabled
}">
Object syntax example
</div>
<!-- Array syntax -->
<div [ngClass]="['base-class', dynamicClass, conditionalClass]">
Array syntax example
</div>
<!-- String syntax -->
<div [ngClass]="classString">
String syntax example
</div>
<!-- Method binding -->
<div [ngClass]="getClasses()">
Method binding example
</div>
<!-- Multiple conditions -->
<div [ngClass]="{
'status-active': user?.status === 'active',
'status-inactive': user?.status === 'inactive',
'premium-user': user?.isPremium,
'new-user': isNewUser()
}">
User status indicator
</div>
<button (click)="toggleHighlight()">Toggle Highlight</button>
<button (click)="toggleError()">Toggle Error</button>
<button (click)="changeTheme()">Change Theme</button>
</div>
`,
styles: [`
.highlight { background-color: yellow; }
.error { color: red; border: 1px solid red; }
.disabled { opacity: 0.5; pointer-events: none; }
.theme-dark { background-color: #333; color: white; }
.theme-light { background-color: white; color: black; }
.status-active { border-left: 4px solid green; }
.status-inactive { border-left: 4px solid red; }
.premium-user { background-color: gold; }
.new-user { border: 2px dashed blue; }
`]
})
export class StylingComponent {
isHighlighted = false;
hasError = false;
isDisabled = false;
currentTheme = 'light';
user = {
status: 'active',
isPremium: true,
joinDate: new Date()
};
get dynamicClass(): string {
return this.currentTheme === 'dark' ? 'theme-dark' : 'theme-light';
}
get conditionalClass(): string {
return this.hasError ? 'error' : '';
}
get classString(): string {
return `base-class ${this.dynamicClass} ${this.conditionalClass}`;
}
getClasses(): object {
return {
'highlight': this.isHighlighted,
'error': this.hasError,
[`theme-${this.currentTheme}`]: true
};
}
isNewUser(): boolean {
const daysSinceJoin = (Date.now() - this.user.joinDate.getTime()) / (1000 * 60 * 60 * 24);
return daysSinceJoin < 30;
}
toggleHighlight(): void {
this.isHighlighted = !this.isHighlighted;
}
toggleError(): void {
this.hasError = !this.hasError;
}
changeTheme(): void {
this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
}
}
ngStyle - Dynamic Inline Styles¶
@Component({
selector: 'app-dynamic-styling',
template: `
<div class="dynamic-styling-demo">
<!-- Object syntax -->
<div [ngStyle]="{
'color': textColor,
'font-size': fontSize + 'px',
'background-color': backgroundColor,
'padding': padding + 'px',
'border-radius': borderRadius + 'px'
}">
Dynamic styling with object syntax
</div>
<!-- Method binding -->
<div [ngStyle]="getStyles()">
Dynamic styling with method
</div>
<!-- Conditional styles -->
<div [ngStyle]="{
'transform': isRotated ? 'rotate(45deg)' : 'rotate(0deg)',
'transition': 'transform 0.3s ease',
'opacity': isVisible ? 1 : 0.3
}">
Conditional transformations
</div>
<!-- Progress bar example -->
<div class="progress-container">
<div
class="progress-bar"
[ngStyle]="{
'width': progress + '%',
'background-color': getProgressColor()
}">
</div>
</div>
<div class="controls">
<label>
Text Color:
<input type="color" [(ngModel)]="textColor">
</label>
<label>
Font Size:
<input type="range" min="12" max="48" [(ngModel)]="fontSize">
</label>
<label>
Background:
<input type="color" [(ngModel)]="backgroundColor">
</label>
<label>
Progress:
<input type="range" min="0" max="100" [(ngModel)]="progress">
</label>
<button (click)="toggleRotation()">Toggle Rotation</button>
<button (click)="toggleVisibility()">Toggle Visibility</button>
</div>
</div>
`,
styles: [`
.progress-container {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
margin: 10px 0;
}
.progress-bar {
height: 100%;
border-radius: 10px;
transition: width 0.3s ease;
}
.controls {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
}
.controls label {
display: flex;
align-items: center;
gap: 10px;
}
`]
})
export class DynamicStylingComponent {
textColor = '#000000';
fontSize = 16;
backgroundColor = '#ffffff';
padding = 10;
borderRadius = 5;
progress = 50;
isRotated = false;
isVisible = true;
getStyles(): object {
return {
'color': this.textColor,
'font-size': this.fontSize + 'px',
'background-color': this.backgroundColor,
'padding': this.padding + 'px',
'border-radius': this.borderRadius + 'px',
'border': '2px solid ' + this.textColor
};
}
getProgressColor(): string {
if (this.progress < 30) return '#ff4444';
if (this.progress < 70) return '#ffaa00';
return '#44ff44';
}
toggleRotation(): void {
this.isRotated = !this.isRotated;
}
toggleVisibility(): void {
this.isVisible = !this.isVisible;
}
}
Creating Custom Attribute Directives¶
Basic Custom Directive¶
// highlight.directive.ts
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
@Input() appHighlight = 'yellow';
@Input() defaultColor = 'transparent';
constructor(private el: ElementRef) {}
ngOnInit(): void {
this.el.nativeElement.style.backgroundColor = this.appHighlight || this.defaultColor;
}
}
Interactive Custom Directive¶
// hover-highlight.directive.ts
import {
Directive,
ElementRef,
HostListener,
Input,
Renderer2,
OnInit
} from '@angular/core';
@Directive({
selector: '[appHoverHighlight]'
})
export class HoverHighlightDirective implements OnInit {
@Input() highlightColor = 'yellow';
@Input() defaultColor = 'transparent';
@Input() animationDuration = '300ms';
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
ngOnInit(): void {
this.renderer.setStyle(
this.el.nativeElement,
'transition',
`background-color ${this.animationDuration} ease`
);
this.setBackgroundColor(this.defaultColor);
}
@HostListener('mouseenter') onMouseEnter(): void {
this.setBackgroundColor(this.highlightColor);
}
@HostListener('mouseleave') onMouseLeave(): void {
this.setBackgroundColor(this.defaultColor);
}
private setBackgroundColor(color: string): void {
this.renderer.setStyle(this.el.nativeElement, 'background-color', color);
}
}
Advanced Custom Directive with Multiple Inputs¶
// tooltip.directive.ts
import {
Directive,
ElementRef,
HostListener,
Input,
Renderer2,
ViewContainerRef,
ComponentRef,
OnDestroy
} from '@angular/core';
@Directive({
selector: '[appTooltip]'
})
export class TooltipDirective implements OnDestroy {
@Input() appTooltip = '';
@Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
@Input() tooltipDelay = 500;
@Input() tooltipClass = '';
private tooltipElement?: HTMLElement;
private showTimeout?: number;
private hideTimeout?: number;
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
@HostListener('mouseenter') onMouseEnter(): void {
this.clearTimeouts();
this.showTimeout = window.setTimeout(() => {
this.showTooltip();
}, this.tooltipDelay);
}
@HostListener('mouseleave') onMouseLeave(): void {
this.clearTimeouts();
this.hideTimeout = window.setTimeout(() => {
this.hideTooltip();
}, 100);
}
private showTooltip(): void {
if (this.tooltipElement || !this.appTooltip) return;
this.tooltipElement = this.renderer.createElement('div');
this.renderer.appendChild(this.tooltipElement,
this.renderer.createText(this.appTooltip));
// Add tooltip classes
this.renderer.addClass(this.tooltipElement, 'tooltip');
this.renderer.addClass(this.tooltipElement, `tooltip-${this.tooltipPosition}`);
if (this.tooltipClass) {
this.renderer.addClass(this.tooltipElement, this.tooltipClass);
}
// Position tooltip
this.renderer.setStyle(this.tooltipElement, 'position', 'absolute');
this.renderer.setStyle(this.tooltipElement, 'z-index', '1000');
this.renderer.setStyle(this.tooltipElement, 'background-color', '#333');
this.renderer.setStyle(this.tooltipElement, 'color', 'white');
this.renderer.setStyle(this.tooltipElement, 'padding', '8px 12px');
this.renderer.setStyle(this.tooltipElement, 'border-radius', '4px');
this.renderer.setStyle(this.tooltipElement, 'font-size', '14px');
this.renderer.setStyle(this.tooltipElement, 'white-space', 'nowrap');
this.renderer.appendChild(document.body, this.tooltipElement);
this.positionTooltip();
}
private positionTooltip(): void {
if (!this.tooltipElement) return;
const hostRect = this.el.nativeElement.getBoundingClientRect();
const tooltipRect = this.tooltipElement.getBoundingClientRect();
let top: number, left: number;
switch (this.tooltipPosition) {
case 'top':
top = hostRect.top - tooltipRect.height - 8;
left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;
break;
case 'bottom':
top = hostRect.bottom + 8;
left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;
break;
case 'left':
top = hostRect.top + (hostRect.height - tooltipRect.height) / 2;
left = hostRect.left - tooltipRect.width - 8;
break;
case 'right':
top = hostRect.top + (hostRect.height - tooltipRect.height) / 2;
left = hostRect.right + 8;
break;
}
this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
}
private hideTooltip(): void {
if (this.tooltipElement) {
this.renderer.removeChild(document.body, this.tooltipElement);
this.tooltipElement = undefined;
}
}
private clearTimeouts(): void {
if (this.showTimeout) {
clearTimeout(this.showTimeout);
this.showTimeout = undefined;
}
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
this.hideTimeout = undefined;
}
}
ngOnDestroy(): void {
this.clearTimeouts();
this.hideTooltip();
}
}
Creating Custom Structural Directives¶
Basic Structural Directive¶
// unless.directive.ts
import {
Directive,
Input,
TemplateRef,
ViewContainerRef
} from '@angular/core';
@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:
Advanced Structural Directive with Context¶
// repeat.directive.ts
import {
Directive,
Input,
TemplateRef,
ViewContainerRef,
EmbeddedViewRef
} from '@angular/core';
interface RepeatContext {
$implicit: number;
index: number;
first: boolean;
last: boolean;
even: boolean;
odd: boolean;
count: number;
}
@Directive({
selector: '[appRepeat]'
})
export class RepeatDirective {
private views: EmbeddedViewRef<RepeatContext>[] = [];
constructor(
private templateRef: TemplateRef<RepeatContext>,
private viewContainer: ViewContainerRef
) {}
@Input() set appRepeat(count: number) {
this.viewContainer.clear();
this.views = [];
for (let i = 0; i < count; i++) {
const context: RepeatContext = {
$implicit: i + 1,
index: i,
first: i === 0,
last: i === count - 1,
even: i % 2 === 0,
odd: i % 2 === 1,
count: count
};
const view = this.viewContainer.createEmbeddedView(
this.templateRef,
context
);
this.views.push(view);
}
}
}
Usage:
<div *appRepeat="5; let num; let i = index; let isFirst = first">
Item {{ num }} (index: {{ i }})
<span *ngIf="isFirst">(First item)</span>
</div>
Conditional Rendering with Loading State¶
// loading-if.directive.ts
import {
Directive,
Input,
TemplateRef,
ViewContainerRef,
OnDestroy
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
@Directive({
selector: '[appLoadingIf]'
})
export class LoadingIfDirective implements OnDestroy {
private hasView = false;
private subscription?: Subscription;
private loadingTemplateRef?: TemplateRef<any>;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appLoadingIf(observable: Observable<any>) {
this.subscription?.unsubscribe();
if (observable) {
this.showLoading();
this.subscription = observable.subscribe({
next: (data) => {
this.showContent();
},
error: (error) => {
this.showError(error);
}
});
}
}
@Input() set appLoadingIfLoadingTemplate(template: TemplateRef<any>) {
this.loadingTemplateRef = template;
}
private showLoading(): void {
this.viewContainer.clear();
this.hasView = false;
if (this.loadingTemplateRef) {
this.viewContainer.createEmbeddedView(this.loadingTemplateRef);
} else {
// Default loading template
const loadingElement = document.createElement('div');
loadingElement.textContent = 'Loading...';
loadingElement.className = 'loading-indicator';
this.viewContainer.element.nativeElement.appendChild(loadingElement);
}
}
private showContent(): void {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
}
private showError(error: any): void {
this.viewContainer.clear();
const errorElement = document.createElement('div');
errorElement.textContent = `Error: ${error.message || error}`;
errorElement.className = 'error-indicator';
this.viewContainer.element.nativeElement.appendChild(errorElement);
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
Directive Composition¶
Combining Multiple Directives¶
// Example component using multiple directives
@Component({
selector: 'app-directive-demo',
template: `
<div class="directive-demo">
<!-- Multiple attribute directives -->
<button
appHoverHighlight
highlightColor="lightblue"
appTooltip="Click to save changes"
tooltipPosition="top"
[ngClass]="{ 'save-button': true, 'disabled': isSaving }"
[disabled]="isSaving"
(click)="save()">
{{ isSaving ? 'Saving...' : 'Save' }}
</button>
<!-- Structural and attribute directives -->
<div *ngIf="showAdvanced"
appHighlight="lightyellow"
[ngStyle]="{ 'padding': '20px', 'margin': '10px' }">
<h3>Advanced Options</h3>
<div *ngFor="let option of advancedOptions; trackBy: trackByOption"
appHoverHighlight
highlightColor="lightgray">
{{ option.name }}: {{ option.value }}
</div>
</div>
<!-- Custom structural directive with attribute directives -->
<div *appRepeat="3; let num"
[ngClass]="'item-' + num"
appTooltip="Item number {{ num }}">
Repeated item {{ num }}
</div>
</div>
`
})
export class DirectiveDemoComponent {
isSaving = false;
showAdvanced = true;
advancedOptions = [
{ id: 1, name: 'Option A', value: 'enabled' },
{ id: 2, name: 'Option B', value: 'disabled' },
{ id: 3, name: 'Option C', value: 'pending' }
];
save(): void {
this.isSaving = true;
setTimeout(() => {
this.isSaving = false;
}, 2000);
}
trackByOption(index: number, option: any): number {
return option.id;
}
}
Directive Communication¶
// parent.directive.ts
import { Directive, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { ChildDirective } from './child.directive';
@Directive({
selector: '[appParent]'
})
export class ParentDirective implements AfterContentInit {
@ContentChildren(ChildDirective) children!: QueryList<ChildDirective>;
ngAfterContentInit(): void {
this.children.forEach((child, index) => {
child.setIndex(index);
child.setParent(this);
});
}
updateAllChildren(): void {
this.children.forEach(child => child.update());
}
}
// child.directive.ts
import { Directive, ElementRef, Renderer2 } from '@angular/core';
import { ParentDirective } from './parent.directive';
@Directive({
selector: '[appChild]'
})
export class ChildDirective {
private index = 0;
private parent?: ParentDirective;
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
setIndex(index: number): void {
this.index = index;
this.updateDisplay();
}
setParent(parent: ParentDirective): void {
this.parent = parent;
}
update(): void {
this.updateDisplay();
}
private updateDisplay(): void {
this.renderer.setAttribute(
this.el.nativeElement,
'data-index',
this.index.toString()
);
this.renderer.addClass(
this.el.nativeElement,
`child-${this.index}`
);
}
}
Best Practices¶
1. Use Renderer2 for DOM Manipulation¶
// ✅ Good: Using Renderer2
@Directive({
selector: '[appSafeHighlight]'
})
export class SafeHighlightDirective {
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
@Input() set appSafeHighlight(color: string) {
this.renderer.setStyle(this.el.nativeElement, 'background-color', color);
}
}
// ❌ Bad: Direct DOM manipulation
@Directive({
selector: '[appUnsafeHighlight]'
})
export class UnsafeHighlightDirective {
constructor(private el: ElementRef) {}
@Input() set appUnsafeHighlight(color: string) {
this.el.nativeElement.style.backgroundColor = color; // Not safe for SSR
}
}
2. Handle Edge Cases and Validation¶
@Directive({
selector: '[appValidatedInput]'
})
export class ValidatedInputDirective implements OnInit {
@Input() validationRules: string[] = [];
@Input() errorClass = 'validation-error';
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
ngOnInit(): void {
if (!this.isValidElement()) {
console.warn('ValidatedInputDirective can only be used on input elements');
return;
}
this.setupValidation();
}
@HostListener('blur') onBlur(): void {
this.validateInput();
}
private isValidElement(): boolean {
return this.el.nativeElement.tagName.toLowerCase() === 'input';
}
private setupValidation(): void {
// Setup validation logic
}
private validateInput(): void {
const value = this.el.nativeElement.value;
const isValid = this.runValidation(value);
if (isValid) {
this.renderer.removeClass(this.el.nativeElement, this.errorClass);
} else {
this.renderer.addClass(this.el.nativeElement, this.errorClass);
}
}
private runValidation(value: string): boolean {
// Implementation of validation logic
return true;
}
}
3. Proper Cleanup¶
@Directive({
selector: '[appCleanupDirective]'
})
export class CleanupDirective implements OnInit, OnDestroy {
private subscription = new Subscription();
private resizeListener?: () => void;
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
ngOnInit(): void {
// Setup event listeners
this.resizeListener = this.renderer.listen('window', 'resize', () => {
this.onResize();
});
// Setup subscriptions
this.subscription.add(
// Add your observables here
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
if (this.resizeListener) {
this.resizeListener();
}
}
private onResize(): void {
// Handle resize
}
}
Common Use Cases¶
1. Form Field Enhancements¶
@Directive({
selector: '[appAutoFocus]'
})
export class AutoFocusDirective implements AfterViewInit {
@Input() autoFocusDelay = 0;
constructor(private el: ElementRef) {}
ngAfterViewInit(): void {
setTimeout(() => {
this.el.nativeElement.focus();
}, this.autoFocusDelay);
}
}
@Directive({
selector: '[appNumericOnly]'
})
export class NumericOnlyDirective {
@HostListener('keydown', ['$event']) onKeyDown(event: KeyboardEvent): void {
const allowedKeys = ['Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'ArrowLeft', 'ArrowRight'];
if (allowedKeys.includes(event.key) ||
(event.key >= '0' && event.key <= '9') ||
(event.ctrlKey && ['a', 'c', 'v', 'x'].includes(event.key))) {
return;
}
event.preventDefault();
}
}
2. Accessibility Enhancements¶
@Directive({
selector: '[appA11yClick]'
})
export class A11yClickDirective {
@HostListener('keydown', ['$event']) onKeyDown(event: KeyboardEvent): void {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.el.nativeElement.click();
}
}
constructor(private el: ElementRef, private renderer: Renderer2) {
// Make element focusable if it's not already
if (!this.el.nativeElement.hasAttribute('tabindex')) {
this.renderer.setAttribute(this.el.nativeElement, 'tabindex', '0');
}
// Add appropriate role if not present
if (!this.el.nativeElement.hasAttribute('role')) {
this.renderer.setAttribute(this.el.nativeElement, 'role', 'button');
}
}
}
3. Performance Optimizations¶
@Directive({
selector: '[appLazyLoad]'
})
export class LazyLoadDirective implements OnInit, OnDestroy {
@Input() appLazyLoad = '';
private observer?: IntersectionObserver;
constructor(private el: ElementRef) {}
ngOnInit(): void {
if ('IntersectionObserver' in window) {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadContent();
this.observer?.unobserve(this.el.nativeElement);
}
});
});
this.observer.observe(this.el.nativeElement);
} else {
// Fallback for browsers without IntersectionObserver
this.loadContent();
}
}
ngOnDestroy(): void {
if (this.observer) {
this.observer.disconnect();
}
}
private loadContent(): void {
if (this.appLazyLoad && this.el.nativeElement.tagName === 'IMG') {
this.el.nativeElement.src = this.appLazyLoad;
}
}
}
Summary¶
Angular directives are powerful tools for extending HTML functionality:
Structural Directives:
- Use
*syntax for shorthand - Modify DOM structure
- Examples:
*ngIf,*ngFor,*ngSwitch
Attribute Directives:
- Modify appearance or behavior
- Examples:
ngClass,ngStyle
Custom Directives:
- Use
@Directivedecorator - Leverage
ElementRefandRenderer2for safe DOM manipulation - Implement proper lifecycle management
- Handle edge cases and validation
Best Practices:
- Use Renderer2 for DOM manipulation
- Implement proper cleanup in
ngOnDestroy - Validate inputs and handle edge cases
- Follow accessibility guidelines
- Consider performance implications
Next, we'll explore Angular pipes and how they help transform and format data in templates.