Angular Directives
What are Directives?
Directives are classes that add behavior to elements in Angular applications. There are three types of directives:
- Components - Directives with templates
- Structural Directives - Change DOM layout (
*ngIf,*ngFor) - 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
| Feature | Directive | Component |
|---|---|---|
| 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.