Angular course Β· Module 7: RxJS
RxJS in Angular - Ninja Scouts
In this lesson4
RxJS (Reactive Extensions for JavaScript) is a library for reactive programming - like a network of ninja scouts observing the terrain and instantly reporting events to headquarters.
What is Reactive Programming?
It is a paradigm based on data streams and change propagation:
- Streams - sequences of events spread over time
- Observers - react to every event in the stream
- Operators - transform, filter, combine streams
Observable - Data Stream
An Observable is an "observable" sequence of values - like a ninja watching a castle and reporting every movement:
import { Observable, of, from, interval, fromEvent } from 'rxjs';
// Creating an Observable from values
const numbers$ = of(1, 2, 3, 4, 5); // Emits values and completes
// From an array
const fromArray$ = from([1, 2, 3]);
// Timer - emits every second
const timer$ = interval(1000); // 0, 1, 2, 3...
// From DOM events
const clicks$ = fromEvent(document, 'click');
// Custom Observable
const custom$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
setTimeout(() => {
subscriber.next(3);
subscriber.complete();
}, 1000);
});Subscription - Listening
// Full subscription form
const subscription = numbers$.subscribe({
next: value => console.log('Value:', value),
error: err => console.error('Error:', err),
complete: () => console.log('Completed')
});
// Short form - only next
numbers$.subscribe(value => console.log(value));
// Cancelling the subscription
subscription.unsubscribe();Naming Convention
In Angular we use the $ suffix for Observables:
samurai$: Observable<Samurai[]>;
loading$: Observable<boolean>;
error$: Observable<string | null>;Code for this lesson: 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>Ninja Headquarters - RxJS</h1>
<section class="scouts-section">
<h2>Scouts (Observables)</h2>
<div class="scout-report">
<h3>Time Report</h3>
<p>Seconds since start: {{ timeElapsed() }}</p>
<p>Even time: {{ evenTime() ? 'Yes' : 'No' }}</p>
</div>
<div class="scout-report">
<h3>Mouse Tracking</h3>
<p>Position X: {{ mouseX() }}</p>
<p>Position Y: {{ mouseY() }}</p>
<p>Zone: {{ mouseZone() }}</p>
</div>
</section>
<section class="messages-section">
<h2>Messages (Subject)</h2>
<div class="message-input">
<input
#messageInput
type="text"
placeholder="Type a message..."
(input)="onMessageInput(messageInput.value)"
>
<button (click)="sendMessage(messageInput.value); messageInput.value = ''">
Send
</button>
</div>
<div class="messages-log">
<h3>Message history:</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">URGENT</span>
}
</div>
}
</div>
</section>
<section class="operators-section">
<h2>RxJS Operators</h2>
<div class="search-demo">
<h3>Search (debounce + distinctUntilChanged)</h3>
<input
type="text"
placeholder="Search samurai..."
(input)="onSearchInput($event)"
>
<p>Search term: "{{ searchTerm() }}"</p>
<p>Number of searches: {{ 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('Center');
messages = signal<Message[]>([]);
searchTerm = signal('');
searchCount = signal(0);
ngOnInit(): void {
// Timer Observable - every second
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 'β North-West';
if (x > 400 && y < 200) return 'β North-East';
if (x < 200 && y > 400) return 'β South-West';
if (x > 400 && y > 400) return 'β South-East';
return 'Center';
}
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;
}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which RxJS function creates an Observable that emits values at a fixed time interval?
2. What does the of() function do in RxJS?
These are 2 of 4 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Click in order
Arrange the Observable variable declaration following the Angular naming convention: samurai$: Observable<Samurai[]>;
- Code editor
Complete the code that creates an Observable from values and subscribes to it.
- Horizontal ordering
Arrange the import of Observable creation functions from the rxjs library:
- Vertical ordering
Arrange the observer object callbacks in the order in which we declare them in subscribe():
- Click in order
Arrange the short subscription form of a stream: numbers$.subscribe(fn)