#angular #directives #composition-api

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

TypeSyntaxPurposeExample
Attribute[appHighlight]Changes appearance/behavior of an element.ngClass, ngStyle
Structural*appIfAdds 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:

  1. Selector: [appClickOutside].
  2. Output: (clickOutside).
  3. 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.
  4. Usage: A dropdown menu that closes when you click away.