Skip to main content

Angular Directives

What are Directives?

Directives are classes that add behavior to elements in Angular applications. There are three types of directives:

  1. Components - Directives with templates
  2. Structural Directives - Change DOM layout (*ngIf, *ngFor)
  3. Attribute Directives - Change appearance or behavior (ngClass, ngStyle)

Built-in Attribute Directives

ngClass

Dynamically add/remove CSS classes.

@Component({
selector: 'app-status',
standalone: true,
imports: [CommonModule],
template: `
<!-- Object syntax -->
<div
[ngClass]="{
active: isActive,
disabled: isDisabled,
error: hasError
}"
>
Status
</div>

<!-- Array syntax -->
<div [ngClass]="['btn', btnType, size]">Button</div>

<!-- String -->
<div [ngClass]="statusClasses">Status</div>
`,
})
export class StatusComponent {
isActive = true;
isDisabled = false;
hasError = false;

btnType = 'btn-primary';
size = 'btn-lg';

statusClasses = 'active premium highlighted';
}

ngStyle

Dynamically set inline styles.

@Component({
selector: 'app-box',
standalone: true,
imports: [CommonModule],
template: `
<div
[ngStyle]="{
'background-color': bgColor,
'width.px': width,
'height.px': height,
'border-radius.px': borderRadius,
display: display
}"
>
Styled Box
</div>
`,
})
export class BoxComponent {
bgColor = '#3b82f6';
width = 200;
height = 100;
borderRadius = 8;
display = 'flex';
}

ngModel

Two-way data binding for form inputs.

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
selector: 'app-form',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="username" placeholder="Username" />
<p>Hello, {{ username }}!</p>

<input type="checkbox" [(ngModel)]="agreed" />
<label>I agree</label>
`,
})
export class FormComponent {
username = '';
agreed = false;
}

Custom Attribute Directives

Create your own directives to extend HTML behavior.

Basic Custom Directive

// highlight.directive.ts
import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
selector: '[appHighlight]',
standalone: true,
})
export class HighlightDirective implements OnInit {
constructor(private el: ElementRef) {}

ngOnInit() {
this.el.nativeElement.style.backgroundColor = 'yellow';
}
}

// Usage
@Component({
selector: 'app-root',
standalone: true,
imports: [HighlightDirective],
template: `<p appHighlight>This text is highlighted</p>`,
})
export class AppComponent {}

Directive with Input

// highlight.directive.ts
import { Directive, ElementRef, Input, OnInit } from '@angular/core';

@Directive({
selector: '[appHighlight]',
standalone: true,
})
export class HighlightDirective implements OnInit {
@Input() appHighlight = 'yellow'; // Default color
@Input() textColor = 'black';

constructor(private el: ElementRef) {}

ngOnInit() {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
this.el.nativeElement.style.color = this.textColor;
}
}

// Usage
@Component({
template: `
<p appHighlight>Default yellow</p>
<p [appHighlight]="'lightblue'">Light blue</p>
<p [appHighlight]="'pink'" [textColor]="'white'">Pink with white text</p>
`,
})
export class AppComponent {}

Directive with HostListener

Respond to host element events.

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
selector: '[appHoverHighlight]',
standalone: true
})
export class HoverHighlightDirective {
@Input() appHoverHighlight = 'yellow';
@Input() defaultColor = 'transparent';

constructor(private el: ElementRef) {}

@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHoverHighlight);
}

@HostListener('mouseleave') onMouseLeave() {
this.highlight(this.defaultColor);
}

private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}

// Usage
<p [appHoverHighlight]="'lightblue'">Hover over me!</p>

Directive with HostBinding

Bind to host element properties.

import { Directive, HostBinding, HostListener } from '@angular/core';

@Directive({
selector: '[appDropdown]',
standalone: true,
})
export class DropdownDirective {
@HostBinding('class.open') isOpen = false;

@HostListener('click') toggle() {
this.isOpen = !this.isOpen;
}
}

// Usage
<div class='dropdown' appDropdown>
<button>Toggle</button>
<ul class='dropdown-menu'>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>;

Custom Structural Directives

Create directives that modify DOM structure.

Basic Structural Directive

// unless.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
selector: '[appUnless]',
standalone: true
})
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
<p *appUnless="false">This is shown (unless false)</p>
<p *appUnless="true">This is hidden (unless true)</p>

Repeat Directive

// repeat.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
selector: '[appRepeat]',
standalone: true
})
export class RepeatDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}

@Input() set appRepeat(times: number) {
this.viewContainer.clear();
for (let i = 0; i < times; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: i,
index: i
});
}
}
}

// Usage
<div *appRepeat="5; let i">
Item {{ i }}
</div>

Real-World Directive Examples

Auto-focus Directive

import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
selector: '[appAutofocus]',
standalone: true
})
export class AutofocusDirective implements OnInit {
constructor(private el: ElementRef) {}

ngOnInit() {
setTimeout(() => {
this.el.nativeElement.focus();
}, 0);
}
}

// Usage
<input appAutofocus type="text" placeholder="Auto-focused">

Click Outside Directive

import {
Directive,
ElementRef,
Output,
EventEmitter,
HostListener,
} from '@angular/core';

@Directive({
selector: '[appClickOutside]',
standalone: true,
})
export class ClickOutsideDirective {
@Output() clickOutside = new EventEmitter<void>();

constructor(private elementRef: ElementRef) {}

@HostListener('document:click', ['$event.target'])
onClick(target: HTMLElement) {
const clickedInside = this.elementRef.nativeElement.contains(target);
if (!clickedInside) {
this.clickOutside.emit();
}
}
}

// Usage
@Component({
template: `
<div class="modal" appClickOutside (clickOutside)="closeModal()">
<p>Click outside to close</p>
</div>
`,
})
export class ModalComponent {
closeModal() {
console.log('Clicked outside!');
}
}

Debounce Click Directive

import { Directive, EventEmitter, HostListener, Input, Output, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

@Directive({
selector: '[appDebounceClick]',
standalone: true
})
export class DebounceClickDirective implements OnDestroy {
@Input() debounceTime = 500;
@Output() debounceClick = new EventEmitter();

private clicks = new Subject<Event>();
private subscription: Subscription;

constructor() {
this.subscription = this.clicks
.pipe(debounceTime(this.debounceTime))
.subscribe(e => this.debounceClick.emit(e));
}

@HostListener('click', ['$event'])
clickEvent(event: Event) {
event.preventDefault();
event.stopPropagation();
this.clicks.next(event);
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
}

// Usage
<button
appDebounceClick
[debounceTime]="1000"
(debounceClick)="handleClick()"
>
Save (debounced)
</button>

Permission Directive

import { Directive, Input, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';

@Directive({
selector: '[appHasPermission]',
standalone: true
})
export class HasPermissionDirective implements OnInit {
@Input() appHasPermission: string[] = [];
private userPermissions = ['read', 'write']; // From auth service

constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}

ngOnInit() {
const hasPermission = this.appHasPermission.some(
permission => this.userPermissions.includes(permission)
);

if (hasPermission) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}

// Usage
<button *appHasPermission="['admin', 'write']">
Edit
</button>

Lazy Load Image Directive

import { Directive, ElementRef, Input, OnInit } from '@angular/core';

@Directive({
selector: '[appLazyLoad]',
standalone: true
})
export class LazyLoadDirective implements OnInit {
@Input() appLazyLoad!: string;

constructor(private el: ElementRef) {}

ngOnInit() {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage();
observer.disconnect();
}
});
});

observer.observe(this.el.nativeElement);
}

private loadImage() {
this.el.nativeElement.src = this.appLazyLoad;
}
}

// Usage
<img [appLazyLoad]="imageUrl" alt="Lazy loaded image">

Best Practices

✅ Do

// Use descriptive selector names
@Directive({ selector: '[appTooltip]' })

// Use HostListener for events
@HostListener('click') onClick() {}

// Use HostBinding for properties
@HostBinding('class.active') isActive = true;

// Clean up subscriptions
ngOnDestroy() {
this.subscription.unsubscribe();
}

// Provide default values for inputs
@Input() color = 'blue';

❌ Don't

// Don't directly manipulate DOM outside directive
// ❌ Bad
document.getElementById('myElement').style.color = 'red';

// ✅ Good
this.el.nativeElement.style.color = 'red';

// Don't forget standalone: true
@Directive({
selector: '[appMyDirective]',
standalone: true // Don't forget this!
})

// Don't create memory leaks
// Always unsubscribe in ngOnDestroy

Directive vs Component

FeatureDirectiveComponent
Has template❌ No✅ Yes
Has styles❌ No✅ Yes
Has view❌ No✅ Yes
Modifies behavior✅ Yes✅ Yes
Can be structural✅ Yes❌ No

Interview Questions

Q: What's the difference between structural and attribute directives? A: Structural directives change DOM structure (add/remove elements) using * syntax. Attribute directives change appearance/behavior without changing DOM structure.

Q: How do you create a custom structural directive? A: Inject TemplateRef and ViewContainerRef, then use createEmbeddedView() and clear() to add/remove the template from the view.

Q: What is HostListener used for? A: HostListener listens to events on the host element the directive is applied to.

Q: When should you use a directive instead of a component? A: Use directives to add behavior to existing elements without creating new DOM elements. Use components when you need a template and encapsulated view.

Next Steps