Angular Forms, Routing & Reactive Data Flow
Build validated reactive forms, navigate with the router and lazy-loaded feature modules, fetch data with HttpClient, and master RxJS patterns — Observables, map, switchMap, the async pipe — plus a practical introduction to signals.
4 sections · ~32 min · 5-question quiz (pass ≥ 70%)
1Reactive Forms: Typed, Testable, Explicit
Reactive forms model the form in the component class as a tree of FormControl, FormGroup, and FormArray instances. The template binds to that model — the class is the single source of truth.
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<input formControlName="email" />
@if (profileForm.controls.email.hasError('email')) {
<span>Invalid email</span>
}
<button type="submit" [disabled]="profileForm.invalid">Save</button>
</form>
`,
})
export class ProfileComponent {
private fb = inject(FormBuilder);
profileForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
name: ['', Validators.minLength(2)],
});
onSubmit() {
if (this.profileForm.valid) {
console.log(this.profileForm.value);
}
}
}
Why reactive over template-driven? Validators live in TypeScript (unit-testable), dynamic fields are easy to add/remove programmatically, and value changes stream through valueChanges as Observables. Use FormBuilder for concise group creation. For complex nested forms, prefer typed FormGroup<{ email: FormControl<string> }> (Angular 14+) so autocomplete catches typos in control names.
2Routing, Lazy Loading & Guards
The Angular Router maps URL paths to components. Routes are configured in a Routes array and provided via provideRouter(routes) (standalone) or RouterModule.forRoot(routes).
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
canActivate: [authGuard],
},
{ path: 'users/:id', component: UserDetailComponent },
{ path: '**', component: NotFoundComponent },
];
Lazy loading (loadChildren or loadComponent) downloads feature code only when the user navigates there — critical for large enterprise apps. Each lazy chunk is a separate JavaScript bundle.
Route parameters arrive via ActivatedRoute:
id = inject(ActivatedRoute).snapshot.paramMap.get('id');
// or reactively:
id$ = inject(ActivatedRoute).paramMap.pipe(map(p => p.get('id')));
Guards (canActivate, canDeactivate, canMatch) are functions (or injectable services) that allow or block navigation — e.g. redirect unauthenticated users to login. Resolvers prefetch data before the route activates, avoiding empty-shell flashes.
3HttpClient: REST Calls in Angular
HttpClient (from @angular/common/http) returns Observables for every request. Provide it with provideHttpClient() in standalone apps.
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
private base = '/api';
getProducts() {
return this.http.get<Product[]>(`${this.base}/products`);
}
createProduct(dto: CreateProductDto) {
return this.http.post<Product>(`${this.base}/products`, dto);
}
}
Important: Observables are lazy — nothing happens until something subscribes. In components, either subscribe in ngOnInit (and unsubscribe in ngOnDestroy or use takeUntilDestroyed) or bind directly in the template with the async pipe:
@for (product of products$ | async; track product.id) {
<li>{{ product.name }}</li>
}
The async pipe subscribes, renders the latest value, and unsubscribes automatically when the component is destroyed — eliminating a common memory-leak source. For mutating requests (POST, PUT, DELETE), chain operators or use switchMap to refresh lists after success. Interceptors (covered in the advanced course) attach auth headers and handle errors globally.
4RxJS Essentials and Signals Overview
Angular leans heavily on RxJS. Three operators cover most component logic:
// map — transform each emitted value
users$ = this.http.get<User[]>('/api/users').pipe(
map(users => users.filter(u => u.active))
);
// switchMap — cancel previous inner Observable when a new outer value arrives
// Classic: search box debounced, only latest query matters
results$ = this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.api.search(term))
);
// async pipe in template — subscribes and unwraps
// {{ users$ | async }}
switchMap vs mergeMap: Use switchMap when only the latest result matters (search, route param changes). Use mergeMap/concatMap when every emission must complete (e.g. fire-and-forget analytics beacons).
Signals (Angular 16+) offer synchronous, fine-grained reactivity as an alternative to Observable-heavy templates:
count = signal(0);
double = computed(() => this.count() * 2);
increment() { this.count.update(n => n + 1); }
Templates read signals as functions: {{ count() }}. computed() derives values; effect() runs side effects. Signals integrate with input()/output() and work alongside RxJS — use toSignal() and toObservable() to bridge between the two worlds. For new code, prefer signals for local UI state; keep Observables for async streams (HTTP, WebSockets).