"RxJS + Angular Reactive Forms â Live Validation & Dynamic Forms"
đ Welcome to Chapter 8!
Forms are EVERYWHERE in web apps â login, registration, checkout, search, settings.
Angular's Reactive Forms are powered by RxJS under the hood. Every FormControl has streams you can subscribe to â valueChanges, statusChanges. This unlocks incredibly powerful form behavior that would be painfully difficult to build any other way.
Today we build real, production-quality forms with RxJS superpowers!
đī¸ The Reactive Forms Quick Setup
First, let's make sure we have the right setup:
app.module.ts
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [ReactiveFormsModule]
})
export class AppModule {}
đĄ The Key Streams on Every FormControl
Every FormControl, FormGroup, and FormArray has two key Observable streams:
valueChanges â emits every time the value changes
statusChanges â emits every time the validation status changes ('VALID', 'INVALID', 'PENDING', 'DISABLED')
const emailControl = new FormControl('');
// Watch the value
emailControl.valueChanges.subscribe(value => {
console.log('Email is now:', value);
});
// Watch the validation status
emailControl.statusChanges.subscribe(status => {
console.log('Status:', status); // 'VALID', 'INVALID', 'PENDING', 'DISABLED'
});
đ Real Example 1 â Live Username Availability Check
This is extremely common: as the user types a username, check if it's available.
// register.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators, AbstractControl } from '@angular/forms';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-register',
template: `
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<div class="field">
<label>Username</label>
<input formControlName="username" placeholder="Choose a username" />
<!-- Checking availability -->
<span *ngIf="isCheckingUsername" class="checking">
Checking availability... âŗ
</span>
<!-- Available -->
<span *ngIf="usernameAvailable === true" class="available">
â
Username is available!
</span>
<!-- Taken -->
<span *ngIf="usernameAvailable === false" class="taken">
â Username is already taken
</span>
<!-- Validation errors -->
<span *ngIf="registerForm.get('username')?.errors?.['required'] &&
registerForm.get('username')?.touched">
Username is required
</span>
<span *ngIf="registerForm.get('username')?.errors?.['minlength']">
At least 3 characters required
</span>
</div>
<div class="field">
<label>Email</label>
<input formControlName="email" type="email" placeholder="Enter email" />
<span *ngIf="registerForm.get('email')?.errors?.['email'] &&
registerForm.get('email')?.touched">
Invalid email address
</span>
</div>
<div class="field">
<label>Password</label>
<input formControlName="password" type="password" placeholder="Password" />
<div class="password-strength" [class]="passwordStrength">
{{ passwordStrengthLabel }}
</div>
</div>
<div class="field">
<label>Confirm Password</label>
<input formControlName="confirmPassword" type="password" />
<span *ngIf="registerForm.errors?.['passwordMismatch'] &&
registerForm.get('confirmPassword')?.touched">
Passwords do not match
</span>
</div>
<button type="submit" [disabled]="registerForm.invalid || isCheckingUsername">
Create Account
</button>
</form>
`
})
export class RegisterComponent implements OnInit, OnDestroy {
registerForm!: FormGroup;
isCheckingUsername = false;
usernameAvailable: boolean | null = null;
passwordStrength = 'weak';
passwordStrengthLabel = '';
private destroy$ = new Subject<void>();
constructor(
private fb: FormBuilder,
private authService: AuthService
) {}
ngOnInit(): void {
this.registerForm = this.fb.group({
username: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}, {
validators: this.passwordMatchValidator
});
// --- RxJS Magic: Username availability check ---
this.registerForm.get('username')!.valueChanges
.pipe(
debounceTime(500), // Wait 500ms after typing
distinctUntilChanged(), // Don't check if same value
takeUntil(this.destroy$)
)
.subscribe(username => {
if (username && username.length >= 3) {
this.isCheckingUsername = true;
this.usernameAvailable = null;
this.authService.checkUsername(username)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (isAvailable) => {
this.usernameAvailable = isAvailable;
this.isCheckingUsername = false;
},
error: () => {
this.isCheckingUsername = false;
}
});
} else {
this.usernameAvailable = null;
}
});
// --- RxJS Magic: Password strength meter ---
this.registerForm.get('password')!.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(password => {
this.updatePasswordStrength(password || '');
});
}
private updatePasswordStrength(password: string): void {
let score = 0;
if (password.length >= 8) score++;
if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
if (score <= 1) {
this.passwordStrength = 'weak';
this.passwordStrengthLabel = 'đ´ Weak';
} else if (score === 2) {
this.passwordStrength = 'fair';
this.passwordStrengthLabel = 'đĄ Fair';
} else if (score === 3) {
this.passwordStrength = 'good';
this.passwordStrengthLabel = 'đĸ Good';
} else {
this.passwordStrength = 'strong';
this.passwordStrengthLabel = 'đĒ Strong';
}
}
private passwordMatchValidator(group: AbstractControl) {
const password = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return password === confirm ? null : { passwordMismatch: true };
}
onSubmit(): void {
if (this.registerForm.valid && this.usernameAvailable) {
console.log('Register:', this.registerForm.value);
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
đī¸ Real Example 2 â Dependent Dropdowns (Country â City)
When selecting a country should dynamically load cities for that country:
@Component({
template: `
<form [formGroup]="addressForm">
<select formControlName="country">
<option value="">Select Country</option>
<option *ngFor="let c of countries" [value]="c.code">
{{ c.name }}
</option>
</select>
<select formControlName="city">
<option value="">
{{ isLoadingCities ? 'Loading cities...' : 'Select City' }}
</option>
<option *ngFor="let city of cities" [value]="city.id">
{{ city.name }}
</option>
</select>
</form>
`
})
export class AddressFormComponent implements OnInit, OnDestroy {
addressForm = this.fb.group({
country: [''],
city: [{ value: '', disabled: true }]
});
countries = [
{ code: 'BD', name: 'Bangladesh' },
{ code: 'IN', name: 'India' },
{ code: 'US', name: 'USA' }
];
cities: any[] = [];
isLoadingCities = false;
private destroy$ = new Subject<void>();
constructor(
private fb: FormBuilder,
private locationService: LocationService
) {}
ngOnInit(): void {
// When country changes â load its cities
this.addressForm.get('country')!.valueChanges
.pipe(
takeUntil(this.destroy$),
tap(() => {
// Reset city when country changes
this.cities = [];
this.addressForm.get('city')!.disable();
this.addressForm.get('city')!.setValue('');
this.isLoadingCities = true;
}),
switchMap(countryCode => {
if (!countryCode) return of([]);
return this.locationService.getCitiesByCountry(countryCode)
.pipe(catchError(() => of([])));
})
)
.subscribe(cities => {
this.cities = cities;
this.isLoadingCities = false;
if (cities.length > 0) {
this.addressForm.get('city')!.enable();
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
When the country changes, the city dropdown automatically:
- Resets to empty
- Shows "Loading cities..."
- Disables itself
- Loads new cities from API
- Re-enables with new options
All powered by valueChanges + switchMap!
đ° Real Example 3 â Live Price Calculator
A checkout form where the price updates as you change quantity and select options:
@Component({
template: `
<form [formGroup]="orderForm">
<label>Quantity</label>
<input type="number" formControlName="quantity" min="1">
<label>Size</label>
<select formControlName="size">
<option value="small">Small</option>
<option value="medium">Medium (+ā§ŗ200)</option>
<option value="large">Large (+ā§ŗ400)</option>
</select>
<label>
<input type="checkbox" formControlName="giftWrap">
Gift Wrap (+ā§ŗ50)
</label>
<div class="price-summary">
<p>Base price: ā§ŗ{{ basePrice }}</p>
<p>Total: <strong>ā§ŗ{{ liveTotal$ | async }}</strong></p>
</div>
</form>
`
})
export class OrderFormComponent implements OnInit {
basePrice = 500;
orderForm = this.fb.group({
quantity: [1],
size: ['small'],
giftWrap: [false]
});
liveTotal$!: Observable<number>;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
const sizeAddon: Record<string, number> = {
small: 0,
medium: 200,
large: 400
};
// Calculate total every time ANY field changes
this.liveTotal$ = this.orderForm.valueChanges.pipe(
startWith(this.orderForm.value),
map(values => {
const qty = values.quantity || 1;
const sizeExtra = sizeAddon[values.size] || 0;
const giftExtra = values.giftWrap ? 50 : 0;
return (this.basePrice + sizeExtra + giftExtra) * qty;
})
);
}
}
The total updates instantly as the user changes any field â no button, no event handlers, just pure reactive magic!
đ§ Using statusChanges for Async Validators
ngOnInit(): void {
// React to form validity changes
this.registerForm.statusChanges
.pipe(
distinctUntilChanged(),
takeUntil(this.destroy$)
)
.subscribe(status => {
// 'VALID', 'INVALID', 'PENDING', 'DISABLED'
if (status === 'VALID') {
this.canSubmit = true;
this.showSuccessHint = true;
} else {
this.canSubmit = false;
this.showSuccessHint = false;
}
});
}
đ§ Chapter 8 Summary â What You Learned
-
FormControl.valueChangesis an Observable â you can use all RxJS operators on it -
FormControl.statusChangestells you when validity changes - Use
debounceTime+switchMapfor async validation (username check, email uniqueness) - Use
valueChanges+switchMapfor dependent dropdowns (country â city) - Use
valueChanges+map+startWithfor live price calculators and previews - Always clean up form subscriptions with
takeUntilorasyncpipe
đ Coming Up in Chapter 9...
Chapter 9 covers debounceTime, throttleTime, distinctUntilChanged â the timing operators.
These are essential for search boxes, scroll events, resize events, and any situation where you need to control HOW OFTEN your code runs.
See you in Chapter 9! đ
đ RxJS Deep Dive Newsletter Series | Chapter 8 of 10
Follow me on : Github Linkedin Threads Youtube Channel
Top comments (0)