Day 18 — Custom Directives Attribute, Structural & Composition
📘 Day 18 — Custom DirectivesAttribute, Structural & Composition API
Zero to Hero — Hands-on Angular Tutorial
Today you will learn:
- ✔️ Difference between Attribute & Structural Directives
- ✔️ Creating a custom Attribute Directive (
appHighlight) - ✔️ Handling events with
@HostListener - ✔️ Modifying host elements with
@HostBinding - ✔️ Creating a Structural Directive (
*appRole) - ✔️ Directive Composition API (Reusing directive logic)
You already used built-in directives (*ngIf, ngClass). Now you will build your own tools to manipulate the DOM without touching the component code. 🛠️
🟦 1. Attribute vs. Structural
| Type | Syntax | Purpose | Example |
|---|---|---|---|
| Attribute | [appHighlight] | Changes appearance/behavior of an element. | ngClass, ngStyle |
| Structural | *appIf | Adds or removes elements from the DOM. | *ngIf, *ngFor |
🟩 2. Creating an Attribute Directive
Let’s build a directive that highlights text when you hover over it.
Generate:
ng g directive directives/highlight --standalone
Code: highlight.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]', // Usage: <p appHighlight>
standalone: true
})
export class HighlightDirective {
// Accept a color from outside: <p appHighlight="blue">
@Input() appHighlight = '';
constructor(private el: ElementRef) {}
// Listen to the 'mouseenter' event on the HOST element
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight || 'yellow');
}
// Listen to 'mouseleave'
@HostListener('mouseleave') onMouseLeave() {
this.highlight('');
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
Usage:
<p appHighlight>Hover me (Default Yellow)</p>
<p appHighlight="cyan">Hover me (Cyan)</p>
- ✔️ @HostListener: Replaces
(mouseenter)="..."in HTML. logic stays in the directive. - ✔️ ElementRef: Direct access to the DOM element.
🟧 3. @HostBinding (The Clean Way)
Instead of using ElementRef to change styles (which is messy), use @HostBinding.
@Directive({ selector: '[appRainbow]' })
export class RainbowDirective {
// Bind this property to the host element's style.color
@HostBinding('style.color') textColor = 'black';
@HostBinding('style.borderColor') borderColor = 'black';
@HostListener('keydown') newColor() {
const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16);
this.textColor = randomColor;
this.borderColor = randomColor;
}
}
<input appRainbow> <!-- Typing changes text color -->
🟥 4. Creating a Structural Directive (*appRole)
Let’s build a directive that removes an element if the user doesn’t have the right role (like *ngIf, but for permissions).
Generate:
ng g directive directives/role --standalone
Code:
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
import { AuthService } from '../auth.service'; // Assuming you have one from Day 8
@Directive({
selector: '[appRole]',
standalone: true
})
export class RoleDirective {
private auth = inject(AuthService);
private templateRef = inject(TemplateRef); // The HTML inside the *directive
private viewContainer = inject(ViewContainerRef); // The container to put HTML into
@Input() set appRole(requiredRole: string) {
const userRole = this.auth.currentUserRole(); // Signal or Value
if (userRole === requiredRole) {
// ✅ Render the content
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
// ❌ Remove from DOM
this.viewContainer.clear();
}
}
}
Usage:
<div *appRole="'admin'">
<button>Delete User (Admins Only)</button>
</div>
- ✔️ TemplateRef: Holds the template contents (the
<div>and button). - ✔️ ViewContainerRef: The location in the DOM where we insert/remove the template.
🟫 5. Directive Composition API (Angular 15+)
You can apply directives to Components via TypeScript, without touching the HTML! Great for standardized UIs.
Scenario: All “Admin Buttons” should have a tooltip and the appRole logic.
@Component({
selector: 'app-admin-button',
standalone: true,
template: `<button><ng-content></ng-content></button>`,
// 👇 Apply directives automatically!
hostDirectives: [
{ directive: RoleDirective, inputs: ['appRole'] },
{ directive: TooltipDirective, inputs: ['tooltip'] }
]
})
export class AdminButtonComponent {
// Logic specific to the button...
}
Usage:
<!-- Automatically gets Role logic + Tooltip logic -->
<app-admin-button appRole="admin" tooltip="Dangerous Action">
Delete Database
</app-admin-button>
🎉 End of Day 18 — What You Learned
Today you gained superpowers over the DOM:
- ✔️ Attribute Directives: Changing colors/styles (
@HostBinding). - ✔️ Events: Handling clicks/hovers (
@HostListener). - ✔️ Structural Directives: Creating custom logic for showing/hiding elements (
*appRole). - ✔️ Composition: Chaining directives together for powerful components.
🧪 Day 18 Challenge
Build a “Click Outside” Directive.
Requirements:
- Selector:
[appClickOutside]. - Output:
(clickOutside). - Logic:
- Listen for global document clicks.
- If the click is NOT inside the host element (
this.el.nativeElement.contains(target)is false), emit the event.
- Usage: A dropdown menu that closes when you click away.