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

Walkthrough

Space play step click any line
Three takeaways
  1. 1A schema-driven form lets you describe fields as data and generate controls, so new fields need no template or class changes.
  2. 2Building validators conditionally from each field's metadata keeps validation rules colocated with the field definition.
  3. 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.

Building dynamic reactive forms in Angular — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code