Angular Directives
Directive composition and reuse
In Angular, directive composition and reuse refer to how you can build modular, reusable UI logic by creating and combining custom directives across components. This approach helps to separate concerns, improve code maintainability, and encourage DRY (Don’t Repeat Yourself) principles.
What is a Directive in Angular?
A directive is a class with a @Directive() decorator that adds behavior to elements in your Angular templates.
There are three types of directives:
- Component — technically a directive with a template.
- Structural Directive — changes the DOM layout (*ngIf, *ngFor).
- Attribute Directive — changes the appearance or behavior of an element ([ngClass], [ngStyle]).
✅ Use Case: Directive Reuse and Composition
Let’s go through a real-world example: You want to highlight an input when it’s focused and show a tooltip on hover. Instead of repeating code in every component, you create two reusable attribute directives.
Create HighlightOnFocusDirective
// highlight-on-focus.directive.ts
import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';
@Directive({
selector: '[appHighlightOnFocus]'
})
export class HighlightOnFocusDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {}
@HostListener('focus') onFocus() {
this.renderer.setStyle(this.el.nativeElement, 'background-color', 'lightyellow');
}
@HostListener('blur') onBlur() {
this.renderer.removeStyle(this.el.nativeElement, 'background-color');
}
}Create TooltipDirective
// tooltip.directive.ts
import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';
@Directive({
selector: '[appTooltip]'
})
export class TooltipDirective {
@Input('appTooltip') tooltipText = '';
tooltip: HTMLElement;
constructor(private el: ElementRef, private renderer: Renderer2) {}
@HostListener('mouseenter') onMouseEnter() {
this.tooltip = this.renderer.createElement('span');
this.tooltip.innerText = this.tooltipText;
this.renderer.appendChild(this.el.nativeElement, this.tooltip);
this.renderer.setStyle(this.tooltip, 'position', 'absolute');
this.renderer.setStyle(this.tooltip, 'background', 'black');
this.renderer.setStyle(this.tooltip, 'color', 'white');
}
@HostListener('mouseleave') onMouseLeave() {
if (this.tooltip) {
this.renderer.removeChild(this.el.nativeElement, this.tooltip);
}
}
}Use Directives Together (Composition)
<!-- app.component.html -->
<input
type="text"
placeholder="Enter name"
appHighlightOnFocus
appTooltip="Enter your full name"
/>Here, the input element is now:
- Highlighted on focus (from HighlightOnFocusDirective)
- Showing tooltip on hover (from TooltipDirective)
You’ve composed functionality by stacking directives on a single element.
How It Works Under the Hood
- Angular identifies all matching directives on a DOM element.
- For each directive, Angular creates an instance and applies:
- Lifecycle hooks (ngOnInit, etc.)
- Input bindings
- Event listeners via @HostListener
All directives share the same DOM element but can apply separate logic independently.
Export as and template variables
In Angular, exportAs and template variables are powerful tools used in templates to refer to and interact with components, directives, or other elements.
exportAs in Angular
exportAs is a property you can define in a directive or component’s decorator (@Directive or @Component). It allows you to assign a custom name that can be used as a reference to the directive/component in the template via a template reference variable.
Custom Directive Example
import { Directive } from '@angular/core';
@Directive({
selector: '[appToggle]',
exportAs: 'toggle' // 👈 This is key
})
export class ToggleDirective {
isOpen = false;
toggle() {
this.isOpen = !this.isOpen;
}
}Usage in Template
<div appToggle #myToggle="toggle">
<p *ngIf="myToggle.isOpen">Toggled content!</p>
<button (click)="myToggle.toggle()">Toggle</button>
</div>- #myToggle=”toggle” : binds the directive instance (exported as toggle) to the variable myToggle
- You can now call methods or access properties from the directive
Template Reference Variables (#variableName)
Template variables are declared using #varName and can be used to refer to a DOM element, directive, or component instance within the template.
Using exportAs with Template Variable
✅ Step 1: Directive with exportAs
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]',
exportAs: 'highlightDir'
})
export class HighlightDirective {
private isHighlighted = false;
toggleHighlight() {
this.isHighlighted = !this.isHighlighted;
console.log(`Highlight is now: ${this.isHighlighted}`);
}
}✅ Step 2: Use in Template
<!-- Apply directive and export it with #highlight -->
<div appHighlight #highlight="highlightDir">
Hover over me!
</div>
<!-- Button triggers a method on the directive -->
<button (click)="highlight.toggleHighlight()">Toggle Highlight</button>What’s Happening Here
- #highlight=”highlightDir” → binds to the directive instance using the name highlightDir defined in exportAs.
- highlight.toggleHighlight() → calls the directive method directly from the template.
Example: Using NgForm (Built-in exportAs)
Angular’s built-in directives like NgForm, NgModel, and ngModelGroup also use exportAs.
<form #f="ngForm" (ngSubmit)="onSubmit(f)">
<input name="username" ngModel required />
<button type="submit">Submit</button>
</form>
<p *ngIf="f.valid">Form is valid!</p>Explanation:
NgForm is exported as ngForm.
#f=”ngForm” gives you access to the form’s validity, values, etc.
Summary
Multi-slot transclusion and directive communication
In Angular, multi-slot transclusion and directive communication are advanced topics that often go hand-in-hand when building reusable and dynamic components. Let’s deep dive into both
Multi-slot Transclusion (Using ng-content)
📣 What is Transclusion?
Transclusion allows you to pass content from a parent component into a child component. Angular provides this with <ng-content>.
📣 What is Multi-slot Transclusion?
Instead of having a single slot (<ng-content></ng-content>), you can define named slots to place content in specific areas using the select attribute.
Multi-slot Transclusion
card.component.html (Child Component)
// card.component.html
<div class="card">
<div class="card-header">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-body">
<ng-content select="[card-body]"></ng-content>
</div>
<div class="card-footer">
<ng-content select="[card-footer]"></ng-content>
</div>
</div>
//card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.css']
})
export class CardComponent {}Usage in Parent Component
<app-card>
<div card-header>Header Content</div>
<div card-body>Main Content</div>
<div card-footer>Footer Content</div>
</app-card>The select in ng-content works like a CSS selector. The content with the matching attribute will be projected into that slot.
Directive Communication
What is Directive Communication?
It involves sharing data or behavior between a directive and a component, or between multiple directives. Common scenarios include using @Input(), @Output(),
Get The Frontend Fuel’s stories in your inbox
Join Medium for free to get updates from this writer.
Example Directive Communicating with a Component
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string | null) {
this.el.nativeElement.style.backgroundColor = color;
}
}Usage in Template
<p [appHighlight]="'lightblue'">Hover over me to see highlight</p>Single-slot Projection in Angular (a.k.a. Content Projection)
Single-slot projection allows you to pass content (HTML or components) from a parent component into a specific location in a child component using the <ng-content> tag.
Think of <ng-content> as a placeholder in your child component, where you can “project” HTML from the parent.
Basic Example
// Child Component: alert-box.component.ts
@Component({
selector: 'app-alert-box',
template: `
<div class="alert">
<ng-content></ng-content> <!-- 👈 Single-slot projection -->
</div>
`,
styles: [`.alert { border: 1px solid red; padding: 10px; }`]
})
export class AlertBoxComponent {}
// Parent Component Template
<app-alert-box>
<strong>Warning:</strong> This action is irreversible!
</app-alert-box>What is Context Projection in Angular and How Does it Work?
Context Projection in Angular refers to passing contextual data from a parent component to the projected content inside a child component. It is an enhancement of content projection using <ng-template> and ngTemplateOutlet, allowing dynamic and flexible templates with data binding.
While basic content projection (<ng-content>) passes static content, context projection allows you to inject dynamic data (context) into that content.
Context projection in Angular is a technique where a parent component passes a TemplateRef and a context object to a child component, allowing the child to render dynamic content with bound data using ngTemplateOutlet.
Example: Context Projection in Angular
We want to create a reusable card component that can display any content passed from a parent with dynamic data like a title and message.
Create the Child Component (card.component.ts)
This component receives:
import { Component, Input, TemplateRef } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-container *ngTemplateOutlet="template; context: context"></ng-container>
</div>
`,
styles: [`
.card {
border: 1px solid #ccc;
padding: 10px;
margin: 10px;
}
`]
})
export class CardComponent {
@Input() template!: TemplateRef<any>;
@Input() context: any = {};
}Use the Card Component in the Parent (app.component.html)
<app-card
[template]="myCardTemplate"
[context]="{ $implicit: 'Angular', subtitle: 'Framework for Web' }">
</app-card>
<ng-template #myCardTemplate let-title let-subtitle="subtitle">
<h2>{{ title }}</h2>
<p>{{ subtitle }}</p>
</ng-template>Explanation:
- myCardTemplate is the actual content to render inside the app-card component.
- let-title binds to $implicit which is passed as ‘Angular’.
- let-subtitle=”subtitle” binds to the subtitle property in the context.
- ngTemplateOutlet renders the content with the passed-in data.
Context projection lets us pass a TemplateRef and a context object to a child component so it can render dynamic content using ngTemplateOutlet, enabling clean, reusable, and flexible component design.
@HostListener and @HostBinding
In Angular, @HostListener and @HostBinding are powerful decorators provided by the @angular/core package. They allow you to interact directly with the host element of a directive or component. Here’s a deep dive into each, including how they work and examples.
@HostListener — Listening to Host Events
✅ Purpose: @HostListener allows your directive or component to listen to DOM events on the host element.
@HostListener('eventName', ['eventArgs'])
methodName(args) {
// logic
}
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@HostListener('mouseenter') onMouseEnter() {
console.log('Mouse entered!');
}
@HostListener('mouseleave') onMouseLeave() {
console.log('Mouse left!');
}
@HostListener('click', ['$event']) onClick(event: MouseEvent) {
console.log('Clicked!', event);
}
}What’s Happening?
- When you hover over an element with appHighlight, it triggers mouseenter and mouseleave.
- When you click on the element, it logs the event object.
- Angular internally binds the events to the host DOM element.
@HostBinding — Binding to Host Properties or Attributes
✅ Purpose: @HostBinding allows you to bind a property or attribute of the host element to a value in the directive/component class.
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@HostBinding('style.backgroundColor') bgColor: string;
@HostListener('mouseenter') onMouseEnter() {
this.bgColor = 'lightblue';
}
@HostListener('mouseleave') onMouseLeave() {
this.bgColor = 'transparent';
}
}What’s Happening?
When the mouse enters, it changes the background color of the host element.
- @HostBinding(‘style.backgroundColor’) binds the style to a class property.
- You don’t need to access the native element manually via ElementRef.
Directive That Highlights and Toggles Classes
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[appToggleHighlight]'
})
export class ToggleHighlightDirective {
private isHighlighted = false;
@HostBinding('class.highlight') get highlightClass() {
return this.isHighlighted;
}
@HostListener('click')
toggleHighlight() {
this.isHighlighted = !this.isHighlighted;
}
}Add CSS
.highlight {
background-color: yellow;
transition: background-color 0.3s;
}How It Works:
- Clicking the element toggles the highlight class.
- The directive manages the class binding with a getter and @HostBinding.
ElementRef, Renderer2, and direct DOM manipulation
✅ ElementRef : Used to access the native DOM element directly in Angular.
- Usage: el.nativeElement
- Caution: Avoid direct access as it can break Angular’s rendering and security (XSS risks).
✅ Renderer2 : Angular’s safe and platform-independent way to manipulate the DOM.
- Usage: renderer.setStyle(), renderer.addClass(), etc.
- Preferred over ElementRef for updating DOM elements.
❌ Direct DOM Manipulation: Accessing DOM via document.getElementById() or nativeElement directly.
- Not recommended in Angular.
- Breaks SSR, testing, and security.
Here’s a combined example using ElementRef, Renderer2, and @HostListener, along with an explanation of how it works.
import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';
@Directive({
selector: '[appHoverHighlight]'
})
export class HoverHighlightDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {}
@HostListener('mouseenter') onMouseEnter() {
this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'lightyellow');
this.renderer.setStyle(this.el.nativeElement, 'cursor', 'pointer');
}
@HostListener('mouseleave') onMouseLeave() {
this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor');
this.renderer.removeStyle(this.el.nativeElement, 'cursor');
}
}💡 How It Works










