Cours Angular · Module 13: UI et styles
Le style dans Angular - La cérémonie du thé
Dans cette leçon4
Le style est comme la cérémonie du thé 茶道 (chado) - il exige harmonie, simplicité et attention à chaque détail. Dans un pavillon de thé traditionnel, chaque élément a sa place et sa signification. Il en va de même pour ton application !
Encapsulation des styles
Angular propose trois modes d'encapsulation des styles :
1import { ViewEncapsulation } from '@angular/core';
2
3@Component({
4 selector: 'app-tea-house',
5 encapsulation: ViewEncapsulation.Emulated, // Default
6 template: `<div class="tea-room">...</div>`,
7 styles: [`
8 :host {
9 display: block;
10 padding: 20px;
11 background: #f5f5dc;
12 }
13
14 :host(.highlighted) {
15 border: 2px solid gold;
16 }
17
18 :host-context(.dark-theme) {
19 background: #2d2d2d;
20 }
21
22 .tea-room {
23 /* Local styles for this component */
24 }
25 `]
26})
27export class TeaHouseComponent {}Modes d'encapsulation :
| Mode | Description |
|---|---|
| Emulated | Encapsulation émulée (par défaut) - ajoute des attributs uniques |
| ShadowDom | Véritable Shadow DOM - isolation complète |
| None | Aucune encapsulation - les styles sont globaux |
Fichiers de style
1@Component({
2 selector: 'app-dojo',
3 templateUrl: './dojo.component.html',
4 styleUrls: ['./dojo.component.scss'] // External SCSS file
5})
6export class DojoComponent {}SCSS/SASS dans Angular
1// dojo.component.scss
2
3// Variables
4$primary-color: #c41e3a;
5$samurai-gold: #ffd700;
6
7// Mixins
8@mixin card-shadow {
9 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
10 transition: box-shadow 0.3s ease;
11
12 &:hover {
13 box-shadow: 0 8px 12px rgba(0, 0, 0, 0.2);
14 }
15}
16
17// Components
18.samurai-card {
19 @include card-shadow;
20 padding: 1rem;
21 border-radius: 8px;
22 background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%);
23
24 &__header {
25 color: $primary-color;
26 border-bottom: 2px solid $samurai-gold;
27 }
28
29 &__content {
30 margin-top: 1rem;
31 }
32
33 &--highlighted {
34 border: 2px solid $samurai-gold;
35 }
36}Variables CSS et thématisation
1// styles.scss (global)
2:root {
3 --primary-color: #c41e3a;
4 --secondary-color: #1e3a8a;
5 --background: #ffffff;
6 --text-color: #333333;
7 --border-radius: 8px;
8}
9
10[data-theme="dark"] {
11 --primary-color: #ff6b6b;
12 --secondary-color: #4dabf7;
13 --background: #1a1a2e;
14 --text-color: #eaeaea;
15}
16
17// Usage in component
18.samurai-card {
19 background: var(--background);
20 color: var(--text-color);
21 border-radius: var(--border-radius);
22}1// Theme service
2@Injectable({ providedIn: 'root' })
3export class ThemeService {
4 private isDark = signal(false);
5
6 toggleTheme(): void {
7 this.isDark.update(v => !v);
8 document.documentElement.setAttribute(
9 'data-theme',
10 this.isDark() ? 'dark' : 'light'
11 );
12 }
13}Code de cette leçon : src/app/app.component.ts
1import { Component, signal } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { trigger, state, style, animate, transition, query, stagger } from '@angular/animations';
4
5@Component({
6 selector: 'app-root',
7 standalone: true,
8 imports: [CommonModule],
9 animations: [
10 // Fade animation
11 trigger('fadeInOut', [
12 transition(':enter', [
13 style({ opacity: 0, transform: 'translateY(-20px)' }),
14 animate('300ms ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
15 ]),
16 transition(':leave', [
17 animate('200ms ease-in', style({ opacity: 0, transform: 'translateY(20px)' }))
18 ])
19 ]),
20
21 // Staggered list animation
22 trigger('listAnimation', [
23 transition('* => *', [
24 query(':enter', [
25 style({ opacity: 0, transform: 'translateX(-50px)' }),
26 stagger(100, [
27 animate('300ms ease-out', style({ opacity: 1, transform: 'translateX(0)' }))
28 ])
29 ], { optional: true })
30 ])
31 ]),
32
33 // Toggle animation
34 trigger('expandCollapse', [
35 state('collapsed', style({
36 height: '0',
37 overflow: 'hidden',
38 opacity: 0
39 })),
40 state('expanded', style({
41 height: '*',
42 opacity: 1
43 })),
44 transition('collapsed <=> expanded', [
45 animate('300ms ease-in-out')
46 ])
47 ])
48 ],
49 template: `
50 <div class="tea-ceremony">
51 <h1>Cérémonie du Thé - Animations Angular</h1>
52
53 <section class="fade-demo">
54 <h2>Fondu entrant/sortant</h2>
55 <button (click)="toggleMessage()">
56 {{ showMessage() ? '{{ showMessage() ? 'Masquer' : 'Afficher' }} le message
57 </button>
58
59 @if (showMessage()) {
60 <div class="message-box" @fadeInOut>
61 <p>Bienvenue à la cérémonie du thé, guerrier !</p>
62 </div>
63 }
64 </section>
65
66 <section class="list-demo">
67 <h2>Liste avec Stagger</h2>
68 <button (click)="addSamurai()">Ajouter un samouraï</button>
69 <button (click)="clearSamurai()">Effacer</button>
70
71 <div class="samurai-list" [@listAnimation]="samuraiList().length">
72 @for (samurai of samuraiList(); track samurai.id) {
73 <div class="samurai-card" @fadeInOut>
74 <span class="rank">{{ samurai.rank }}</span>
75 <span class="name">{{ samurai.name }}</span>
76 <button class="remove" (click)="removeSamurai(samurai.id)">×</button>
77 </div>
78 }
79 </div>
80 </section>
81
82 <section class="expand-demo">
83 <h2>Déplier/Replier</h2>
84
85 @for (item of accordionItems(); track item.id) {
86 <div class="accordion-item">
87 <div class="accordion-header" (click)="toggleAccordion(item.id)">
88 <span>{{ item.title }}</span>
89 <span class="icon">{{ item.expanded ? '▼' : '▶' }}</span>
90 </div>
91 <div class="accordion-content"
92 [@expandCollapse]="item.expanded ? 'expanded' : 'collapsed'">
93 <p>{{ item.content }}</p>
94 </div>
95 </div>
96 }
97 </section>
98
99 <section class="code-section">
100 <h2>Code d'animation</h2>
101 <pre><code>{{ animationCode }}</code></pre>
102 </section>
103 </div>
104 `,
105 styles: [`
106 .tea-ceremony {
107 font-family: 'Segoe UI', sans-serif;
108 padding: 20px;
109 background: linear-gradient(135deg, #1a472a 0%, #2d5a3d 100%);
110 min-height: 100vh;
111 color: #f5f5dc;
112 }
113
114 h1 {
115 text-align: center;
116 color: #90ee90;
117 }
118
119 h2 {
120 color: #90ee90;
121 border-bottom: 2px solid #90ee90;
122 padding-bottom: 10px;
123 }
124
125 section {
126 background: rgba(0,0,0,0.3);
127 border-radius: 10px;
128 padding: 20px;
129 margin: 20px 0;
130 }
131
132 button {
133 padding: 10px 20px;
134 background: #90ee90;
135 color: #1a472a;
136 border: none;
137 border-radius: 5px;
138 font-weight: bold;
139 cursor: pointer;
140 margin: 5px;
141 }
142
143 button:hover {
144 background: #7ccd7c;
145 }
146
147 .message-box {
148 background: rgba(144, 238, 144, 0.2);
149 border: 2px solid #90ee90;
150 border-radius: 10px;
151 padding: 20px;
152 margin-top: 15px;
153 text-align: center;
154 }
155
156 .samurai-list {
157 margin-top: 15px;
158 }
159
160 .samurai-card {
161 display: flex;
162 align-items: center;
163 gap: 15px;
164 padding: 12px;
165 margin: 8px 0;
166 background: rgba(0,0,0,0.3);
167 border-radius: 8px;
168 border-left: 4px solid #90ee90;
169 }
170
171 .samurai-card .rank {
172 background: #90ee90;
173 color: #1a472a;
174 padding: 4px 10px;
175 border-radius: 4px;
176 font-size: 0.85em;
177 }
178
179 .samurai-card .name {
180 flex: 1;
181 }
182
183 .samurai-card .remove {
184 padding: 5px 10px;
185 background: #e74c3c;
186 color: white;
187 border-radius: 3px;
188 }
189
190 .accordion-item {
191 margin: 10px 0;
192 border-radius: 8px;
193 overflow: hidden;
194 }
195
196 .accordion-header {
197 display: flex;
198 justify-content: space-between;
199 align-items: center;
200 padding: 15px;
201 background: rgba(144, 238, 144, 0.2);
202 cursor: pointer;
203 transition: background 0.3s;
204 }
205
206 .accordion-header:hover {
207 background: rgba(144, 238, 144, 0.3);
208 }
209
210 .accordion-content {
211 background: rgba(0,0,0,0.3);
212 padding: 0 15px;
213 }
214
215 .accordion-content p {
216 margin: 15px 0;
217 }
218
219 pre {
220 background: #0a1f0f;
221 padding: 15px;
222 border-radius: 8px;
223 overflow-x: auto;
224 border-left: 4px solid #90ee90;
225 }
226
227 code {
228 color: #f5f5dc;
229 font-family: 'Fira Code', monospace;
230 font-size: 0.85em;
231 }
232 `]
233})
234export class AppComponent {
235 showMessage = signal(false);
236 nextId = signal(1);
237
238 samuraiList = signal<Samurai[]>([]);
239
240 accordionItems = signal([
241 { id: 1, title: 'À propos de la cérémonie du thé', expanded: false, content: "La cérémonie du thé (chado) est une tradition japonaise de préparation et de service du thé matcha. Elle symbolise l'harmonie, le respect, la pureté et la tranquillité." },
242 { id: 2, title: 'Code Bushido', expanded: false, content: "Le Bushido est le code d'honneur des samouraïs, regroupant des valeurs telles que la loyauté, le courage, l'honneur, le respect et l'honnêteté." },
243 { id: 3, title: 'Architecture des châteaux', expanded: false, content: 'Les châteaux japonais (shiro) sont des fortifications construites principalement en bois et en pierre. Ils comportent des tours à plusieurs niveaux et des systèmes défensifs complexes.' }
244 ]);
245
246 animationCode = `// Définition de l'animation dans @Component
247animations: [
248 trigger('fadeInOut', [
249 transition(':enter', [
250 style({ opacity: 0, transform: 'translateY(-20px)' }),
251 animate('300ms ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
252 ]),
253 transition(':leave', [
254 animate('200ms ease-in', style({ opacity: 0, transform: 'translateY(20px)' }))
255 ])
256 ]),
257
258 trigger('listAnimation', [
259 transition('* => *', [
260 query(':enter', [
261 style({ opacity: 0, transform: 'translateX(-50px)' }),
262 stagger(100, [
263 animate('300ms ease-out', style({ opacity: 1, transform: 'translateX(0)' }))
264 ])
265 ], { optional: true })
266 ])
267 ])
268]
269
270// Utilisation dans le template
271@if (showMessage()) {
272 <div @fadeInOut>Message</div>
273}
274
275<div [@listAnimation]="items.length">
276 @for (item of items; track item.id) {
277 <div @fadeInOut>{{ item.name }}</div>
278 }
279</div>`;
280
281 toggleMessage(): void {
282 this.showMessage.update(v => !v);
283 }
284
285 addSamurai(): void {
286 const names = ['Musashi', 'Kojiro', 'Hanzo', 'Jubei', 'Masamune', 'Nobunaga'];
287 const ranks = ['Ronin', 'Hatamoto', 'Daimyo', 'Shogun'];
288
289 const newSamurai: Samurai = {
290 id: this.nextId(),
291 name: names[Math.floor(Math.random() * names.length)],
292 rank: ranks[Math.floor(Math.random() * ranks.length)]
293 };
294
295 this.samuraiList.update(list => [...list, newSamurai]);
296 this.nextId.update(id => id + 1);
297 }
298
299 removeSamurai(id: number): void {
300 this.samuraiList.update(list => list.filter(s => s.id !== id));
301 }
302
303 clearSamurai(): void {
304 this.samuraiList.set([]);
305 }
306
307 toggleAccordion(id: number): void {
308 this.accordionItems.update(items =>
309 items.map(item =>
310 item.id === id ? { ...item, expanded: !item.expanded } : item
311 )
312 );
313 }
314}
315
316interface Samurai {
317 id: number;
318 name: string;
319 rank: string;
320}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 fait ViewEncapsulation.Emulated dans Angular ?
2. Quel mode de ViewEncapsulation rend les styles d'un composant globaux (sans isolation) ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Construis un sélecteur qui applique un style à l'élément hôte du composant avec la classe .highlighted : :host(.highlighted)
- Éditeur de code
Complète le composant avec sa propre encapsulation des styles.
- Tri horizontal
Ordonne sur une seule ligne la déclaration d'une variable SCSS contenant la couleur du clan.
- Tri vertical
Ordonne les lignes d'une règle SCSS qui inclut un mixin et définit un padding (de haut en bas dans l'éditeur).