Directives
Types of Directives
- Structural: Modify DOM structure (
*ngIf,*ngFor,*ngSwitch) - Attribute: Modify element appearance/behavior (
ngClass,ngStyle) - Custom: Create your own reusable directives
*ngIf - Conditional Rendering
Basic usage:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-player-status',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="isPlaying">
<p>🎵 Now Playing...</p>
</div>
<button (click)="isPlaying = !isPlaying">Toggle Playback</button>
`
})
export class PlayerStatusComponent {
isPlaying = false;
}
With else:
<div *ngIf="album; else loading">
<h2>{{ album.name }}</h2>
<p>{{ album.artist }}</p>
</div>
<ng-template #loading>
<p>Loading album data...</p>
</ng-template>
With then/else:
<div *ngIf="isPremium; then premiumContent else freeContent"></div>
<ng-template #premiumContent>
<p>🎧 Premium Subscription - Unlimited Downloads</p>
</ng-template>
<ng-template #freeContent>
<p>🎵 Free Tier - Stream Only</p>
</ng-template>
Store result (as):
<div *ngIf="album$ | async as album">
<p>{{ album.name }}</p>
<p>{{ album.artist }}</p>
</div>
*ngFor - List Rendering
Basic usage:
@Component({
selector: 'app-track-list',
standalone: true,
imports: [CommonModule],
template: `
<ul>
<li *ngFor="let track of tracks">
{{ track.title }}
</li>
</ul>
`
})
export class TrackListComponent {
tracks = [
{ id: 1, title: 'Bohemian Rhapsody' },
{ id: 2, title: 'Stairway to Heaven' },
{ id: 3, title: 'Hotel California' }
];
}
With index:
<li *ngFor="let track of tracks; let i = index">
{{ i + 1 }}. {{ track.title }}
</li>
With trackBy for performance:
@Component({
selector: 'app-album-list',
standalone: true,
imports: [CommonModule],
template: `
<div *ngFor="let album of albums; trackBy: trackByAlbumId">
{{ album.name }}
</div>
`
})
export class AlbumListComponent {
albums = [
{ id: 1, name: 'Dark Side of the Moon', artist: 'Pink Floyd' },
{ id: 2, name: 'Abbey Road', artist: 'The Beatles' }
];
// Tracks items by ID, prevents unnecessary re-renders
trackByAlbumId(index: number, album: any): number {
return album.id;
}
}
*All ngFor variables:
<li *ngFor="let track of tracks;
let i = index;
let first = first;
let last = last;
let even = even;
let odd = odd">
Track {{ i }}: {{ track }}
<span *ngIf="first">(Opening Track)</span>
<span *ngIf="last">(Closing Track)</span>
</li>
*ngSwitch - Multiple Conditions
@Component({
selector: 'app-player-status',
standalone: true,
imports: [CommonModule],
template: `
<div [ngSwitch]="status">
<p *ngSwitchCase="'playing'" class="text-green">▶️ Playing</p>
<p *ngSwitchCase="'paused'" class="text-yellow">⏸️ Paused</p>
<p *ngSwitchCase="'stopped'" class="text-gray">⏹️ Stopped</p>
<p *ngSwitchDefault class="text-red">Unknown Status</p>
</div>
<button (click)="changeStatus()">Change Player Status</button>
`
})
export class PlayerStatusComponent {
status: string = 'playing';
changeStatus() {
const statuses = ['playing', 'paused', 'stopped', 'unknown'];
const current = statuses.indexOf(this.status);
this.status = statuses[(current + 1) % statuses.length];
}
}
ngClass - Dynamic CSS Classes
Object syntax:
@Component({
selector: 'app-play-button',
standalone: true,
imports: [CommonModule],
template: `
<button
[ngClass]="{
'btn-playing': isPlaying,
'btn-large': isLarge,
'btn-disabled': isDisabled
}">
▶️ Play Track
</button>
`,
styles: [`
.btn-playing { background: green; color: white; }
.btn-large { font-size: 18px; padding: 12px; }
.btn-disabled { opacity: 0.5; cursor: not-allowed; }
`]
})
export class PlayButtonComponent {
isPlaying = true;
isLarge = false;
isDisabled = false;
}
Array syntax:
<div [ngClass]="['class1', 'class2', 'class3']">
Content
</div>
String syntax:
<div [ngClass]="'class1 class2'">
Content
</div>
Method syntax:
@Component({
template: `
<div [ngClass]="getClasses()">Content</div>
`
})
export class MyComponent {
getClasses() {
return {
'active': this.isActive,
'highlighted': this.isHighlighted
};
}
}
ngStyle - Dynamic Inline Styles
Object syntax:
@Component({
selector: 'app-track-title',
standalone: true,
imports: [CommonModule],
template: `
<p [ngStyle]="{
'color': textColor,
'font-size': fontSize + 'px',
'font-weight': isBold ? 'bold' : 'normal',
'background-color': bgColor
}">
🎵 Now Playing: Bohemian Rhapsody
</p>
<input [(ngModel)]="textColor" placeholder="Color">
<input type="number" [(ngModel)]="fontSize">
`
})
export class TrackTitleComponent {
textColor = 'blue';
fontSize = 16;
isBold = false;
bgColor = '#f0f0f0';
}
Custom Attribute Directive
Modern signal-based directive:
import { Directive, ElementRef, input, effect, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true,
host: {
'(mouseenter)': 'onMouseEnter()',
'(mouseleave)': 'onMouseLeave()'
}
})
export class HighlightDirective {
private el = inject(ElementRef);
// Signal-based inputs
appHighlight = input<string>('yellow');
defaultColor = input<string>('transparent');
constructor() {
// Set default color on init
effect(() => {
this.el.nativeElement.style.backgroundColor = this.defaultColor();
});
}
onMouseEnter() {
this.highlight(this.appHighlight());
}
onMouseLeave() {
this.highlight(this.defaultColor());
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
Usage in component:
@Component({
selector: 'app-playlist',
standalone: true,
imports: [HighlightDirective],
template: `
<p appHighlight>🎵 Track 1: Bohemian Rhapsody</p>
<p [appHighlight]="'lightblue'">🎵 Track 2: Stairway to Heaven</p>
<p [appHighlight]="'pink'" [defaultColor]="'lightgray'">
🎵 Track 3: Hotel California
</p>
`
})
export class PlaylistComponent {}
Custom Structural Directive
*Unless directive (opposite of ngIf):
import { Directive, input, effect, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appUnless]',
standalone: true
})
export class UnlessDirective {
// Signal-based input
appUnless = input<boolean>(false);
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {
// Effect reacts to condition changes
effect(() => {
const condition = this.appUnless();
if (!condition) {
// Show content when condition is FALSE
if (this.viewContainer.length === 0) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
} else {
// Remove content when condition is TRUE
this.viewContainer.clear();
}
});
}
}
Usage:
@Component({
selector: 'app-track-display',
standalone: true,
imports: [UnlessDirective],
template: `
<p *appUnless="isHidden">
🎵 Now Playing: Bohemian Rhapsody - Queen
</p>
<button (click)="isHidden = !isHidden">Toggle Track Display</button>
`
})
export class TrackDisplayComponent {
isHidden = false;
}
Practical Custom Directives
Click Outside Directive
import { Directive, ElementRef, output, inject } from '@angular/core';
@Directive({
selector: '[appClickOutside]',
standalone: true,
host: {
'(document:click)': 'onClick($event.target)'
}
})
export class ClickOutsideDirective {
private elementRef = inject(ElementRef);
// Signal-based output
clickOutside = output<void>();
onClick(target: HTMLElement) {
const clickedInside = this.elementRef.nativeElement.contains(target);
if (!clickedInside) {
this.clickOutside.emit();
}
}
}
Usage:
@Component({
template: `
<div class="genre-filter"
appClickOutside
(clickOutside)="closeFilter()">
<button (click)="isOpen = !isOpen">🎸 Genre Filter</button>
<div *ngIf="isOpen" class="menu">
<p>🎸 Rock</p>
<p>🎹 Jazz</p>
<p>🎤 Pop</p>
</div>
</div>
`
})
export class GenreFilterComponent {
isOpen = false;
closeFilter() {
this.isOpen = false;
}
}
Auto-focus Directive
import { Directive, ElementRef, OnInit, inject } from '@angular/core';
@Directive({
selector: '[appAutoFocus]',
standalone: true
})
export class AutoFocusDirective implements OnInit {
private el = inject(ElementRef);
ngOnInit() {
setTimeout(() => {
this.el.nativeElement.focus();
}, 0);
}
}
Usage:
<input appAutoFocus placeholder="Search for tracks...">
Debounce Click Directive
import { Directive, input, output } from '@angular/core';
@Directive({
selector: '[appDebounceClick]',
standalone: true,
host: {
'(click)': 'onClick($event)'
}
})
export class DebounceClickDirective {
// Signal-based inputs/outputs
debounceTime = input<number>(500);
debounceClick = output<Event>();
private timeout: any;
onClick(event: Event) {
event.preventDefault();
event.stopPropagation();
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.debounceClick.emit(event);
}, this.debounceTime());
}
}
Usage:
<button
appDebounceClick
[debounceTime]="1000"
(debounceClick)="addToFavorites()">
⭐ Add to Favorites (debounced)
</button>
Best Practices
- Use structural directives for DOM manipulation
- Use attribute directives for styling/behavior
- Prefer built-in directives over custom when possible
- Use trackBy with *ngFor for performance
- Keep custom directives focused (single responsibility)
- Use HostListener for event handling
- Use HostBinding for property binding
- Make custom directives reusable and configurable
- Use standalone: true for all new directives