Dynamic Forms From Metadata JSON

Yogesh Bali
4 min readJan 23, 2023

--

To generate the forms in an application in an faster and easier way, we use dynamic forms.

Dynamic forms are created based upon the input metadata json.

Metadata json descibes the business object model. It represents the field names required to be displayed on the form.

What we need:

  1. A metadata json
  2. A formObject class that is mapping the metadata String json.
  3. A service to read the metadatajson and convert it into metadata object.
  4. A service to read the metadata object and convert it into a formGroup.
  5. A dynamic component that will read this metadataObject and this formGroup to create the form.
  6. A component that will pass metadataObject and formgroup to the dynamic component of step 5.

Create new project:

  • ng new dynamicForms

Import ReactiveFormsModule

Dynamic forms are based on reactive forms. Therefore, we need to import ReactiveFormsModule.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ReactiveFormsModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Step 1: Create Mock Metadata Json

This metadatajson will be used in rendering the form. Either take it from the backend code, or have it hardcoded for now to complete this tutorial.

export const MetadataJson=[
{
key: 'firstName',
label: 'First name',
value: '',
required: true,
type: 'text',
controlType: 'textbox',
order: 1
},
{
key: 'lastName',
label: 'Last name',
value: '',
required: true,
type: 'text',
controlType: 'textbox',
order: 2
},
{
key: 'emailAddress',
label: 'Email',
value: '',
required: false,
type: 'text',
controlType: 'textbox',
order: 3
}
];

Step 2: Create Form Object

  • To map the json Meta received from the backend or hardcoded constant into an object.
  • ng generate class FormObject
export class FormObject<T> {
value: T|undefined;
key!: string;
label!: string;
required!: boolean;
order!: number;
type!: string;
controlType!: string;
}

Step 3: Create Service class to convert the json into object

This class is used to convert the metadata json into a metadata object.

ng generate class JSONToObjectService

import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { FormObject } from './form-object';
import { MetadataJson } from './mockMetadata';

@Injectable({
providedIn: 'root'
})
export class JSONToObjectService {

constructor() { }

getObjects() {
const objects: FormObject<string>[] = [];
for (const field of MetadataJson) {
let formObject = new FormObject<string>();
formObject.key=field['key'];
formObject.value=field['value'];
formObject.label=field['label'];
formObject.required=field['required'];
formObject.type=field['type'];
formObject.controlType=field['controlType'];
formObject.order=field['order'];
objects.push(formObject);
}
return of(objects.sort((a, b) => a.order - b.order));
}
}

Step 4: Transform metadata object to Formgroup

ng generate class FormGroupService

A dynamic form uses a service to create grouped sets of input controls, based on the form model. The following FormGroupService collects a set of FormGroup instances that consume the metadata from the formObject model. You can specify default values and validation rules.

import { Injectable } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { FormObject } from './form-object';


@Injectable()
export class FormGroupService {
toFormGroup(formObjects: FormObject<string>[] ) {
const group: any = {};

formObjects.forEach(formObject => {
group[formObject.key] = formObject.required ? new FormControl(formObject.value || '', Validators.required)
: new FormControl(formObject.value || '');
});
console.log(group);
return new FormGroup(group);
}
}

Step 5: Create Component to create and design the form

This component will use the metadata object and the formgroup to create the form.

The DynamicFormComponent is responsible for rendering the details of an individual field based on values in the data-bound formObject.

import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormObject } from '../form-object';


@Component({
selector: 'app-dynamic-form',
templateUrl: './dynamic-form.component.html',
styleUrls: ['./dynamic-form.component.scss']
})
export class DynamicFormComponent implements OnInit {
@Input() formObject!: FormObject<string>;
@Input() form!: FormGroup;

get isValid() { return this.form.controls[this.formObject.key].valid; }

constructor() { }

ngOnInit(): void {
}

}
<div [formGroup]="form">
<label [attr.for]="formObject.key">{{formObject.label}}</label>

<div [ngSwitch]="formObject.controlType">

<input *ngSwitchCase="'textbox'" [formControlName]="formObject.key"
[id]="formObject.key" [type]="formObject.type">

<div *ngSwitchCase="'dropdown'">
<select></select>
</div>

</div>

<div class="errorMessage" *ngIf="!isValid">{{formObject.label}} is required</div>
</div>

The form relies on a [formGroup] directive to connect the template HTML to the underlying control objects. The DynamicFormComponent creates form groups and populates them with controls defined in the formObject model, specifying display and validation rules.

The ngSwitch statement in the template determines which type of field to display. The switch uses directives with the formControlName and formGroup selectors. Both directives are defined in ReactiveFormsModule

Step 6: Create Component to get metadata object, formGroup and call our dynamic form component.

The ContactComponent component is the entry point and the main container for the form.

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormGroupName } from '@angular/forms';
import { JSONToObjectService } from '../jsonto-object-service';
import { FormGroupService } from '../form-group-service';
import { FormObject } from '../form-object';

@Component({
selector: 'app-contact',
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.scss']
})
export class ContactComponent implements OnInit {
form!: FormGroup;
metaObject!: FormObject<string>[];
constructor(private jsonToObjectService:JSONToObjectService, private formgroupService:FormGroupService) { }

ngOnInit(): void {
this.jsonToObjectService.getObjects().subscribe((metaObject:any)=>{
this.form=this.formgroupService.toFormGroup(metaObject);
this.metaObject=metaObject;
});
}
onSubmit() {
console.log(JSON.stringify(this.form.getRawValue()));
}

}
<div  style="padding-left:100px;"><h3>Add Contact</h3></div>
<div style="padding-left:100px;padding-top:10px;">
<form (ngSubmit)="onSubmit()" [formGroup]="form">

<div *ngFor="let object of metaObject" class="form-row">
<app-dynamic-form [formObject]="object" [form]="form"></app-dynamic-form>
</div>
<br/>
<div class="form-row">
<button type="submit" [disabled]="!form.valid">Save</button>
</div>
</form>
</div>

Step 7: Display the form

Modify the app.component.html and include the app-contact tag.

<app-contact></app-contact>

Final app.module.ts will look:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';
import { ContactComponent } from './contact/contact.component';
import { DynamicFormComponent } from './dynamic-form/dynamic-form.component';
import { FormGroupService } from './form-group-service';

@NgModule({
declarations: [
AppComponent,
ContactComponent,
DynamicFormComponent
],
imports: [
BrowserModule,
ReactiveFormsModule,
AppRoutingModule
],
providers: [FormGroupService],
bootstrap: [AppComponent]
})
export class AppModule { }

Run the application and you will see the input form created from the json metadata.

--

--

Yogesh Bali
Yogesh Bali

Written by Yogesh Bali

Senior Technical Lead in Thales

Responses (2)