Skip to main content

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