Kurs Angular · Moduł 7: RxJS
RxJS w Angular - Zwiadowcy Ninja
W tej lekcji4
RxJS (Reactive Extensions for JavaScript) to biblioteka do programowania reaktywnego - jak sieć ninja zwiadowców obserwujących teren i natychmiast raportujących zdarzenia do centrali.
Czym jest Programowanie Reaktywne?
To paradygmat oparty na strumieniach danych i propagacji zmian:
- Strumienie - sekwencje zdarzeń rozłożone w czasie
- Obserwatorzy - reagują na każde zdarzenie w strumieniu
- Operatory - transformują, filtrują, łączą strumienie
Observable - Strumień Danych
Observable to "obserwowalna" sekwencja wartości - jak ninja obserwujący zamek i raportujący każdy ruch:
import { Observable, of, from, interval, fromEvent } from 'rxjs';
// Tworzenie Observable z wartości
const numbers$ = of(1, 2, 3, 4, 5); // Emituje wartości i kończy
// Z tablicy
const fromArray$ = from([1, 2, 3]);
// Timer - emituje co sekundę
const timer$ = interval(1000); // 0, 1, 2, 3...
// Ze zdarzeń DOM
const clicks$ = fromEvent(document, 'click');
// Własny Observable
const custom$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
setTimeout(() => {
subscriber.next(3);
subscriber.complete();
}, 1000);
});Subskrypcja - Nasłuchiwanie
// Pełna forma subskrypcji
const subscription = numbers$.subscribe({
next: value => console.log('Wartość:', value),
error: err => console.error('Błąd:', err),
complete: () => console.log('Zakończono')
});
// Skrócona forma - tylko next
numbers$.subscribe(value => console.log(value));
// Anulowanie subskrypcji
subscription.unsubscribe();Konwencja nazewnictwa
W Angular używamy sufiksu $ dla Observable:
samurai$: Observable<Samurai[]>;
loading$: Observable<boolean>;
error$: Observable<string | null>;Kod do tej lekcji: src/app/app.component.ts
import { Component, signal, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Subject, interval, fromEvent, merge, BehaviorSubject, Subscription } from 'rxjs';
import { map, filter, takeUntil, debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: `
<div class="ninja-hq">
<h1>Kwatera Ninja - RxJS</h1>
<section class="scouts-section">
<h2>Zwiadowcy (Observables)</h2>
<div class="scout-report">
<h3>Raport czasowy</h3>
<p>Sekundy od startu: {{ timeElapsed() }}</p>
<p>Parzysty czas: {{ evenTime() ? 'Tak' : 'Nie' }}</p>
</div>
<div class="scout-report">
<h3>Śledzenie myszy</h3>
<p>Pozycja X: {{ mouseX() }}</p>
<p>Pozycja Y: {{ mouseY() }}</p>
<p>Strefa: {{ mouseZone() }}</p>
</div>
</section>
<section class="messages-section">
<h2>Wiadomości (Subject)</h2>
<div class="message-input">
<input
#messageInput
type="text"
placeholder="Wpisz wiadomość..."
(input)="onMessageInput(messageInput.value)"
>
<button (click)="sendMessage(messageInput.value); messageInput.value = ''">
Wyślij
</button>
</div>
<div class="messages-log">
<h3>Historia wiadomości:</h3>
@for (msg of messages(); track $index) {
<div class="message" [class.urgent]="msg.urgent">
<span class="time">{{ msg.time }}</span>
<span class="text">{{ msg.text }}</span>
@if (msg.urgent) {
<span class="badge">PILNE</span>
}
</div>
}
</div>
</section>
<section class="operators-section">
<h2>Operatory RxJS</h2>
<div class="search-demo">
<h3>Wyszukiwanie (debounce + distinctUntilChanged)</h3>
<input
type="text"
placeholder="Szukaj samuraja..."
(input)="onSearchInput($event)"
>
<p>Wyszukiwana fraza: "{{ searchTerm() }}"</p>
<p>Liczba wyszukań: {{ searchCount() }}</p>
</div>
</section>
</div>
`,
styles: [`
.ninja-hq {
font-family: 'Segoe UI', sans-serif;
padding: 20px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #eee;
}
h1 {
text-align: center;
color: #e94560;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
h2 {
color: #0f3460;
background: #e94560;
padding: 10px;
border-radius: 5px;
}
section {
background: rgba(255,255,255,0.1);
border-radius: 10px;
padding: 15px;
margin: 15px 0;
}
.scout-report {
background: rgba(0,0,0,0.3);
padding: 10px;
border-radius: 5px;
margin: 10px 0;
border-left: 3px solid #e94560;
}
.scout-report h3 {
margin: 0 0 10px 0;
color: #e94560;
}
.message-input {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.message-input input {
flex: 1;
padding: 10px;
border: none;
border-radius: 5px;
background: #0f3460;
color: white;
}
.message-input button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.message-input button:hover {
background: #ff6b6b;
}
.messages-log {
max-height: 200px;
overflow-y: auto;
}
.message {
background: rgba(0,0,0,0.3);
padding: 8px;
margin: 5px 0;
border-radius: 5px;
display: flex;
gap: 10px;
align-items: center;
}
.message.urgent {
border: 2px solid #ff6b6b;
background: rgba(255,107,107,0.2);
}
.message .time {
color: #888;
font-size: 0.8em;
}
.message .badge {
margin-left: auto;
}
.search-demo input {
width: 100%;
padding: 10px;
border: none;
border-radius: 5px;
background: #0f3460;
color: white;
margin: 10px 0;
}
`]
})
export class AppComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
private messageSubject = new BehaviorSubject<Message[]>([]);
private searchSubject = new Subject<string>();
// Signals from Observables
timeElapsed = signal(0);
evenTime = signal(false);
mouseX = signal(0);
mouseY = signal(0);
mouseZone = signal('Centrum');
messages = signal<Message[]>([]);
searchTerm = signal('');
searchCount = signal(0);
ngOnInit(): void {
// Timer Observable - co sekundę
interval(1000).pipe(
takeUntil(this.destroy$),
tap(value => {
this.timeElapsed.set(value);
this.evenTime.set(value % 2 === 0);
})
).subscribe();
// Mouse Observable
fromEvent<MouseEvent>(document, 'mousemove').pipe(
takeUntil(this.destroy$),
map(event => ({ x: event.clientX, y: event.clientY })),
tap(pos => {
this.mouseX.set(pos.x);
this.mouseY.set(pos.y);
this.mouseZone.set(this.getZone(pos.x, pos.y));
})
).subscribe();
// Messages BehaviorSubject
this.messageSubject.pipe(
takeUntil(this.destroy$)
).subscribe(msgs => this.messages.set(msgs));
// Search with debounce
this.searchSubject.pipe(
takeUntil(this.destroy$),
debounceTime(300),
distinctUntilChanged(),
tap(term => {
this.searchTerm.set(term);
this.searchCount.update(c => c + 1);
})
).subscribe();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private getZone(x: number, y: number): string {
if (x < 200 && y < 200) return '↖ Północny-Zachód';
if (x > 400 && y < 200) return '↗ Północny-Wschód';
if (x < 200 && y > 400) return '↙ Południowy-Zachód';
if (x > 400 && y > 400) return '↘ Południowy-Wschód';
return 'Centrum';
}
onMessageInput(value: string): void {
// Could add real-time typing indicator here
}
sendMessage(text: string): void {
if (!text.trim()) return;
const newMessage: Message = {
text,
time: new Date().toLocaleTimeString(),
urgent: text.includes('!')
};
const current = this.messageSubject.getValue();
this.messageSubject.next([...current, newMessage]);
}
onSearchInput(event: Event): void {
const input = event.target as HTMLInputElement;
this.searchSubject.next(input.value);
}
}
interface Message {
text: string;
time: string;
urgent: boolean;
}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Która funkcja RxJS tworzy Observable emitujący wartości co określony interwał czasowy?
2. Co robi funkcja of() w RxJS?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Klikanie w kolejności
Ułóż deklarację zmiennej Observable zgodnie z konwencją nazewnictwa w Angular: samurai$: Observable<Samurai[]>;
- Edytor kodu
Uzupełnij kod tworzący Observable z podanych wartości i subskrybujący go.
- Układanie w poziomie
Ułóż import funkcji tworzących Observable z biblioteki rxjs:
- Układanie w pionie
Ułóż callbacki obiektu obserwatora w kolejności, w jakiej deklarujemy je w subscribe():
- Klikanie w kolejności
Ułóż skróconą subskrypcję strumienia: numbers$.subscribe(fn)