Cours Angular · Module 7: RxJS
RxJS dans Angular - Les éclaireurs ninja
Dans cette leçon4
RxJS (Reactive Extensions for JavaScript) est une bibliothèque de programmation réactive - comme un réseau d'éclaireurs ninja observant le terrain et rapportant instantanément les événements au quartier général.
Qu'est-ce que la programmation réactive ?
C'est un paradigme basé sur les flux de données et la propagation des changements :
- Flux - des séquences d'événements réparties dans le temps
- Observateurs - réagissent à chaque événement du flux
- Opérateurs - transforment, filtrent, combinent les flux
Observable - Un flux de données
Un Observable est une séquence de valeurs « observable » - comme un ninja surveillant un château et rapportant chaque mouvement :
1import { Observable, of, from, interval, fromEvent } from 'rxjs';
2
3// Création d'un Observable à partir de valeurs
4const numbers$ = of(1, 2, 3, 4, 5); // Émet des valeurs puis se termine
5
6// À partir d'un tableau
7const fromArray$ = from([1, 2, 3]);
8
9// Timer - émet toutes les secondes
10const timer$ = interval(1000); // 0, 1, 2, 3...
11
12// À partir d'événements DOM
13const clicks$ = fromEvent(document, 'click');
14
15// Observable personnalisé
16const custom$ = new Observable<number>(subscriber => {
17 subscriber.next(1);
18 subscriber.next(2);
19 setTimeout(() => {
20 subscriber.next(3);
21 subscriber.complete();
22 }, 1000);
23});L'abonnement - Être à l'écoute
1// Forme complète de l'abonnement
2const subscription = numbers$.subscribe({
3 next: value => console.log('Value:', value),
4 error: err => console.error('Error:', err),
5 complete: () => console.log('Completed')
6});
7
8// Forme courte - uniquement next
9numbers$.subscribe(value => console.log(value));
10
11// Annulation de l'abonnement
12subscription.unsubscribe();Convention de nommage
Dans Angular, on utilise le suffixe $ pour les Observables :
1samurai$: Observable<Samurai[]>;
2loading$: Observable<boolean>;
3error$: Observable<string | null>;Code de cette leçon : src/app/app.component.ts
1import { Component, signal, OnInit, OnDestroy } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { Subject, interval, fromEvent, merge, BehaviorSubject, Subscription } from 'rxjs';
4import { map, filter, takeUntil, debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
5import { toSignal } from '@angular/core/rxjs-interop';
6
7@Component({
8 selector: 'app-root',
9 standalone: true,
10 imports: [CommonModule],
11 template: `
12 <div class="ninja-hq">
13 <h1>Quartier général ninja - RxJS</h1>
14
15 <section class="scouts-section">
16 <h2>Éclaireurs (Observables)</h2>
17
18 <div class="scout-report">
19 <h3>Rapport du temps</h3>
20 <p>Secondes depuis le départ : {{ timeElapsed() }}</p>
21 <p>Seconde paire : {{ evenTime() ? 'Oui' : 'Non' }}</p>
22 </div>
23
24 <div class="scout-report">
25 <h3>Suivi de la souris</h3>
26 <p>Position X : {{ mouseX() }}</p>
27 <p>Position Y : {{ mouseY() }}</p>
28 <p>Zone : {{ mouseZone() }}</p>
29 </div>
30 </section>
31
32 <section class="messages-section">
33 <h2>Messages (Subject)</h2>
34
35 <div class="message-input">
36 <input
37 #messageInput
38 type="text"
39 placeholder="Écris un message..."
40 (input)="onMessageInput(messageInput.value)"
41 >
42 <button (click)="sendMessage(messageInput.value); messageInput.value = ''">
43 Envoyer
44 </button>
45 </div>
46
47 <div class="messages-log">
48 <h3>Historique des messages :</h3>
49 @for (msg of messages(); track $index) {
50 <div class="message" [class.urgent]="msg.urgent">
51 <span class="time">{{ msg.time }}</span>
52 <span class="text">{{ msg.text }}</span>
53 @if (msg.urgent) {
54 <span class="badge">URGENT</span>
55 }
56 </div>
57 }
58 </div>
59 </section>
60
61 <section class="operators-section">
62 <h2>Opérateurs RxJS</h2>
63
64 <div class="search-demo">
65 <h3>Recherche (debounce + distinctUntilChanged)</h3>
66 <input
67 type="text"
68 placeholder="Chercher un samouraï..."
69 (input)="onSearchInput($event)"
70 >
71 <p>Terme recherché : « {{ searchTerm() }} »</p>
72 <p>Nombre de recherches : {{ searchCount() }}</p>
73 </div>
74 </section>
75 </div>
76 `,
77 styles: [`
78 .ninja-hq {
79 font-family: 'Segoe UI', sans-serif;
80 padding: 20px;
81 background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
82 min-height: 100vh;
83 color: #eee;
84 }
85
86 h1 {
87 text-align: center;
88 color: #e94560;
89 text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
90 }
91
92 h2 {
93 color: #0f3460;
94 background: #e94560;
95 padding: 10px;
96 border-radius: 5px;
97 }
98
99 section {
100 background: rgba(255,255,255,0.1);
101 border-radius: 10px;
102 padding: 15px;
103 margin: 15px 0;
104 }
105
106 .scout-report {
107 background: rgba(0,0,0,0.3);
108 padding: 10px;
109 border-radius: 5px;
110 margin: 10px 0;
111 border-left: 3px solid #e94560;
112 }
113
114 .scout-report h3 {
115 margin: 0 0 10px 0;
116 color: #e94560;
117 }
118
119 .message-input {
120 display: flex;
121 gap: 10px;
122 margin-bottom: 15px;
123 }
124
125 .message-input input {
126 flex: 1;
127 padding: 10px;
128 border: none;
129 border-radius: 5px;
130 background: #0f3460;
131 color: white;
132 }
133
134 .message-input button {
135 padding: 10px 20px;
136 background: #e94560;
137 color: white;
138 border: none;
139 border-radius: 5px;
140 cursor: pointer;
141 }
142
143 .message-input button:hover {
144 background: #ff6b6b;
145 }
146
147 .messages-log {
148 max-height: 200px;
149 overflow-y: auto;
150 }
151
152 .message {
153 background: rgba(0,0,0,0.3);
154 padding: 8px;
155 margin: 5px 0;
156 border-radius: 5px;
157 display: flex;
158 gap: 10px;
159 align-items: center;
160 }
161
162 .message.urgent {
163 border: 2px solid #ff6b6b;
164 background: rgba(255,107,107,0.2);
165 }
166
167 .message .time {
168 color: #888;
169 font-size: 0.8em;
170 }
171
172 .message .badge {
173 margin-left: auto;
174 }
175
176 .search-demo input {
177 width: 100%;
178 padding: 10px;
179 border: none;
180 border-radius: 5px;
181 background: #0f3460;
182 color: white;
183 margin: 10px 0;
184 }
185 `]
186})
187export class AppComponent implements OnInit, OnDestroy {
188 private destroy$ = new Subject<void>();
189 private messageSubject = new BehaviorSubject<Message[]>([]);
190 private searchSubject = new Subject<string>();
191
192 // Signals alimentés par les Observables
193 timeElapsed = signal(0);
194 evenTime = signal(false);
195 mouseX = signal(0);
196 mouseY = signal(0);
197 mouseZone = signal('Centre');
198 messages = signal<Message[]>([]);
199 searchTerm = signal('');
200 searchCount = signal(0);
201
202 ngOnInit(): void {
203 // Observable minuteur - toutes les secondes
204 interval(1000).pipe(
205 takeUntil(this.destroy$),
206 tap(value => {
207 this.timeElapsed.set(value);
208 this.evenTime.set(value % 2 === 0);
209 })
210 ).subscribe();
211
212 // Observable de la souris
213 fromEvent<MouseEvent>(document, 'mousemove').pipe(
214 takeUntil(this.destroy$),
215 map(event => ({ x: event.clientX, y: event.clientY })),
216 tap(pos => {
217 this.mouseX.set(pos.x);
218 this.mouseY.set(pos.y);
219 this.mouseZone.set(this.getZone(pos.x, pos.y));
220 })
221 ).subscribe();
222
223 // BehaviorSubject des messages
224 this.messageSubject.pipe(
225 takeUntil(this.destroy$)
226 ).subscribe(msgs => this.messages.set(msgs));
227
228 // Recherche avec debounce
229 this.searchSubject.pipe(
230 takeUntil(this.destroy$),
231 debounceTime(300),
232 distinctUntilChanged(),
233 tap(term => {
234 this.searchTerm.set(term);
235 this.searchCount.update(c => c + 1);
236 })
237 ).subscribe();
238 }
239
240 ngOnDestroy(): void {
241 this.destroy$.next();
242 this.destroy$.complete();
243 }
244
245 private getZone(x: number, y: number): string {
246 if (x < 200 && y < 200) return '↖ Nord-Ouest';
247 if (x > 400 && y < 200) return '↗ Nord-Est';
248 if (x < 200 && y > 400) return '↙ Sud-Ouest';
249 if (x > 400 && y > 400) return '↘ Sud-Est';
250 return 'Centre';
251 }
252
253 onMessageInput(value: string): void {
254 // Ici, on pourrait ajouter un indicateur de saisie en temps réel
255 }
256
257 sendMessage(text: string): void {
258 if (!text.trim()) return;
259
260 const newMessage: Message = {
261 text,
262 time: new Date().toLocaleTimeString(),
263 urgent: text.includes('!')
264 };
265
266 const current = this.messageSubject.getValue();
267 this.messageSubject.next([...current, newMessage]);
268 }
269
270 onSearchInput(event: Event): void {
271 const input = event.target as HTMLInputElement;
272 this.searchSubject.next(input.value);
273 }
274}
275
276interface Message {
277 text: string;
278 time: string;
279 urgent: boolean;
280}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. Quelle fonction RxJS crée un Observable qui émet des valeurs à intervalle de temps fixe ?
2. Que fait la fonction of() dans RxJS ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Ordonne la déclaration de variable Observable selon la convention de nommage Angular : samurai$: Observable<Samurai[]>;
- Éditeur de code
Complète le code qui crée un Observable à partir de valeurs et s'y abonne.
- Tri horizontal
Ordonne l'import des fonctions de création d'Observable depuis la bibliothèque rxjs :
- Tri vertical
Ordonne les callbacks de l'objet observer dans l'ordre où on les déclare dans subscribe() :
- Clique dans l'ordre
Ordonne la forme courte d'abonnement à un flux : numbers$.subscribe(fn)