Cours Angular · Module 14: Patterns avancés

Techniques avancées - Le sommet du mont Fuji

2 min de lecture
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. 1. À quoi sert l'attribut select sur <ng-content> ?

  2. 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) :

Articles utiles