Cours Angular · Module 7: RxJS

RxJS dans Angular - Les éclaireurs ninja

2 min de lecture
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. 1. Quelle fonction RxJS crée un Observable qui émet des valeurs à intervalle de temps fixe ?

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

Articles utiles