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.)
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 directiveselector: 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: initialisationngOnChanges: quand @Input changengOnDestroy: nettoyage avant destructionngAfterViewInit: 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>