Directives Custom Angular : progresser en expertise

🏷️ Front-end 📅 10/04/2026 11:00:00 👤 Mezgani Said
Angular Directives Custom Expertise Décorateurs
Directives Custom Angular : progresser en expertise

Créez vos propres directives Angular avancées : lifecycle, @Input/@Output, validation, permissions, click-outside. Au-delà de ngIf et ngFor.

Limites des directives natives

Angular livre avec les directives structurelles natives : *ngIf, *ngFor, *ngSwitch. Mais ces directives ne couvrent que 10% des cas réels. Dès qu'on a besoin de logique métier, il faut créer des directives custom.

Cas où les directives natives échouent :

  • Ajouter dynamiquement des comportements au DOM
  • Transformer l'apparence d'un élément (dark mode, themes)
  • Implémenter des validations custom
  • Gérer l'accessibilité (ARIA, focus management)
  • Intégrer des librairies tierces au template
  • Créer des comportements réutilisables (scroll, click outside, etc.)
À retenir : Les directives custom sont la fondation de la réutilisabilité et de la scalabilité en Angular.

Créer une directive custom

Commençons par une directive simple qui ajoute un background au survol :

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  @Input() appHighlight = '#FFFF00'; // Couleur par défaut

  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight);
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight('transparent');
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

// Utilisation
<p appHighlight>Texte avec highlight au survol</p>
<p appHighlight="red">Avec couleur custom</p>

Éléments clés :

  • @Directive : décorateur pour définir la directive
  • selector : critère d'application (attribut, classe, etc.)
  • ElementRef : accès à l'élément DOM
  • @HostListener : écouter les événements de l'hôte
  • @Input : passer des valeurs à la directive

Lifecycle des directives

Les directives suivent le même lifecycle que les composants :

import { Directive, OnInit, OnDestroy, Input } from '@angular/core';

@Directive({
  selector: '[appLogger]'
})
export class LoggerDirective implements OnInit, OnDestroy {
  @Input() appLogger = 'Element'; // Nom pour les logs

  ngOnInit() {
    console.log(`✨ ${this.appLogger} initialized`);
  }

  ngOnDestroy() {
    console.log(`🗑️ ${this.appLogger} destroyed`);
  }
}

// En template
<div appLogger="Mon div">Contenu</div>
// Console: ✨ Mon div initialized
// (Au destruction) 🗑️ Mon div destroyed

Hooks disponibles :

  • ngOnInit : initialisation
  • ngOnChanges : quand @Input change
  • ngOnDestroy : nettoyage avant destruction
  • ngAfterViewInit : après rendu de la vue

Directives avec @Input et @Output

Créons une directive qui gère un click-outside (fermer un menu au clic extérieur) :

import { Directive, EventEmitter, HostListener, Output } from '@angular/core';

@Directive({
  selector: '[appClickOutside]'
})
export class ClickOutsideDirective {
  @Output() clickOutside = new EventEmitter<void>();

  @HostListener('document:click', ['$event'])
  onClick(event: MouseEvent) {
    const target = event.target as HTMLElement;
    // Si le click n'est pas sur l'élément hôte, émettre l'événement
    if (!this.elementRef.nativeElement.contains(target)) {
      this.clickOutside.emit();
    }
  }

  constructor(private elementRef: ElementRef) {}
}

// Utilisation
<div appClickOutside (clickOutside)="onOutsideClick()">
  Menu ou dropdown
</div>

Directive avec options avancées :

import { Directive, Input, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';

@Directive({
  selector: '[appDebounce]'
})
export class DebounceDirective implements OnInit, OnDestroy {
  @Input() debounceTime = 300; // en ms
  @Input() appDebounce: Function = () => {}; // Fonction à debouncer

  private destroy$ = new Subject<void>();
  private debounceSubject$ = new Subject<any>();

  ngOnInit() {
    this.debounceSubject$.pipe(
      debounceTime(this.debounceTime),
      takeUntil(this.destroy$)
    ).subscribe(() => this.appDebounce());
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

// Utilisation
<input (input)="debounceSubject$.next($event)"
       [appDebounce]="onSearchChange"
       [debounceTime]="500">

Directives multi-sélecteurs

Une directive peut s'appliquer de plusieurs façons :

@Directive({
  selector: '[appTheme], [app-theme], .app-theme'
})
export class ThemeDirective {
  // S'applique sur :
  // - Attribut: <div appTheme></div>
  // - Attribut avec traits: <div app-theme></div>
  // - Classe: <div class="app-theme"></div>
}

// Ou même sélecteur composant-directive
@Directive({
  selector: 'app-button[appLoading]'
})
export class LoadingDirective {
  // S'applique UNIQUEMENT sur <app-button appLoading></app-button>
  // Pas sur d'autres éléments
}

Cas d'usage avancés

1. Directive d'auto-focus

import { Directive, AfterViewInit, ElementRef } from '@angular/core';

@Directive({
  selector: '[appAutoFocus]'
})
export class AutoFocusDirective implements AfterViewInit {
  constructor(private el: ElementRef) {}

  ngAfterViewInit() {
    // Utiliser setTimeout pour s'assurer que le DOM est prêt
    setTimeout(() => {
      (this.el.nativeElement as HTMLInputElement).focus();
    }, 0);
  }
}

// Utilisation
<input appAutoFocus type="text">

2. Directive de validation personnalisée

import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';

@Directive({
  selector: '[appPasswordStrength]',
  providers: [
    { provide: NG_VALIDATORS, useExisting: PasswordStrengthDirective, multi: true }
  ]
})
export class PasswordStrengthDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    const value = control.value;

    if (!value) return null;

    const hasUpperCase = /[A-Z]/.test(value);
    const hasLowerCase = /[a-z]/.test(value);
    const hasNumbers = /\d/.test(value);
    const isLongEnough = value.length >= 8;

    const valid = hasUpperCase && hasLowerCase && hasNumbers && isLongEnough;

    return valid ? null : { passwordStrength: true };
  }
}

// Utilisation
<input formControl="password" appPasswordStrength>
<div *ngIf="form.get('password')?.hasError('passwordStrength')">
  Le mot de passe doit contenir majuscules, minuscules, chiffres et 8+ caractères
</div>

3. Directive de permission (NgIf avancé)

import { Directive, Input, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
import { AuthService } from './auth.service';

@Directive({
  selector: '[appHasPermission]'
})
export class HasPermissionDirective implements OnInit {
  @Input() appHasPermission: string[] = [];

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private authService: AuthService
  ) {}

  ngOnInit() {
    const hasPermission = this.authService.hasPermissions(this.appHasPermission);

    if (hasPermission) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

// Utilisation
<button *appHasPermission="['delete']">Supprimer</button>
<div *appHasPermission="['admin', 'user']">
  Contenu visible pour admin OU user
</div>
Résumé : Les directives custom sont la fondation de l'architecture scalable. Maîtrisez-les pour progresser d'expert Angular.