Dynamic Forms From Metadata JSON
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:
- A metadata json
- A formObject class that is mapping the metadata String json.
- A service to read the metadatajson and convert it into metadata object.
- A service to read the metadata object and convert it into a formGroup.
- A dynamic component that will read this metadataObject and this formGroup to create the form.
- 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.