À propos de ce cours
Angular distingue les composants, les directives d’attribut et les directives structurelles. Les liaisons [class] et [style] remplacent le plus souvent NgClass et NgStyle, et les blocs @if, @for et @switch remplacent *ngIf, *ngFor et [ngSwitch]. Une directive d’attribut se compose d’un sélecteur entre crochets, d’entrées, de sorties et de liaisons sur son hôte (host). Elle agit sur le DOM avec ElementRef et Renderer2. Une directive structurelle affiche son modèle grâce à TemplateRef et ViewContainerRef, et l’étoile * est un raccourci pour . ngTemplateOutlet rend un composant générique. hostDirectives compose des comportements, et exportAs nomme une directive. On teste une directive à travers un composant hôte. Le cours se termine par 7 activités corrigées et les erreurs fréquentes.
-
Objectifs
- Distinguer les trois familles de directives : composants, directives d'attribut, directives structurelles
- Utiliser les directives intégrées (
NgClass,NgStyle,NgTemplateOutlet) et savoir quand une simple liaison suffit - Passer des anciennes directives
*ngIf,*ngFor,[ngSwitch]aux blocs@if,@for,@switch - Créer ses propres directives d'attribut : sélecteur, entrées, sorties, liaisons de l'hôte
- Créer une directive structurelle avec
TemplateRefetViewContainerRef, et comprendre l'étoile* - Composer des directives (
hostDirectives), les nommer (exportAs) et les tester
-
Présentation
- Une directive est une classe qui ajoute un comportement à un élément du template. Cette leçon présente les deux familles que l'on crée soi-même : les directives d'attribut, qui modifient l'élément qui les porte, et les directives structurelles, qui ajoutent ou retirent des éléments. Vous en utilisez depuis le début du cours :
ngModelen est une, et chaque composant est une directive munie d'un template. - Écrire ses propres directives permet de réutiliser un comportement sur n'importe quel élément : surligner au survol, donner le focus, fermer un menu au clic extérieur, afficher un contenu selon un rôle… sans recopier le même code dans chaque composant.
- Une directive est une classe qui ajoute un comportement à un élément du template. Cette leçon présente les deux familles que l'on crée soi-même : les directives d'attribut, qui modifient l'élément qui les porte, et les directives structurelles, qui ajoutent ou retirent des éléments. Vous en utilisez depuis le début du cours :
- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/directives/. Tous les exemples ont été compilés et testés avec Angular 22.2 (composantsOnPushpar défaut, sans Zone.js).
-
Qu'est-ce qu'une directive ?
- Angular distingue trois familles :
- les composants: des directives avec un template, utilisées comme une balise (
<app-carte />) ; - les directives d'attribut: elles modifient l'apparence ou le comportement de l'élément qui les porte, appelé l'hôte (
<p appSurligner>) ; - les directives structurelles: elles ajoutent ou retirent des éléments, en travaillant sur un
<ng-template>(<p *appSauf="…">).
- les composants: des directives avec un template, utilisées comme une balise (
- Comme un composant, une directive ne s'applique que si elle est importée dans le composant qui l'utilise (
imports: [Surligner]).
- Angular distingue trois familles :

-
Classes et styles : NgClass, NgStyle ou liaisons ?
NgClassetNgStylesont des directives d'attribut intégrées. Depuis l'arrivée des liaisons[class]et[style]avec objet, elles ne sont presque plus nécessaires :
import { Component, signal } from '@angular/core';
import { NgClass, NgStyle } from '@angular/common';
// Directives d'attribut intégrées : NgClass et NgStyle, comparées aux liaisons [class] et [style]
@Component({
selector: 'app-classes',
imports: [NgClass, NgStyle],
template: `
<label><input type="checkbox" [checked]="urgent()" (change)="urgent.set(!urgent())" /> Urgent</label>
<label>Taille : <input type="range" min="12" max="24" [value]="taille()" (input)="taille.set(+$any($event.target).value)" /></label>
<p [ngClass]="{ urgent: urgent(), fait: false }" [ngStyle]="{ 'font-size.px': taille() }">Avec NgClass et NgStyle</p>
<p [class.urgent]="urgent()" [style.font-size.px]="taille()">Avec [class.urgent] et [style.font-size.px]</p>
<p [class]="{ urgent: urgent() }" [style]="{ 'font-size': taille() + 'px' }">Avec [class] et [style] (objets)</p>
`,
styles: `
p { margin: 6px 0; padding: 6px 10px; border-radius: 6px; background: #f1f5f9; }
.urgent { background: #fee2e2; color: #b91c1c; font-weight: 700; }
label { margin-right: 16px; }
`,
})
export class Classes {
protected readonly urgent = signal(true);
protected readonly taille = signal(16);
}

- Les trois paragraphes ont le même rendu. Les liaisons n'exigent aucun import ;
NgClassetNgStyledoivent être importés depuis@angular/common. - Angular fournit deux migrations automatiques :
ng generate @angular/core:ngclass-to-classetng generate @angular/core:ngstyle-to-style.

-
Les directives structurelles historiques
- Avant Angular 17, on affichait conditionnellement avec
*ngIf, on répétait avec*ngForet on choisissait avec[ngSwitch]. Ces directives sont dépréciées depuis Angular 20: on les rencontre encore dans les projets existants, mais un nouveau code utilise les blocs.
- Avant Angular 17, on affichait conditionnellement avec
import { Component, signal } from '@angular/core';
import { NgFor, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
// Les directives structurelles historiques (dépréciées depuis Angular 20) et leurs blocs équivalents
@Component({
selector: 'app-ancienne-syntaxe',
imports: [NgIf, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault],
template: `
<button (click)="vider()">Vider la liste</button>
<button (click)="etat.set(etat() === 'ouvert' ? 'ferme' : 'ouvert')">Changer l'état</button>
<div class="cols">
<div>
<h5>Directives (*ngIf, *ngFor, [ngSwitch])</h5>
<ul *ngIf="taches().length > 0; else vide">
<li *ngFor="let t of taches(); let i = index">{{ i + 1 }}. {{ t }}</li>
</ul>
<ng-template #vide><p>Aucune tâche.</p></ng-template>
<p [ngSwitch]="etat()">
<span *ngSwitchCase="'ouvert'">Inscriptions ouvertes</span>
<span *ngSwitchDefault>Inscriptions fermées</span>
</p>
</div>
<div>
<h5>Blocs (@if, @for, @switch)</h5>
@if (taches().length > 0) {
<ul>
@for (t of taches(); track $index; let i = $index) {
<li>{{ i + 1 }}. {{ t }}</li>
}
</ul>
} @else {
<p>Aucune tâche.</p>
}
<p>
@switch (etat()) {
@case ('ouvert') { <span>Inscriptions ouvertes</span> }
@default { <span>Inscriptions fermées</span> }
}
</p>
</div>
</div>
`,
styles: `.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } h5 { margin: 6px 0; } ul { margin: 4px 0; padding-left: 20px; }`,
})
export class AncienneSyntaxe {
protected readonly taches = signal(['Lire le cours', 'Faire le TP']);
protected readonly etat = signal<'ouvert' | 'ferme'>('ouvert');
protected vider() {
this.taches.set([]);
}
}

- Les deux colonnes affichent la même chose. Les directives exigent un import (
NgIf,NgFor…), les blocs non. - Oublier l'import de
NgIfne bloque pas la compilation, mais la console affiche :NG0303: Can't bind to 'ngIf' since it isn't a known property of 'p', et le paragraphe ne s'affiche jamais. - Pour convertir tout un projet :
ng generate @angular/core:control-flow.

-
Créer une directive d'attribut
- Le CLI crée le fichier, avec un sélecteur d'attribut (entre crochets) préfixé par
app:
- Le CLI crée le fichier, avec un sélecteur d'attribut (entre crochets) préfixé par
$ ng generate directive directives/surligner
CREATE src/app/directives/surligner.spec.ts (212 bytes)
CREATE src/app/directives/surligner.ts (123 bytes)
import { Directive } from '@angular/core';
@Directive({
selector: '[appSurligner]',
})
export class Surligner {}
- On la complète : une entrée qui porte le même nom que le sélecteur permet d'écrire la couleur directement sur l'attribut, et la propriété
hostécoute la souris et lie le style de l'hôte :
import { Component, Directive, input, signal } from '@angular/core';
// Une directive d'attribut : elle modifie l'apparence de l'élément qui la porte (l'hôte)
@Directive({
selector: '[appSurligner]',
host: {
'(mouseenter)': 'survol.set(true)',
'(mouseleave)': 'survol.set(false)',
'[style.backgroundColor]': 'survol() ? (appSurligner() || couleurParDefaut()) : null',
'[style.transition]': '"background-color .2s"',
},
})
export class Surligner {
// Même nom que le sélecteur : on écrit appSurligner="…" directement sur l'élément
readonly appSurligner = input('');
readonly couleurParDefaut = input('#fde68a');
protected readonly survol = signal(false);
}
@Component({
selector: 'app-demo-surligner',
imports: [Surligner],
template: `
<p appSurligner>Survolez-moi : couleur par défaut.</p>
<p appSurligner="#bae6fd">Survolez-moi : bleu clair.</p>
<p [appSurligner]="couleur()" couleurParDefaut="#bbf7d0">Survolez-moi : couleur choisie ci-dessous.</p>
<select (change)="couleur.set($any($event.target).value)">
<option value="">(par défaut : vert)</option>
<option value="#fbcfe8">Rose</option>
<option value="#ddd6fe">Violet</option>
</select>
`,
styles: `p { padding: 6px 10px; border-radius: 6px; margin: 4px 0; }`,
})
export class DemoSurligner {
protected readonly couleur = signal('');
}

<p appSurligner>: l'entrée vaut'', la couleur par défaut s'applique.appSurligner="#bae6fd"transmet un texte fixe ;[appSurligner]="couleur()"une expression.couleurParDefautest une seconde entrée : sur l'hôte, elle s'écrit comme un attribut ordinaire.- Sans l'import,
<p appSurligner="#bae6fd">compile et ne fait rien (c'est un simple attribut HTML) ;[appSurligner]="…"est refusé :NG8002: Can't bind to 'appSurligner' since it isn't a known property of 'p'.

-
Accéder à l'élément hôte
- Une directive peut obtenir l'élément qui la porte avec
inject(ElementRef). Exemple : donner le focus à un champ dès qu'il apparaît.
- Une directive peut obtenir l'élément qui la porte avec
import { Component, Directive, ElementRef, afterNextRender, inject, signal } from '@angular/core';
// Accéder à l'élément hôte : inject(ElementRef), puis agir après le premier affichage
@Directive({ selector: '[appFocusAuto]' })
export class FocusAuto {
private readonly hote = inject<ElementRef<HTMLElement>>(ElementRef);
constructor() {
afterNextRender(() => this.hote.nativeElement.focus());
}
}
@Component({
selector: 'app-demo-focus-auto',
imports: [FocusAuto],
template: `
<button (click)="ouvert.set(!ouvert())">{{ ouvert() ? 'Fermer' : 'Rechercher' }}</button>
@if (ouvert()) {
<input appFocusAuto placeholder="Le curseur est déjà ici" />
}
`,
})
export class DemoFocusAuto {
protected readonly ouvert = signal(false);
}

afterNextRender()attend que l'élément soit réellement affiché : appelerfocus()directement dans le constructeur serait trop tôt.- Pour lire l'élément,
ElementRefsuffit. Pour créer ou modifier des éléments, préférezRenderer2(activité 2) : il fonctionne aussi hors du navigateur (rendu côté serveur). - La propriété
hostremplace les anciens décorateurs@HostListeneret@HostBinding: c'est l'écriture recommandée.

-
Une directive qui émet : l'appui long
- Une directive peut aussi avoir des sorties. Celle-ci émet quand on maintient l'appui assez longtemps, ce qui évite une boîte de confirmation pour une suppression :
import { Component, Directive, input, numberAttribute, output, signal } from '@angular/core';
// Une directive avec une sortie : elle émet quand on maintient l'appui assez longtemps
@Directive({
selector: '[appAppuiLong]',
host: {
'(pointerdown)': 'commencer()',
'(pointerup)': 'annuler()',
'(pointerleave)': 'annuler()',
'[class.appui-en-cours]': 'enCours()',
},
})
export class AppuiLong {
readonly duree = input(800, { transform: numberAttribute });
readonly appAppuiLong = output<void>();
protected readonly enCours = signal(false);
private minuterie?: ReturnType<typeof setTimeout>;
protected commencer() {
this.enCours.set(true);
this.minuterie = setTimeout(() => {
this.enCours.set(false);
this.appAppuiLong.emit();
}, this.duree());
}
protected annuler() {
clearTimeout(this.minuterie);
this.enCours.set(false);
}
}
@Component({
selector: 'app-demo-appui-long',
imports: [AppuiLong],
template: `
@for (f of fichiers(); track f) {
<div class="ligne">
{{ f }}
<button duree="700" (appAppuiLong)="supprimer(f)">Maintenir pour supprimer</button>
</div>
} @empty {
<p>Corbeille vide.</p>
}
`,
styles: `
.ligne { display: flex; justify-content: space-between; gap: 10px; padding: 4px 0; }
button.appui-en-cours { background: #fecaca; outline: 3px solid #ef4444; }
`,
})
export class DemoAppuiLong {
protected readonly fichiers = signal(['brouillon.txt', 'ancien-logo.png', 'notes.md']);
protected supprimer(f: string) {
this.fichiers.update((l) => l.filter((x) => x !== f));
}
}

- La sortie porte le nom du sélecteur : on écrit
(appAppuiLong)="supprimer(f)", comme un événement natif. duree="700"est un texte :numberAttributele convertit en nombre.pointerdownetpointerupfonctionnent à la souris comme au doigt ;pointerleaveannule si le pointeur quitte le bouton.
-
Créer une directive structurelle
- Une directive structurelle reçoit le modèle (
TemplateRef) et l'emplacement où l'afficher (ViewContainerRef). Elle décide combien de fois l'afficher : zéro, une, ou plusieurs. appSaufest l'inverse de@if: elle affiche son contenu sauf si la condition est vraie, avec un modèle de remplacement facultatif :
- Une directive structurelle reçoit le modèle (
import { Component, Directive, TemplateRef, ViewContainerRef, effect, inject, input, signal } from '@angular/core';
// Une directive structurelle : elle décide si son modèle (<ng-template>) est affiché
@Directive({ selector: '[appSauf]' })
export class Sauf {
private readonly modele = inject(TemplateRef);
private readonly conteneur = inject(ViewContainerRef);
readonly appSauf = input.required<boolean>();
readonly appSaufSinon = input<TemplateRef<unknown> | null>(null);
constructor() {
effect(() => {
this.conteneur.clear();
if (!this.appSauf()) {
this.conteneur.createEmbeddedView(this.modele);
} else {
const sinon = this.appSaufSinon();
if (sinon) {
this.conteneur.createEmbeddedView(sinon);
}
}
});
}
}
@Component({
selector: 'app-demo-sauf',
imports: [Sauf],
template: `
<button (click)="connecte.set(!connecte())">{{ connecte() ? 'Se déconnecter' : 'Se connecter' }}</button>
<p *appSauf="connecte(); sinon: bienvenue">Connectez-vous pour voir vos notes.</p>
<ng-template #bienvenue><p>Bienvenue, Amel : vous avez 3 nouvelles notes.</p></ng-template>
<!-- La forme longue, sans l'étoile -->
<ng-template [appSauf]="connecte()"><p class="discret">(forme longue) Accès invité</p></ng-template>
`,
styles: `.discret { color: #64748b; font-size: 13px; }`,
})
export class DemoSauf {
protected readonly connecte = signal(false);
}

- L'
effectrelit les entrées : à chaque changement deconnecte(), il vide l'emplacement puis crée la vue voulue. - Dans
*appSauf="connecte(); sinon: bienvenue", la clésinonest préfixée par le sélecteur : elle alimente l'entréeappSaufSinon. - Utilisée sans étoile sur un élément ordinaire (
<p [appSauf]="…">), la directive ne trouve pas de modèle :NG0201: No provider for TemplateRef found. - Une seule directive structurelle par élément :
NG5002: Can't have multiple template bindings on one element. Use only one attribute prefixed with *. Pour en combiner deux, placez la première sur un<ng-container>.


-
ng-template, ng-container et ngTemplateOutlet
<ng-template>définit un modèle qui n'est pas affiché tant qu'une directive ne le demande pas.<ng-container>regroupe sans ajouter de balise.NgTemplateOutletaffiche un modèle en lui passant un contexte.- Ensemble, ils permettent un composant générique dont le parent choisit l'affichage de chaque ligne :
import { Component, TemplateRef, input, signal } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
interface Cours { titre: string; heures: number; niveau: 'débutant' | 'avancé'; }
// Un composant générique : le parent fournit le modèle de chaque ligne
@Component({
selector: 'app-liste-generique',
imports: [NgTemplateOutlet],
template: `
<ul>
@for (e of elements(); track $index) {
<li>
<ng-container *ngTemplateOutlet="modele() ?? defaut; context: { $implicit: e, rang: $index + 1 }" />
</li>
}
</ul>
<ng-template #defaut let-e>{{ e }}</ng-template>
`,
styles: `ul { margin: 4px 0; padding-left: 18px; }`,
})
export class ListeGenerique<T> {
readonly elements = input.required<readonly T[]>();
readonly modele = input<TemplateRef<{ $implicit: T; rang: number }> | null>(null);
}
@Component({
selector: 'app-demo-modeles',
imports: [ListeGenerique, NgTemplateOutlet],
template: `
<h5>Sans modèle</h5>
<app-liste-generique [elements]="noms" />
<h5>Avec un modèle fourni par le parent</h5>
<app-liste-generique [elements]="cours()" [modele]="ligneCours" />
<ng-template #ligneCours let-c let-rang="rang">
<strong>{{ rang }}. {{ c.titre }}</strong> · {{ c.heures }} h
<ng-container *ngTemplateOutlet="badge; context: { $implicit: c.niveau }" />
</ng-template>
<ng-template #badge let-n><span class="badge" [class.avance]="n === 'avancé'">{{ n }}</span></ng-template>
`,
styles: `.badge { font-size: 11px; padding: 1px 8px; border-radius: 10px; background: #dcfce7; } .avance { background: #fee2e2; } h5 { margin: 6px 0 2px; }`,
})
export class DemoModeles {
protected readonly noms = ['Amel', 'Karim', 'Sana'];
protected readonly cours = signal<Cours[]>([
{ titre: 'Angular', heures: 40, niveau: 'débutant' },
{ titre: 'Services et DI', heures: 12, niveau: 'avancé' },
]);
}

let-clit$implicit;let-rang="rang"lit la clérangdu contexte.modele() ?? defaut: sans modèle fourni, le composant utilise le sien.- C'est ainsi que fonctionnent les bibliothèques de tableaux ou de listes : le composant gère la structure, le parent le contenu.

-
Composer des directives : hostDirectives
- Un composant peut appliquer lui-même des directives à son élément hôte, et exposer leurs entrées et sorties sous le nom de son choix :
import { Component, signal } from '@angular/core';
import { AppuiLong } from './appui-long';
import { Surligner } from './surligner';
// La composition de directives : le composant applique lui-même deux directives à son hôte
@Component({
selector: 'app-bouton-danger',
hostDirectives: [
{ directive: Surligner, inputs: ['appSurligner: couleurSurvol'] },
{ directive: AppuiLong, inputs: ['duree'], outputs: ['appAppuiLong: confirme'] },
],
host: { role: 'button', tabindex: '0' },
template: `<ng-content />`,
styles: `:host { display: inline-block; padding: 6px 14px; border: 2px solid #dc2626; color: #b91c1c; border-radius: 8px; cursor: pointer; user-select: none; }
:host(.appui-en-cours) { outline: 3px solid #ef4444; }`,
})
export class BoutonDanger {}
@Component({
selector: 'app-demo-composition',
imports: [BoutonDanger],
template: `
<app-bouton-danger couleurSurvol="#fee2e2" duree="600" (confirme)="compte.set(compte() + 1)">
Maintenir pour vider la corbeille
</app-bouton-danger>
<p>Corbeille vidée {{ compte() }} fois.</p>
`,
})
export class DemoComposition {
protected readonly compte = signal(0);
}

BoutonDangern'écrit aucune logique : il assembleSurligneretAppuiLong.- Seules les entrées et sorties listées sont accessibles.
[appSurligner]sur<app-bouton-danger>est refusé :NG8002: Can't bind to 'appSurligner' since it isn't a known property of 'app-bouton-danger'.On écritcouleurSurvol.

-
exportAs et dialogue avec une directive parente
exportAsdonne un nom à la directive : le template peut la désigner avec une variable de référence et appeler ses méthodes publiques. Une directive peut aussi injecter celle d'un élément parent:
import { Component, Directive, inject, input, signal } from '@angular/core';
// exportAs : le template peut nommer la directive et lire ses membres publics
@Directive({
selector: '[appCompteurClics]',
exportAs: 'compteur',
host: { '(click)': 'nombre.set(nombre() + 1)' },
})
export class CompteurClics {
readonly nombre = signal(0);
remettreAZero() {
this.nombre.set(0);
}
}
// Une directive qui injecte la directive de son parent : l'accordéon garde un seul panneau ouvert
@Directive({ selector: '[appAccordeon]' })
export class Accordeon {
readonly ouvert = signal<string | null>(null);
}
@Directive({
selector: '[appPanneau]',
host: { '[class.ouvert]': 'estOuvert()', '(click)': 'basculer()' },
})
export class Panneau {
private readonly accordeon = inject(Accordeon);
readonly appPanneau = input.required<string>();
estOuvert() {
return this.accordeon.ouvert() === this.appPanneau();
}
basculer() {
this.accordeon.ouvert.set(this.estOuvert() ? null : this.appPanneau());
}
}
@Component({
selector: 'app-demo-export-parent',
imports: [CompteurClics, Accordeon, Panneau],
template: `
<button appCompteurClics #c="compteur">Cliquez-moi</button>
<span> {{ c.nombre() }} clic(s) </span>
<button (click)="c.remettreAZero()">Remettre à zéro</button>
<div appAccordeon class="acc">
<div appPanneau="html"><b>HTML</b><p>La structure de la page.</p></div>
<div appPanneau="css"><b>CSS</b><p>La mise en forme.</p></div>
<div appPanneau="ts"><b>TypeScript</b><p>La logique, typée.</p></div>
</div>
`,
styles: `
.acc { margin-top: 10px; border: 1px solid #cbd5e1; border-radius: 8px; }
.acc > div { padding: 6px 10px; border-bottom: 1px solid #e2e8f0; cursor: pointer; }
.acc p { display: none; margin: 4px 0 0; color: #475569; }
.acc .ouvert { background: #eff6ff; } .acc .ouvert p { display: block; }
`,
})
export class DemoExportParent {}

#c="compteur"désigne la directive, pas le bouton :c.nombre()etc.remettreAZero()sont accessibles.- Un nom inconnu est refusé :
NG8003: No directive found with exportAs 'compteurs'. inject(Accordeon)remonte l'arbre des éléments : chaque panneau trouve l'accordéon qui l'entoure, et un seul panneau reste ouvert.

-
Tester une directive
- On teste une directive à travers un petit composant hôte, écrit dans le fichier de test :
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Surligner } from './surligner';
import { Sauf } from './sauf';
@Component({
imports: [Surligner, Sauf],
template: `
<p appSurligner="rgb(186, 230, 253)">Texte</p>
<span *appSauf="cache()">Visible</span>
`,
})
class Hote {
readonly cache = signal(false);
}
describe('Directives', () => {
it('Surligner colore l\'hôte au survol, puis le rétablit', async () => {
const f = TestBed.createComponent(Hote);
await f.whenStable();
const p: HTMLElement = f.nativeElement.querySelector('p');
p.dispatchEvent(new Event('mouseenter'));
await f.whenStable();
expect(p.style.backgroundColor).toBe('rgb(186, 230, 253)');
p.dispatchEvent(new Event('mouseleave'));
await f.whenStable();
expect(p.style.backgroundColor).toBe('');
});
it('Sauf retire son modèle quand la condition devient vraie', async () => {
const f = TestBed.createComponent(Hote);
await f.whenStable();
expect(f.nativeElement.querySelector('span')?.textContent).toBe('Visible');
f.componentInstance.cache.set(true);
await f.whenStable();
expect(f.nativeElement.querySelector('span')).toBeNull();
});
});
$ ng test --include src/app/directives/surligner.spec.ts
✓ Directives > Surligner colore l'hôte au survol, puis le rétablit
✓ Directives > Sauf retire son modèle quand la condition devient vraie
Tests 2 passed (2)
- Piège : avec un champ ordinaire (
cache = false) dans le composant hôte, le second test échoue. Les composants sontOnPush: modifier le champ ne prévient pas Angular. Unsignalle fait. - Le test fourni par
ng generate(new Surligner()) ne vérifie que la création : il échoue même dès que la directive utiliseinject(), appelé hors d'un contexte d'injection.

-
Activité 1 : une bordure configurable
- Créer la directive
appBordureavec trois entrées : la couleur (même nom que le sélecteur, gris par défaut),epaisseur(nombre, 2 par défaut, avecnumberAttribute) etarrondi(booléen, avecbooleanAttribute). - Elle lie
border,border-radius(12 px si arrondi) etpaddingde l'hôte. L'appliquer à trois cartes produits : standard, promotion (vert, 4 px, arrondi), rupture (rouge, 6 px).
- Créer la directive

- Vérifier :
arrondiécrit seul (sans valeur) arrondit bien la carte. - Pourquoi
epaisseur="4"a-t-il besoin denumberAttribute, alors que[epaisseur]="6"non ?
-
Activité 2 : une infobulle
- Créer
appInfobulle: au survol (et au focus clavier), elle crée avecRenderer2un<span class="infobulle">contenant le texte reçu, et le retire ensuite. - La directive est exportée sous le nom
infobulle: deux boutons « Montrer l'aide » et « Cacher » appellent ses méthodesafficher()etmasquer()par une variable de référence.
- Créer

- Vérifier : survoler 🖨️ affiche « Imprimer (Ctrl + P) » ; « Montrer l'aide » affiche la bulle du bouton ❔ sans le survoler.
- Pourquoi utiliser
Renderer2plutôt quedocument.createElement()?
-
Activité 3 : copier dans le presse-papiers
- Créer
appCopier: au clic, elle copie le texte reçu avecnavigator.clipboard.writeText(), puis émet la sortiecopie(le texte copié) ou, en cas d'échec, la sortieechec. - Afficher trois codes promo, chacun avec un bouton « Copier » ; un message « « … » copié ! » s'affiche deux secondes.
- Créer

- Vérifier : coller (Ctrl + V) dans un éditeur donne bien « RENTREE-2026 ».
- Pourquoi le message est-il géré par le composant et non par la directive ?
-
Activité 4 : fermer au clic extérieur
- Créer
appClicExterieur: elle écoute les clics sur tout le document ('(document:click)') et émet sa sortie quand le clic a lieu hors de son hôte (contains()). - L'utiliser pour un menu déroulant « Mon compte » : un clic sur le bouton l'ouvre, un clic n'importe où ailleurs le ferme.
- Créer

- Vérifier : cliquer sur « Profil » ne ferme pas le menu ; cliquer sur le texte en dessous le ferme.
- Que se passerait-il si la directive était placée sur le bouton au lieu du conteneur du menu ?
-
Activité 5 : afficher selon le rôle
- Créer le service
Session(un signalrole:'invite','eleve'ou'admin') et la directive structurelle*appSiRole: elle accepte un rôle ou une liste de rôles, et une clésinonpour un modèle de remplacement. - Afficher « Télécharger le corrigé » aux élèves et aux administrateurs (sinon : « Inscrivez-vous… »), et « Modifier le cours » aux seuls administrateurs. Trois boutons changent le rôle.
- Créer le service

- Vérifier : en invité, seul le message d'inscription apparaît ; en élève, le lien de téléchargement ; en administrateur, aussi le bouton de modification.
- Cette directive suffit-elle à protéger le corrigé ? Que faut-il faire côté serveur ?
-
Activité 6 : répéter un modèle avec un contexte typé
- Créer
*appRepeter="n": elle affiche son modèlenfois, avec un contexte{ $implicit, index, premier, dernier }, et une méthode statiquengTemplateContextGuardqui type ce contexte. - L'utiliser pour afficher une note en étoiles (pleines jusqu'à la note), une notation cliquable, et la liste « [S1, S2, S3, S4] » avec
premieretdernier.
- Créer

- Vérifier : cliquer sur la 4e étoile affiche « 4 / 5 ».
- Sans
ngTemplateContextGuard, quel est le type deidanslet i = index? Quel risque cela fait-il courir ?
-
Activité 7 : tester les directives
- Écrire
directives.spec.tsavec un composant hôte qui utiliseappBordure,*appRepeter(piloté par un signal) et*appSiRole:- la bordure, l'épaisseur et l'arrondi sont appliqués ;
*appRepetercrée 3 vues, puis 1 quand le signal change, avecderniercorrect ;*appSiRoleaffiche « Refusé », puis « Zone admin » quandTestBed.inject(Session)passe en administrateur.
- Écrire
$ ng test --include src/app/act-directives/directives.spec.ts
✓ Activités : directives > Bordure applique la couleur, l'épaisseur et l'arrondi
✓ Activités : directives > Repeter crée n vues avec index et dernier, et suit le signal
✓ Activités : directives > SiRole affiche le modèle selon le rôle de la session
Tests 3 passed (3)
- Vérifier : 3 tests réussis.
- Pourquoi le test de
*appSiRolen'a-t-il pas besoin de cliquer sur un bouton ?
- Les corrigés des sept activités, testés avec Angular 22, sont fournis dans l'archive de la leçon (dossier
corriges/).
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG8002: Can't bind to 'appSurligner' since it isn't a known property of 'p'. |
Directive non importée | L'ajouter aux imports du composant |
L'attribut appSurligner="…" ne fait rien, sans erreur |
Directive non importée (attribut HTML ordinaire) | Importer la directive |
NG0303: Can't bind to 'appSauf' since it isn't a known property of 'p' (console) |
Directive structurelle non importée | Importer la directive ; pour *ngIf, importer NgIf ou passer à @if |
NG5002: Can't have multiple template bindings on one element. Use only one attribute prefixed with * |
Deux directives structurelles sur un élément | Placer l'une d'elles sur un <ng-container> |
NG0201: No provider for TemplateRef found. |
Directive structurelle utilisée sans * |
Écrire *appSauf ou l'appliquer à un <ng-template> |
NG8003: No directive found with exportAs 'compteurs'. |
Nom d'export erroné | Utiliser la valeur de exportAs |
NG8002: Can't bind to 'appSurligner' since it isn't a known property of 'app-bouton-danger'. |
Entrée d'une hostDirective non exposée ou renommée |
L'ajouter à inputs, sous le nom exposé |
| La directive ne s'applique à aucun élément | Sélecteur sans crochets ('appSurligner' = une balise) |
selector: '[appSurligner]' |
focus() sans effet |
Appelé dans le constructeur, avant l'affichage | afterNextRender() |
| L'affichage ne suit pas une valeur modifiée | Champ ordinaire dans un composant OnPush |
Utiliser un signal |
epaisseur="4" reçu comme texte |
Attribut statique sans conversion | input(2, { transform: numberAttribute }) |
-
À retenir
- Trois familles : les composants (avec template), les directives d'attribut (l'hôte change), les directives structurelles (des éléments apparaissent ou disparaissent).
- Pour les classes et les styles, les liaisons
[class]et[style]suffisent presque toujours ;*ngIf,*ngForet[ngSwitch]sont remplacés par@if,@for,@switch. - Une directive d'attribut : un sélecteur entre crochets, des
input()(dont un au nom du sélecteur), desoutput(), et des liaisons d'hôte danshost. - Pour agir sur le DOM :
inject(ElementRef),Renderer2, etafterNextRender()pour attendre l'affichage. - Une directive structurelle utilise
TemplateRefetViewContainerRef; l'étoile*est un raccourci pour<ng-template>. hostDirectivescompose des comportements,exportAsnomme une directive,inject()retrouve une directive parente.- On teste une directive à travers un composant hôte de test.
Un composant dit « quoi afficher » ; une directive dit « comment se comporter ». Écrite une fois, elle s'applique à n'importe quel élément.