Directive
Understand how Angular directives add reusable behavior to existing elements, control DOM structure, expose inputs and outputs, and interact safely with host elements.
Lesson content
What is an Angular Directive? A directive is an Angular class that adds behavior to an existing HTML element, component, or template. Unlike a component, a directive normally does not define its own view. Simple Definition Directive = Reusable DOM Behavior Directive Flow Template Element ↓ Directive Selector Matches ↓ Directive Instance Created ↓ Host Behavior Applied Types of Angular Directives Type Purpose Example Component directive Creates a reusable view @Component() Attribute directive Changes behavior or appearance [appHighlight] Structural directive Adds or removes template views *ngIf , *ngFor A component is technically a directive with its own template. 1. Attribute Directive An attribute directive changes the behavior, appearance, properties, attributes, classes, or events of an existing element. Basic Highlight Directive import { Directive } from '@angular/core'; @Directive({ selector: '[appHighlight]', standalone: true, host: { 'style.backgroundColor': '"yellow"' } }) export class HighlightDirective {} Usage <p appHighlight> Highlighted paragraph </p> Result Existing Paragraph + appHighlight Directive ↓ Highlighted Paragraph @Directive Decorator The @Directive() decorator tells Angular that a TypeScript class defines reusable directive behavior. @Directive({ selector: '[appAutofocus]', standalone: true }) export class AutofocusDirective {} Directive Selector Directives usually use attribute selectors. selector: '[appHighlight]' Usage <div appHighlight></div> Element-Specific Selector @Directive({ selector: 'button[appSave]', standalone: true }) export class SaveButtonDirective {} This directive applies only to buttons containing the appSave attribute. <button appSave> Save </button> Using the host Property The host metadata property connects directive state and behavior to the host element. It can define: Property bindings Attribute bindings Class bindings Style bindings Event listeners Host Class Binding import { Directive, signal } from '@angular/core'; @Directive({ selector: '[appSelectable]', standalone: true, host: { '[class.selected]': 'selected()', '(click)': 'toggle()' } }) export class SelectableDirective { selected = signal(false); toggle() { this.selected.update( value => !value ); } } Usage <div appSelectable> Select this item </div> Host Style Binding @Directive({ selector: '[appTextColor]', standalone: true, host: { '[style.color]': 'color()' } }) export class TextColorDirective { color = input('blue'); } Usage <p appTextColor color="red" > Red text </p> Directive Inputs A directive input allows the parent template to configure directive behavior. import { Directive, input } from '@angular/core'; @Directive({ selector: '[appHighlight]', standalone: true, host: { '[style.backgroundColor]': 'appHighlight()' } }) export class HighlightDirective { appHighlight = input('yellow'); } Usage <p appHighlight="lightblue"> Highlighted text </p> Using the selector name as the input name allows compact template syntax. Multiple Directive Inputs import { Directive, input } from '@angular/core'; @Directive({ selector: '[appBorder]', standalone: true, host: { '[style.borderColor]': 'color()', '[style.borderWidth.px]': 'width()', '[style.borderStyle]': '"solid"' } }) export class BorderDirective { color = input('black'); width = input(1); } Usage <div appBorder color="red" [width]="3" > Bordered content </div> Directive Outputs A directive can emit events to its parent using output() . import { Directive, output } from '@angular/core'; @Directive({ selector: '[appDoubleClick]', standalone: true, host: { '(dblclick)': 'handleDoubleClick()' } }) export class DoubleClickDirective { activated = output<void>(); handleDoubleClick() { this.activated.emit(); } } Usage <button appDoubleClick (activated)="openEditor()" > Edit </button> Accessing the Host Element Use ElementRef when direct access to the directive's host element is necessary. import { Directive, ElementRef, inject } from '@angular/core'; @Directive({ selector: '[appFocus]', standalone: true }) export class FocusDirective { private element = inject( ElementRef<HTMLInputElement> ); focus() { this.element.nativeElement.focus(); } } Prefer host bindings for classes, attributes, styles, and events. Use direct DOM access only when the required operation cannot be expressed declaratively. Renderer2 Renderer2 provides an abstraction for performing DOM operations. import { Directive, ElementRef, inject, Renderer2 } from '@angular/core'; @Directive({ selector: '[appSafeHighlight]', standalone: true }) export class SafeHighlightDirective { private element = inject(ElementRef); private renderer = inject(Renderer2); constructor() { this.renderer.setStyle( this.element.nativeElement, 'background-color', 'yellow' ); } } For simple styles and classes, prefer the directive's host metadata instead. Autofocus Directive import { afterNextRender, Directive, ElementRef, inject } from '@angular/core'; @Directive({ selector: '[appAutofocus]', standalone: true }) export class AutofocusDirective { private element = inject( ElementRef<HTMLElement> ); constructor() { afterNextRender(() => { this.element.nativeElement .focus(); }); } } Usage <input appAutofocus /> Permission Directive A directive can disable or hide functionality based on user permissions. import { Directive, computed, inject, input } from '@angular/core'; @Directive({ selector: '[appPermission]', standalone: true, host: { '[disabled]': '!allowed()', '[attr.aria-disabled]': '!allowed()' } }) export class PermissionDirective { private authService = inject(AuthService); appPermission = input.required<string>(); allowed = computed(() => this.authService .hasPermission( this.appPermission() ) ); } Usage <button appPermission="users.delete" > Delete User </button> A frontend permission directive only controls the user interface. The backend must still validate authorization. Directive Composition Angular allows a component or directive to reuse behavior from other standalone directives through hostDirectives . Reusable Directive @Directive({ selector: '[appTooltip]', standalone: true }) export class TooltipDirective { text = input(''); } Compose It into a Component @Component({ selector: 'app-help-button', standalone: true, hostDirectives: [ { directive: TooltipDirective, inputs: [ 'text: tooltipText' ] } ], template: ` <button>Help</button> ` }) export class HelpButtonComponent {} Usage <app-help-button tooltipText="Open help" ></app-help-button> 2. Structural Directives A structural directive changes the DOM structure by creating, removing, or repeating embedded views. Common Structural Directives *ngIf *ngFor *ngSwitchCase Modern Angular templates generally prefer built-in control-flow blocks: @if @for @switch Structural Directive Syntax <p *appIf="isVisible"> Visible content </p> The asterisk is shorthand for an underlying ng-template . Expanded Form <ng-template [appIf]="isVisible" > <p>Visible content</p> </ng-template> Custom Structural Directive A structural directive commonly uses: TemplateRef – Represents the template content ViewContainerRef – Creates or removes embedded views Custom appIf Directive import { Directive, effect, inject, input, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appIf]', standalone: true }) export class IfDirective { appIf = input.required<boolean>(); private template = inject(TemplateRef<unknown>); private container = inject(ViewContainerRef); private hasView = false; constructor() { effect(() => { const shouldDisplay = this.appIf(); if ( shouldDisplay && !this.hasView ) { this.container .createEmbeddedView( this.template ); this.hasView = true; } if ( !shouldDisplay && this.hasView ) { this.container.clear(); this.hasView = false; } }); } } Usage <p *appIf="isLoggedIn"> Welcome back </p> TemplateRef TemplateRef represents the content on which a structural directive is applied. <ng-template> Template content </ng-template> TemplateRef = Runtime Representation of ng-template ViewContainerRef ViewContainerRef represents a location where Angular can insert views or components. Common methods include: createEmbeddedView() createComponent() clear() remove() Directive Lifecycle Directives participate in Angular lifecycle processing. import { Directive, OnDestroy, OnInit } from '@angular/core'; @Directive({ selector: '[appTracker]', standalone: true }) export class TrackerDirective implements OnInit, OnDestroy { ngOnInit() { console.log( 'Directive initialized' ); } ngOnDestroy() { console.log( 'Directive destroyed' ); } } Directive Dependency Injection Directives can inject Angular services and other dependencies. @Directive({ selector: '[appAnalytics]', standalone: true, host: { '(click)': 'trackClick()' } }) export class AnalyticsDirective { private analytics = inject(AnalyticsService); trackClick() { this.analytics.track( 'element_clicked' ); } } Injecting a Host Directive A directive can inject another directive that exists on the same element. @Directive({ selector: '[appTooltip]', standalone: true }) export class TooltipDirective { show() { console.log('Show tooltip'); } } @Directive({ selector: '[appTooltipTrigger]', standalone: true, host: { '(mouseenter)': 'open()' } }) export class TooltipTriggerDirective { private tooltip = inject(TooltipDirective); open() { this.tooltip.show(); } } Usage <button appTooltip appTooltipTrigger > Help </button> Component vs Attribute Directive vs Structural Directive Component Attribute Directive Structural Directive Owns a template Enhances an existing element Creates or removes views Creates a UI block Changes behavior or appearance Changes DOM structure Usually element selector Usually attribute selector Usually used with * @Component() @Directive() @Directive() One component per host element Multiple directives can share an element Controls embedded templates Directive vs Pipe Directive Pipe Changes element behavior Transforms displayed values Works with host elements Works inside template expressions appHighlight value | uppercase Can handle events Should remain a pure transformation Importing a Standalone Directive A standalone directive must be imported by the component that uses it. import { Component } from '@angular/core'; @Component({ selector: 'app-page', standalone: true, imports: [ HighlightDirective ], template: ` <p appHighlight> Highlighted content </p> ` }) export class PageComponent {} Common Mistakes 1. Directly Manipulating the Document Avoid: document .querySelector('button') ?.classList.add('active'); Prefer host bindings, ElementRef , or Renderer2 where appropriate. 2. Creating a Directive When a Component is Needed Use a component when the feature owns a reusable view. Use a directive when it enhances an existing element. 3. Using a Generic Selector Avoid: selector: '[highlight]' Prefer: selector: '[appHighlight]' 4. Forgetting to Import the Directive Standalone directives must be included in the consuming component's imports . 5. Using Directives as Security Hiding or disabling a button does not provide authorization. The backend must still validate every protected operation. 6. Creating Unnecessary Structural Directives Use built-in @if , @for , and @switch for standard control flow. 7. Adding Unmanaged Event Listeners Prefer host event bindings because Angular manages their lifecycle automatically. Best Practices Use attribute directives for reusable element behavior Use components when the feature owns its own template Prefer the host metadata property for host bindings and events Use meaningful prefixed directive selectors Keep directives small and focused Expose configurable behavior through typed inputs Emit reusable events through typed outputs Prefer declarative host bindings over direct DOM manipulation Use structural directives only for genuine custom view-management requirements Use hostDirectives to compose reusable behavior Always enforce permissions on the backend Interview Questions What is an Angular directive? A directive is an Angular class that adds reusable behavior to an existing element, component, or template. What are the main directive types? The main types are components, attribute directives, and structural directives. What is an attribute directive? An attribute directive changes the appearance or behavior of an existing element without creating its own view. What is a structural directive? A structural directive changes the DOM structure by creating, removing, or repeating embedded views. What is the difference between a component and a directive? A component owns a template and creates a UI block. A directive normally enhances an existing element or controls a template view. Why do structural directives use an asterisk? The asterisk is shorthand that Angular expands into an underlying ng-template . What is TemplateRef? TemplateRef represents the template content managed by a structural directive. What is ViewContainerRef? ViewContainerRef provides a location where Angular can create, insert, remove, or clear views and components. What are hostDirectives? hostDirectives allow a component or directive to reuse and expose behavior from other standalone directives. Should a directive manipulate the DOM directly? Prefer declarative host bindings. Use direct DOM access only when the required operation cannot be expressed through Angular bindings. Key Point A directive adds reusable behavior to an existing host element or controls how Angular creates and removes template views. Easy Formula Component = Directive + Template Attribute Directive = Existing Element + Behavior Structural Directive = Template + View Creation TemplateRef = Template Content ViewContainerRef = View Location host = Host Properties + Events hostDirectives = Directive Composition Element ↓ Selector Match ↓ Directive Created ↓ Inputs Received ↓ Host Behavior Applied ↓ Directive Destroyed