MohammedMohammedMohammedAnas K V

Initializing
0%
Press?for keyboard shortcuts
Back to Blog
Angular

Building Dynamic Forms in Angular: The Pattern That Saved Our ERP Project

How to build business-configurable forms that non-technical users can modify without code changes, with Angular reactive forms and JSON-driven schemas.

March 5, 2025
14 min read
AngularReactive FormsTypeScriptERPArchitecture

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.

Technologies

AngularReactive FormsTypeScriptERPArchitecture
Share