Cours Angular · Module 14: Patterns avancés

Les directives personnalisées

2 min de lecture
Dans cette leçon3

Les directives sont comme les techniques secrètes des ninjas - elles étendent les capacités des éléments.

Directive d'attribut

1@Directive({
2  selector: '[appHighlight]',
3  standalone: true
4})
5export class HighlightDirective {
6  private el = inject(ElementRef);
7  private renderer = inject(Renderer2);
8
9  // Input with alias (directive name)
10  @Input() appHighlight = '#ffff00';
11  @Input() highlightOnFocus = false;
12
13  @HostListener('mouseenter')
14  onMouseEnter(): void {
15    this.setBackground(this.appHighlight);
16  }
17
18  @HostListener('mouseleave')
19  onMouseLeave(): void {
20    this.setBackground('');
21  }
22
23  @HostListener('focus')
24  onFocus(): void {
25    if (this.highlightOnFocus) {
26      this.setBackground(this.appHighlight);
27    }
28  }
29
30  private setBackground(color: string): void {
31    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);
32  }
33}
34
35// Usage
36<p appHighlight="#ffff00">Surlignage jaune</p>  <!-- un simple <p appHighlight> passerait une chaîne vide à l'input au lieu de sa valeur par défaut -->
37<p [appHighlight]="'#c41e3a'">Surlignage rouge</p>
38<input [appHighlight]="'#90EE90'" [highlightOnFocus]="true">

Directive structurelle

1@Directive({
2  selector: '[appRepeat]',
3  standalone: true
4})
5export class RepeatDirective {
6  private templateRef = inject(TemplateRef<any>);
7  private viewContainer = inject(ViewContainerRef);
8
9  @Input() set appRepeat(count: number) {
10    this.viewContainer.clear();
11
12    for (let i = 0; i < count; i++) {
13      this.viewContainer.createEmbeddedView(this.templateRef, {
14        $implicit: i,  // Available as let-i
15        index: i,
16        first: i === 0,
17        last: i === count - 1
18      });
19    }
20  }
21}
22
23// Usage
24<div *appRepeat="5; let i; let isFirst = first">
25  Element {{ i }} {{ isFirst ? '(first)' : '' }}
26</div>

Directive de permission

1@Directive({
2  selector: '[appHasPermission]',
3  standalone: true
4})
5export class HasPermissionDirective {
6  private templateRef = inject(TemplateRef<any>);
7  private viewContainer = inject(ViewContainerRef);
8  private authService = inject(AuthService);
9
10  private hasView = false;
11
12  @Input() set appHasPermission(permission: string | string[]) {
13    const permissions = Array.isArray(permission) ? permission : [permission];
14    const hasPermission = this.authService.hasAnyPermission(permissions);
15
16    if (hasPermission && !this.hasView) {
17      this.viewContainer.createEmbeddedView(this.templateRef);
18      this.hasView = true;
19    } else if (!hasPermission && this.hasView) {
20      this.viewContainer.clear();
21      this.hasView = false;
22    }
23  }
24}
25
26// Usage
27<button *appHasPermission="'admin'">Admin Panel</button>
28<div *appHasPermission="['editor', 'admin']">Edit Content</div>
Code de cette leçon : src/app/app.component.ts
1import { Component, signal, Directive, ElementRef, Renderer2, inject, HostListener, input, Pipe, PipeTransform } from '@angular/core';
2import { CommonModule } from '@angular/common';
3
4// Directive personnalisée - Surbrillance
5@Directive({
6  selector: '[appHighlight]',
7  standalone: true
8})
9export class HighlightDirective {
10  private el = inject(ElementRef);
11  private renderer = inject(Renderer2);
12
13  appHighlight = input('#ffff00');
14
15  @HostListener('mouseenter')
16  onMouseEnter(): void {
17    this.setBackground(this.appHighlight());
18  }
19
20  @HostListener('mouseleave')
21  onMouseLeave(): void {
22    this.setBackground('');
23  }
24
25  private setBackground(color: string): void {
26    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);
27    this.renderer.setStyle(this.el.nativeElement, 'transition', 'background-color 0.3s');
28  }
29}
30
31// Pipe personnalisé - Truncate
32@Pipe({
33  name: 'truncate',
34  standalone: true
35})
36export class TruncatePipe implements PipeTransform {
37  transform(value: string, limit = 50, trail = '...'): string {
38    if (!value) return '';
39    if (value.length <= limit) return value;
40    return value.substring(0, limit) + trail;
41  }
42}
43
44// Pipe personnalisé - Temps relatif
45@Pipe({
46  name: 'relativeTime',
47  standalone: true
48})
49export class RelativeTimePipe implements PipeTransform {
50  transform(value: Date | string | number): string {
51    const date = new Date(value);
52    const now = new Date();
53    const diffMs = now.getTime() - date.getTime();
54    const diffMins = Math.floor(diffMs / 60000);
55    const diffHours = Math.floor(diffMs / 3600000);
56    const diffDays = Math.floor(diffMs / 86400000);
57
58    if (diffMins < 1) return "à l'instant";
59    if (diffMins < 60) return `il y a ${diffMins} min`;
60    if (diffHours < 24) return `il y a ${diffHours} heures`;
61    if (diffDays < 7) return `il y a ${diffDays} jours`;
62
63    return date.toLocaleDateString('fr-FR');
64  }
65}
66
67@Component({
68  selector: 'app-root',
69  standalone: true,
70  imports: [CommonModule, HighlightDirective, TruncatePipe, RelativeTimePipe],
71  template: `
72    <div class="advanced-dojo">
73      <h1>Directives et Pipes personnalisés</h1>
74
75      <section>
76        <h2>Directive personnalisée - Surbrillance</h2>
77
78        <div class="directive-demo">
79          <p appHighlight>
80            Survolez-moi ! (couleur par défaut : jaune)
81          </p>
82
83          <p [appHighlight]="'#e94560'">
84            Survolez-moi ! (couleur : rose)
85          </p>
86
87          <p [appHighlight]="'#2ecc71'">
88            Survolez-moi ! (couleur : vert)
89          </p>
90        </div>
91
92        <pre><code>{{ directiveCode }}</code></pre>
93      </section>
94
95      <section>
96        <h2>Pipe personnalisé - Truncate</h2>
97
98        <div class="pipe-demo">
99          <p><strong>Texte original :</strong></p>
100          <p class="original">{{ longText }}</p>
101
102          <p><strong>Tronqué (50 caractères) :</strong></p>
103          <p class="truncated">{{ longText | truncate }}</p>
104
105          <p><strong>Tronqué (30 caractères, ">>>") :</strong></p>
106          <p class="truncated">{{ longText | truncate:30:'>>>' }}</p>
107        </div>
108      </section>
109
110      <section>
111        <h2>⏰ Pipe personnalisé - Temps relatif</h2>
112
113        <div class="time-demo">
114          @for (event of events(); track event.id) {
115            <div class="event-item">
116              <span class="event-name">{{ event.name }}</span>
117              <span class="event-time">{{ event.date | relativeTime }}</span>
118            </div>
119          }
120        </div>
121      </section>
122
123      <section>
124        <h2>Code des Pipes</h2>
125        <pre><code>{{ pipeCode }}</code></pre>
126      </section>
127    </div>
128  `,
129  styles: [`
130    .advanced-dojo {
131      font-family: 'Segoe UI', sans-serif;
132      padding: 20px;
133      background: linear-gradient(135deg, #2d1b4e 0%, #1a1a2e 100%);
134      min-height: 100vh;
135      color: #eee;
136    }
137
138    h1 {
139      text-align: center;
140      color: #a855f7;
141    }
142
143    h2 {
144      color: #a855f7;
145      border-bottom: 2px solid #a855f7;
146      padding-bottom: 10px;
147    }
148
149    section {
150      background: rgba(255,255,255,0.05);
151      border-radius: 10px;
152      padding: 20px;
153      margin: 20px 0;
154    }
155
156    .directive-demo p {
157      padding: 15px;
158      margin: 10px 0;
159      border-radius: 8px;
160      background: rgba(0,0,0,0.3);
161      cursor: pointer;
162      transition: all 0.3s;
163    }
164
165    .pipe-demo .original {
166      background: rgba(0,0,0,0.3);
167      padding: 15px;
168      border-radius: 8px;
169    }
170
171    .pipe-demo .truncated {
172      background: rgba(168, 85, 247, 0.2);
173      border: 1px solid #a855f7;
174      padding: 15px;
175      border-radius: 8px;
176    }
177
178    .time-demo {
179      display: flex;
180      flex-direction: column;
181      gap: 10px;
182    }
183
184    .event-item {
185      display: flex;
186      justify-content: space-between;
187      padding: 12px;
188      background: rgba(0,0,0,0.3);
189      border-radius: 8px;
190      border-left: 4px solid #a855f7;
191    }
192
193    .event-name {
194      font-weight: bold;
195    }
196
197    .event-time {
198      color: #a855f7;
199    }
200
201    pre {
202      background: #0a0a15;
203      padding: 15px;
204      border-radius: 8px;
205      overflow-x: auto;
206      border-left: 4px solid #a855f7;
207    }
208
209    code {
210      color: #e6e6e6;
211      font-family: 'Fira Code', monospace;
212      font-size: 0.85em;
213    }
214  `]
215})
216export class AppComponent {
217  longText = 'Miyamoto Musashi était un épéiste, philosophe et artiste japonais légendaire. Il a écrit le « Traité des cinq roues », un texte classique sur la stratégie et les arts martiaux.';
218
219  events = signal([
220    { id: 1, name: 'Entraînement matinal', date: new Date() },
221    { id: 2, name: 'Méditation', date: new Date(Date.now() - 30 * 60000) },
222    { id: 3, name: "Combat d'entraînement", date: new Date(Date.now() - 3 * 3600000) },
223    { id: 4, name: 'Rencontre avec le maître', date: new Date(Date.now() - 2 * 86400000) },
224    { id: 5, name: 'Cérémonie du thé', date: new Date(Date.now() - 10 * 86400000) }
225  ]);
226
227  directiveCode = `@Directive({
228  selector: '[appHighlight]',
229  standalone: true
230})
231export class HighlightDirective {
232  private el = inject(ElementRef);
233  private renderer = inject(Renderer2);
234
235  appHighlight = input('#ffff00');
236
237  @HostListener('mouseenter')
238  onMouseEnter(): void {
239    this.setBackground(this.appHighlight());
240  }
241
242  @HostListener('mouseleave')
243  onMouseLeave(): void {
244    this.setBackground('');
245  }
246
247  private setBackground(color: string): void {
248    this.renderer.setStyle(
249      this.el.nativeElement,
250      'backgroundColor',
251      color
252    );
253  }
254}
255
256// Utilisation
257<p appHighlight>Jaune par défaut</p>
258<p [appHighlight]="'#e94560'">Rose</p>`;
259
260  pipeCode = `@Pipe({
261  name: 'truncate',
262  standalone: true
263})
264export class TruncatePipe implements PipeTransform {
265  transform(value: string, limit = 50, trail = '...'): string {
266    if (!value) return '';
267    if (value.length <= limit) return value;
268    return value.substring(0, limit) + trail;
269  }
270}
271
272// Utilisation
273{{ longText | truncate }}
274{{ longText | truncate:30:'>>>' }}
275
276@Pipe({
277  name: 'relativeTime',
278  standalone: true
279})
280export class RelativeTimePipe implements PipeTransform {
281  transform(value: Date | string | number): string {
282    const date = new Date(value);
283    const now = new Date();
284    const diffMs = now.getTime() - date.getTime();
285    const diffMins = Math.floor(diffMs / 60000);
286
287    if (diffMins < 1) return "à l'instant";
288    if (diffMins < 60) return \`il y a \${diffMins} min\`;
289    // ...
290  }
291}
292
293// Utilisation
294{{ event.date | relativeTime }}`;
295}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que doit-on injecter pour créer une directive structurelle personnalisée ?

  2. 2. Une directive qui change l'arrière-plan d'un élément au survol de la souris (appHighlight) est un exemple de :

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri horizontal

    Ordonne sur une seule ligne le décorateur qui écoute l'événement mouseenter sur l'élément hôte :

  • Éditeur de code

    Complète la directive highlight qui change l'arrière-plan au survol.

  • Tri vertical

    Ordonne les lignes de la définition d'une directive d'attribut (de haut en bas) :

Articles utiles