Cours Angular · Module 14: Patterns avancés
Techniques avancées - Le sommet du mont Fuji
Dans cette leçon4
Au sommet du mont Fuji 富士山 - la montagne sacrée du Japon - tu apprendras les techniques de sabre les plus élevées. C'est l'épreuve ultime avant d'obtenir le titre de maître Angular !
Projection de contenu (ng-content)
La projection de contenu est comme la technique de transmettre un sabre au successeur - elle permet aux composants d'accepter un contenu arbitraire :
1// Simple slot
2@Component({
3 selector: 'app-panel',
4 template: `
5 <div class="panel">
6 <ng-content></ng-content>
7 </div>
8 `
9})
10export class PanelComponent {}
11
12// Usage
13<app-panel>
14 <p>Any content</p>
15</app-panel>Projection de contenu multi-emplacements
1@Component({
2 selector: 'app-card',
3 template: `
4 <article class="card">
5 <header class="card-header">
6 <ng-content select="[card-header]"></ng-content>
7 </header>
8
9 <main class="card-body">
10 <ng-content></ng-content>
11 </main>
12
13 <footer class="card-footer">
14 <ng-content select="[card-footer]"></ng-content>
15 </footer>
16 </article>
17 `
18})
19export class CardComponent {}
20
21// Usage
22<app-card>
23 <h2 card-header>Samurai Profile</h2>
24
25 <p>Main card content - samurai description...</p>
26 <img src="samurai.jpg" alt="Samurai">
27
28 <div card-footer>
29 <button>Edit</button>
30 <button>Delete</button>
31 </div>
32</app-card>Contenu conditionnel avec @if
1@Component({
2 selector: 'app-expandable',
3 template: `
4 <div class="header" (click)="toggle()">
5 <ng-content select="[header]"></ng-content>
6 <span>{{ isExpanded() ? '▼' : '▶' }}</span>
7 </div>
8
9 @if (isExpanded()) {
10 <div class="content">
11 <ng-content></ng-content>
12 </div>
13 }
14 `
15})
16export class ExpandableComponent {
17 isExpanded = signal(false);
18
19 toggle(): void {
20 this.isExpanded.update(v => !v);
21 }
22}ContentChild et ContentChildren
1@Component({
2 selector: 'app-tabs',
3 template: `
4 <div class="tab-headers">
5 @for (tab of tabs(); track tab.title) {
6 <button
7 [class.active]="tab === activeTab()"
8 (click)="selectTab(tab)">
9 {{ tab.title }}
10 </button>
11 }
12 </div>
13 <div class="tab-content">
14 <ng-content></ng-content>
15 </div>
16 `
17})
18export class TabsComponent implements AfterContentInit {
19 @ContentChildren(TabComponent) tabsList!: QueryList<TabComponent>;
20
21 tabs = signal<TabComponent[]>([]);
22 activeTab = signal<TabComponent | null>(null);
23
24 ngAfterContentInit(): void {
25 this.tabs.set(this.tabsList.toArray());
26 if (this.tabs().length > 0) {
27 this.selectTab(this.tabs()[0]);
28 }
29 }
30
31 selectTab(tab: TabComponent): void {
32 this.tabs().forEach(t => t.isActive.set(false));
33 tab.isActive.set(true);
34 this.activeTab.set(tab);
35 }
36}
37
38@Component({
39 selector: 'app-tab',
40 template: `
41 @if (isActive()) {
42 <ng-content></ng-content>
43 }
44 `
45})
46export class TabComponent {
47 @Input() title = '';
48 isActive = signal(false);
49}Code de cette leçon : src/app/app.component.ts
1import { Component, signal, contentChildren, AfterContentInit, input, TemplateRef, ViewContainerRef, Directive, inject } from '@angular/core';
2import { CommonModule } from '@angular/common';
3
4// Composant Onglet
5@Component({
6 selector: 'app-tab',
7 standalone: true,
8 template: `
9 @if (isActive()) {
10 <ng-content></ng-content>
11 }
12 `
13})
14export class TabComponent {
15 title = input.required<string>();
16 isActive = signal(false);
17}
18
19// Composant Conteneur d'Onglets
20@Component({
21 selector: 'app-tabs',
22 standalone: true,
23 imports: [CommonModule],
24 template: `
25 <div class="tabs-container">
26 <div class="tab-headers">
27 @for (tab of tabs(); track $index) {
28 <button
29 class="tab-header"
30 [class.active]="tab.isActive()"
31 (click)="selectTab(tab)"
32 >
33 {{ tab.title() }}
34 </button>
35 }
36 </div>
37 <div class="tab-content">
38 <ng-content></ng-content>
39 </div>
40 </div>
41 `,
42 styles: [`
43 .tabs-container {
44 border: 2px solid #e94560;
45 border-radius: 10px;
46 overflow: hidden;
47 }
48 .tab-headers {
49 display: flex;
50 background: rgba(0,0,0,0.3);
51 }
52 .tab-header {
53 flex: 1;
54 padding: 12px;
55 background: transparent;
56 border: none;
57 color: #888;
58 cursor: pointer;
59 transition: all 0.3s;
60 }
61 .tab-header:hover {
62 background: rgba(233, 69, 96, 0.2);
63 }
64 .tab-header.active {
65 background: #e94560;
66 color: white;
67 }
68 .tab-content {
69 padding: 20px;
70 }
71 `]
72})
73export class TabsComponent implements AfterContentInit {
74 tabsList = contentChildren(TabComponent);
75 tabs = signal<TabComponent[]>([]);
76
77 ngAfterContentInit(): void {
78 this.tabs.set(this.tabsList().slice());
79 if (this.tabs().length > 0) {
80 this.selectTab(this.tabs()[0]);
81 }
82 }
83
84 selectTab(selectedTab: TabComponent): void {
85 this.tabs().forEach(tab => tab.isActive.set(false));
86 selectedTab.isActive.set(true);
87 }
88}
89
90// Composant Carte avec Projection de Contenu
91@Component({
92 selector: 'app-card',
93 standalone: true,
94 template: `
95 <article class="card">
96 <header class="card-header">
97 <ng-content select="[card-header]"></ng-content>
98 </header>
99 <main class="card-body">
100 <ng-content></ng-content>
101 </main>
102 <footer class="card-footer">
103 <ng-content select="[card-footer]"></ng-content>
104 </footer>
105 </article>
106 `,
107 styles: [`
108 .card {
109 background: rgba(0,0,0,0.3);
110 border-radius: 10px;
111 border: 1px solid #e94560;
112 overflow: hidden;
113 }
114 .card-header {
115 background: rgba(233, 69, 96, 0.2);
116 padding: 15px;
117 border-bottom: 1px solid #e94560;
118 }
119 .card-body {
120 padding: 20px;
121 }
122 .card-footer {
123 background: rgba(0,0,0,0.2);
124 padding: 15px;
125 border-top: 1px solid rgba(233, 69, 96, 0.3);
126 }
127 `]
128})
129export class CardComponent {}
130
131@Component({
132 selector: 'app-root',
133 standalone: true,
134 imports: [CommonModule, TabsComponent, TabComponent, CardComponent],
135 template: `
136 <div class="mastery-dojo">
137 <h1>Maîtrise du Sabre - Projection de Contenu</h1>
138
139 <section>
140 <h2>Onglets avec ContentChildren</h2>
141
142 <app-tabs>
143 <app-tab title="Profil du Samouraï">
144 <h3>Miyamoto Musashi</h3>
145 <p>Samouraï légendaire et maître du sabre, auteur du « Traité des cinq roues ».</p>
146 <p>Style de combat : Niten Ichi-ryū (deux sabres)</p>
147 </app-tab>
148 <app-tab title="Compétences">
149 <h3>Compétences de combat</h3>
150 <ul>
151 <li>Épée : ████████░░ 80%</li>
152 <li>Stratégie : ██████████ 100%</li>
153 <li>Méditation : ███████░░░ 70%</li>
154 </ul>
155 </app-tab>
156 <app-tab title="Équipement">
157 <h3>Arsenal</h3>
158 <ul>
159 <li>Katana - « Musashi »</li>
160 <li>Wakizashi - « Kojiro »</li>
161 <li>Bokken (sabre en bois)</li>
162 </ul>
163 </app-tab>
164 </app-tabs>
165 </section>
166
167 <section>
168 <h2>Projection de contenu multi-slots</h2>
169
170 <div class="cards-grid">
171 <app-card>
172 <h3 card-header>Katana</h3>
173 <p>Sabre japonais traditionnel à lame courbe.</p>
174 <p>Longueur : 60-80 cm</p>
175 <div card-footer>
176 <button>Choisir</button>
177 <span>+50 dégâts</span>
178 </div>
179 </app-card>
180
181 <app-card>
182 <h3 card-header>Yumi (Arc)</h3>
183 <p>Arc japonais asymétrique utilisé par les samouraïs.</p>
184 <p>Portée : 200 m</p>
185 <div card-footer>
186 <button>Choisir</button>
187 <span>+30 portée</span>
188 </div>
189 </app-card>
190
191 <app-card>
192 <h3 card-header>Armure</h3>
193 <p>Armure traditionnelle de samouraï (yoroi).</p>
194 <p>Protection : élevée</p>
195 <div card-footer>
196 <button>Choisir</button>
197 <span>+100 défense</span>
198 </div>
199 </app-card>
200 </div>
201 </section>
202
203 <section>
204 <h2>Code</h2>
205 <pre><code>{{ codeExample }}</code></pre>
206 </section>
207 </div>
208 `,
209 styles: [`
210 .mastery-dojo {
211 font-family: 'Segoe UI', sans-serif;
212 padding: 20px;
213 background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
214 min-height: 100vh;
215 color: #eee;
216 }
217
218 h1 {
219 text-align: center;
220 color: #e94560;
221 }
222
223 h2 {
224 color: #e94560;
225 border-bottom: 2px solid #e94560;
226 padding-bottom: 10px;
227 }
228
229 section {
230 background: rgba(255,255,255,0.05);
231 border-radius: 10px;
232 padding: 20px;
233 margin: 20px 0;
234 }
235
236 .cards-grid {
237 display: grid;
238 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
239 gap: 20px;
240 }
241
242 button {
243 padding: 8px 16px;
244 background: #e94560;
245 color: white;
246 border: none;
247 border-radius: 5px;
248 cursor: pointer;
249 margin-right: 10px;
250 }
251
252 button:hover {
253 background: #ff6b6b;
254 }
255
256 pre {
257 background: #0a0a15;
258 padding: 15px;
259 border-radius: 8px;
260 overflow-x: auto;
261 border-left: 4px solid #e94560;
262 }
263
264 code {
265 color: #e6e6e6;
266 font-family: 'Fira Code', monospace;
267 font-size: 0.85em;
268 }
269 `]
270})
271export class AppComponent {
272 codeExample = `// Projection de contenu multi-slots
273@Component({
274 selector: 'app-card',
275 template: \`
276 <article class="card">
277 <header>
278 <ng-content select="[card-header]"></ng-content>
279 </header>
280 <main>
281 <ng-content></ng-content>
282 </main>
283 <footer>
284 <ng-content select="[card-footer]"></ng-content>
285 </footer>
286 </article>
287 \`
288})
289export class CardComponent {}
290
291// Utilisation
292<app-card>
293 <h3 card-header>Titre</h3>
294 <p>Contenu principal de la carte...</p>
295 <div card-footer>
296 <button>Action</button>
297 </div>
298</app-card>
299
300// ContentChildren pour un accès dynamique
301@Component({
302 selector: 'app-tabs'
303})
304export class TabsComponent implements AfterContentInit {
305 tabsList = contentChildren(TabComponent);
306
307 ngAfterContentInit() {
308 // Accès à tous les TabComponent à l'intérieur
309 this.tabsList().forEach(tab => ...);
310 }
311}`;
312}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. À quoi sert l'attribut select sur <ng-content> ?
2. Qu'est-ce que la projection de contenu dans Angular ?
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 la balise d'un emplacement unique de projection de contenu :
- Clique dans l'ordre
Clique sur les éléments dans l'ordre pour construire la balise d'ouverture de l'emplacement : <ng-content>
- Éditeur de code
Complète le composant carte avec plusieurs emplacements de projection de contenu.
- Tri vertical
Ordonne les emplacements de la carte selon leur ordre dans le template (de haut en bas) :