Cours Angular · Module 14: Patterns avancés
Les directives personnalisées
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. Que doit-on injecter pour créer une directive structurelle personnalisée ?
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) :