What are directives and what types exist in Angular?
Angular directive is a class decorated with @Directive that Angular uses to extend or transform HTML elements during template compilation.
Theory
TL;DR
- Directives are like HTML plugins: attach one to any element to add styles, events, or DOM changes without touching the element itself
- 3 types: Attribute (decorates existing elements), Structural (adds or removes DOM nodes), Component (directive with its own template)
- Attribute selector:
[appHighlight]. Structural uses the*prefix:*ngIf. Component uses a tag:<app-card> - Need reusable DOM behavior? Directive. Need a UI block with markup? Component.
Quick Example
The classic appHighlight attribute directive shows the full pattern in about 12 lines:
<div appHighlight>Hover me</div>import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.backgroundColor = 'yellow';
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.backgroundColor = null;
}
}Angular matches the [appHighlight] attribute to the selector, injects ElementRef, and @HostListener wires the mouse events. No template needed.
Key Difference
Attribute directives decorate an existing element: they change its style, class, or behavior but leave the DOM structure alone. Structural directives reshape the DOM. *ngIf removes an element entirely when the condition is false. *ngFor clones a template node for each item in an array. Components are technically directives too, just with an added template and view encapsulation. That separation lets Angular skip template compilation for attribute directives entirely, while structural ones trigger ViewContainerRef manipulation on every change.
When to Use
- Changing styles or classes on an element: Attribute directive (
ngClass,ngStyle, custom validator styling) - Conditional rendering or iteration: Structural (
*ngIf,*ngFor,*ngSwitch) - Reusable UI with its own markup: Component, not a directive
- Attaching event handlers to a host element: Attribute with
@HostListeneror@HostBinding - Logic with no DOM dependency at all: Service, not a directive
Directive Types
| Type | Selector syntax | DOM impact | Key Angular APIs | Built-in examples |
|---|---|---|---|---|
| Attribute | [appHighlight] | None (decorates host element) | ElementRef, HostListener, HostBinding | ngClass, ngStyle |
| Structural | *appUnless, *ngIf | Adds or removes elements | ViewContainerRef, TemplateRef | *ngIf, *ngFor, *ngSwitch |
| Component | <app-user-card> | Renders its own template | @Component (extends @Directive) | Any custom UI block |
How Angular Processes Directives
During AOT compilation Angular scans templates, matches selectors to @Directive metadata, and generates factory functions. At runtime the framework creates directive instances via dependency injection and runs lifecycle hooks in order: ngOnInit, then ngAfterViewInit. For structural directives, any state change calls createEmbeddedView() or clear() on ViewContainerRef. That is how *ngIf removes and re-inserts DOM nodes without a full re-render. Attribute directives skip that step completely, which is why they cost less inside large lists.
Common Mistakes
1. Direct nativeElement access instead of Renderer2
// Breaks SSR (Angular Universal)
constructor(el: ElementRef) {
el.nativeElement.style.color = 'red';
}
// Correct: Renderer2 works in both browser and server contexts
constructor(private el: ElementRef, private renderer: Renderer2) {
renderer.setStyle(el.nativeElement, 'color', 'red');
}Direct nativeElement manipulation causes hydration mismatches in Angular Universal. Renderer2 abstracts the DOM so Angular handles both environments correctly.
2. Forgetting TemplateRef and ViewContainerRef in structural directives
// The template never renders
@Directive({ selector: '[appIf]' })
export class AppIfDirective {
@Input() appIf: boolean; // Angular has nowhere to insert the template
}
// Correct
export class AppIfDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appIf(condition: boolean) {
condition
? this.viewContainer.createEmbeddedView(this.templateRef)
: this.viewContainer.clear();
}
}This is the most common "directive not rendering" bug. The * syntax is syntactic sugar. Angular still needs ViewContainerRef to physically insert the template into the DOM.
3. Multiple structural directives on one element
<!-- Angular applies only the first structural directive, the second is ignored -->
<div *ngFor="let item of items" *ngIf="item.active">{{ item.name }}</div>
<!-- Correct: nest them -->
<div *ngFor="let item of items">
<span *ngIf="item.active">{{ item.name }}</span>
</div>4. Missing $event in @HostListener
@HostListener('click') onClick() {} // No access to coordinates or target
@HostListener('click', ['$event']) onClick(event: MouseEvent) {
console.log(event.target); // Works as expected
}Real-World Usage
- Angular Material:
matTooltipadds hover tips to any element via a single attribute - NG Bootstrap:
ngbTooltipbinds the same way to Bootstrap CSS classes - PrimeNG:
pTooltipused in enterprise data table cells - Nx workspaces: custom structural
*nxLoadingfor lazy module loading states - Reactive Forms: custom attribute validators integrate directly with Angular's
NgFormcontrol system
Follow-up Questions
Q: What is the difference between @Directive and @Component?
A: @Component extends @Directive and adds a template, styles, and view encapsulation. A component is a directive that knows how to render itself. A plain @Directive has no template and no associated view.
Q: How does *ngFor use trackBy and why does it matter?
A: trackBy takes a function that returns a unique identifier per item. Angular reuses existing DOM nodes when the array changes instead of destroying and recreating every element. Without it, any mutation like sort, filter, or push rebuilds the entire list in the DOM.
Q: Build a custom structural directive. What do you need?
A: Inject TemplateRef<any> and ViewContainerRef in the constructor. Add an @Input setter. When the condition is true, call this.viewContainer.createEmbeddedView(this.templateRef). When false, call this.viewContainer.clear(). Use it in templates as *appDirectiveName="expression".
Q: Why use @HostBinding instead of a [style.color] binding in the template?
A: @HostBinding lives inside the directive class, works correctly in inheritance chains, and is SSR-safe. Template bindings require you to control the template, which is not always the case for a shared directive library.
Q: What is the performance cost of directives in large lists?
A: Structural directives recreate views on every change detection pass if the parent uses the default strategy. Attribute directives are lighter, but @HostListener callbacks fire on every matching event regardless. Fix: ChangeDetectionStrategy.OnPush on the parent and trackBy in every *ngFor. Angular 17 signals reduce unnecessary checks further by reacting only to specific state changes.
Examples
Basic: SSR-safe hover highlight
<p appHighlight>Hover to highlight</p>import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {}
@HostListener('mouseenter') onEnter() {
// Renderer2 keeps this safe for Angular Universal
this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'yellow');
}
@HostListener('mouseleave') onLeave() {
this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor');
}
}Renderer2 instead of direct nativeElement access is what separates a quick prototype from a production directive. The same attribute works on any element in any template.
Intermediate: Custom structural directive
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;
}
}
}<div *appUnless="isLoading">Content is ready</div>The hasView flag prevents duplicate renders when the input changes rapidly. This is exactly the pattern behind *ngIf. I've seen this exercise come up in senior Angular interviews at mid-size product companies, and the hasView guard is usually what separates a passing answer from a strong one.
Advanced: Structural directive with async input
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appUnlessLoading]'
})
export class UnlessLoadingDirective {
private hasView = false;
@Input() set appUnlessLoading(loading: Promise<boolean> | boolean) {
if (typeof loading === 'boolean') {
this.updateView(loading);
} else {
// Async resolution: Promise resolves to final loading state
loading.then(result => this.updateView(result));
}
}
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
private updateView(loading: boolean) {
if (!loading && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (loading && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}<div *appUnlessLoading="fetchingUsers$ | async">Users are loaded</div>The real gotcha: without the hasView guard, rapid async changes call createEmbeddedView multiple times and leak duplicate DOM nodes into the view. clear() removes all views before creating a new one, but checking hasView first avoids the unnecessary DOM work entirely.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.