The Problem with Hardcoded Forms
In our CAAD ERP, every client wanted slightly different order forms:
- Client A needed a "warranty period" field
- Client B wanted "custom discount" with tiered logic
- Client C needed conditional "shipping insurance" fields
Hardcoding meant: new form = new deploy = 2-week turnaround.
The Dynamic Form Solution
Build forms from JSON schemas stored in MongoDB. Non-technical users configure forms via UI. Zero code changes.
Step 1: Form Schema Model
typescript
interface FormField {
id: string;
type: 'text' | 'number' | 'select' | 'date' | 'checkbox' | 'textarea';
label: string;
placeholder?: string;
required?: boolean;
validation?: ValidationRule[];
options?: { value: any; label: string }[]; // For select
conditional?: {
show: boolean; // Show only when condition is true
dependsOn: string;
condition: { field: string; operator: 'equals' | 'gt' | 'lt'; value: any };
};
calculated?: {
enabled: boolean;
formula: string; // "price * quantity"
};
}
interface FormSchema {
_id: string;
name: string;
fields: FormField[];
version: number;
tenantId: string;
}Step 2: Dynamic Form Renderer
typescript
@Component({
selector: 'app-dynamic-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div *ngFor="let field of schema.fields" [class.hidden]="!shouldShow(field)">
<label>{{ field.label }} *</label>
<input *ngIf="field.type === 'text'"
[formControlName]="field.id"
[placeholder]="field.placeholder" />
<select *ngIf="field.type === 'select'"
[formControlName]="field.id">
<option *ngFor="let opt of field.options"
[value]="opt.value">{{ opt.label }}</option>
</select>
<div *ngIf="field.calculated?.enabled">
<small>Calculated: {{ calculatedValue(field) }}</small>
</div>
</div>
<button type="submit" [disabled]="!form.valid">Save</button>
</form>
`
})
export class DynamicFormComponent implements OnInit {
@Input() schema: FormSchema;
form: FormGroup;
ngOnInit() {
this.form = this.buildForm(this.schema.fields);
this.form.valueChanges.subscribe(() => this.recalculate());
}
private buildForm(fields: FormField[]): FormGroup {
const controls = {};
fields.forEach(field => {
const validators = [];
if (field.required) validators.push(Validators.required);
if (field.validation?.includes('email')) validators.push(Validators.email);
controls[field.id] = ['', validators];
});
return new FormGroup(controls);
}
shouldShow(field: FormField): boolean {
if (!field.conditional) return true;
const dependentValue = this.form.get(field.conditional.condition.field)?.value;
return this.evaluateCondition(field.conditional.condition, dependentValue);
}
}Step 3: Conditional Logic Engine
typescript
private evaluateCondition(condition: any, values: any): boolean {
const val = this.form.get(condition.field)?.value;
switch (condition.operator) {
case 'equals': return val === condition.value;
case 'gt': return Number(val) > Number(condition.value);
case 'lt': return Number(val) < Number(condition.value);
default: return true;
}
}
private calculatedValue(field: FormField): number {
if (!field.calculated?.enabled) return 0;
const values = {};
field.calculated.formula.match(/\b\w+\b/g)?.forEach(key => {
values[key] = this.form.get(key)?.value || 0;
});
try {
return Function(...Object.keys(values), `return ${field.calculated.formula}`)(...Object.values(values));
} catch {
return 0;
}
}Step 4: Form Builder UI (Admin)
Non-technical users configure forms with drag-and-drop:
typescript
@Component({
selector: 'app-form-builder',
template: `
<div class="builder">
<div class="field-palette">
<button (click)="addField('text')">+ Text Field</button>
<button (click)="addField('number')">+ Number</button>
<button (click)="addField('select')">+ Dropdown</button>
</div>
<div cdkDropList class="field-list" (cdkDropListDropped)="reorder($event)">
<div *ngFor="let field of schema.fields; let i = index" cdkDrag>
<input [(ngModel)]="field.label" placeholder="Label" />
<select [(ngModel)]="field.type">
<option value="text">Text</option>
<option value="number">Number</option>
</select>
<button (click)="removeField(i)">Remove</button>
</div>
</div>
<button (click)="save()">Save Form Schema</button>
</div>
`
})Results After 6 Months
- Form changes deploy instantly (no code release)
- Admins create forms in minutes vs 2-week dev cycles
- Zero form-related bug reports since launch
- 30+ custom forms created by users without touching code
When This Pattern Shines
- ✅ Forms vary by customer/region
- ✅ Business rules change frequently
- ✅ Non-technical admins need control
- ✅ Multi-tenant SaaS products
When NOT To Use It
- ❌ Simple forms that never change
- ❌ Forms with complex custom UI components
- ❌ Performance-critical forms (>50 fields with real-time validation)
The dynamic form pattern transformed our ERP from "monthly updates" to "daily business configuration." Worth the investment for complex enterprise systems.