Cours Angular · Module 13: UI et styles

Le style dans Angular - La cérémonie du thé

2 min de lecture
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 :

ModeDescription
EmulatedEncapsulation émulée (par défaut) - ajoute des attributs uniques
ShadowDomVéritable Shadow DOM - isolation complète
NoneAucune 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. 1. Que fait ViewEncapsulation.Emulated dans Angular ?

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

Articles utiles