typescript
56 lines · 8 steps
Building dynamic reactive forms in Angular
Generate a validated reactive form at runtime from a schema array instead of hardcoding controls.
Explained by
highlit
1import { Component, Input, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup, FormArray, FormControl, Validators, ValidatorFn } from '@angular/forms';
3
4interface FieldSchema {
5 key: string;
6 label: string;
7 type: 'text' | 'number' | 'email' | 'checkbox' | 'select';
8 required?: boolean;
9 min?: number;
10 max?: number;
11 options?: { value: string; label: string }[];
12}
13
14@Component({
15 selector: 'app-dynamic-form',
16 templateUrl: './dynamic-form.component.html',
17})
18export class DynamicFormComponent implements OnInit {
19 @Input() schema: FieldSchema[] = [];
20
21 form!: FormGroup;
22
23 constructor(private fb: FormBuilder) {}
24
25 ngOnInit(): void {
26 this.form = this.fb.group({
27 fields: this.fb.array(this.schema.map((field) => this.buildControl(field))),
28 });
29 }
30
31 get fields(): FormArray<FormControl> {
32 return this.form.get('fields') as FormArray<FormControl>;
33 }
34
35 private buildControl(field: FieldSchema): FormControl {
36 const validators: ValidatorFn[] = [];
37 if (field.required) {
38 validators.push(field.type === 'checkbox' ? Validators.requiredTrue : Validators.required);
39 }
40 if (field.type === 'email') validators.push(Validators.email);
41 if (field.min != null) validators.push(Validators.min(field.min));
42 if (field.max != null) validators.push(Validators.max(field.max));
43
44 const initial = field.type === 'checkbox' ? false : '';
45 return this.fb.control(initial, validators);
46 }
47
48 submit(): { key: string; value: unknown }[] {
49 this.form.markAllAsTouched();
50 if (this.form.invalid) return [];
51 return this.schema.map((field, i) => ({
52 key: field.key,
53 value: this.fields.at(i).value,
54 }));
55 }
56}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A schema-driven form lets you describe fields as data and generate controls, so new fields need no template or class changes.
- 2Building validators conditionally from each field's metadata keeps validation rules colocated with the field definition.
- 3Marking a form as touched before checking validity ensures error messages surface when the user attempts submission.
Related explainers
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config';
How a JWT strategy authenticates in NestJS
authentication
jwt
passport
Intermediate
7 steps
php
<?php namespace App\Http\Controllers;
Streaming a filtered CSV export in Laravel
streaming
csv-export
query-builder
Intermediate
9 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
typescript
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt';
How a GraphQL auth guard works in NestJS
authentication
authorization
jwt
Intermediate
7 steps
typescript
type ResizeCallback = (size: { width: number; height: number }) => void; export function observeResize(callback: ResizeCallback, delay = 150) { let timeoutId: ReturnType<typeof setTimeout> | undefined;
Debouncing window resize in TypeScript
debounce
closures
event-listeners
Intermediate
6 steps
typescript
import { Injectable, Logger, OnApplicationShutdown } from '@nestjs/common'; import { InjectRedis } from '@nestjs-modules/ioredis'; import Redis from 'ioredis'; import { DataSource } from 'typeorm';
Graceful shutdown hooks in NestJS
graceful-shutdown
lifecycle-hooks
dependency-injection
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-dynamic-reactive-forms-in-angular-explained-typescript-09d9/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.