Angular Dynamic Form With Configurable Controls And Validation

Summary
In this post we are going to see how to dynamically create a reactive form with configurable layout , fields and validations.
What can be configured in the form ?
- Number of navigation bars and the label of nav
- Number of panels at the single side and the label of panel
- Number of fields in the single panel and label
- Type of control for the field ( Label, Text Input, Number Input, Text Input, Select, Date, Text area )
GitHub link for the the Project- Link
Run below line for cloning the project from GitHub
git clone https://github.com/imdurgeshpal/Angular-Configurable-Dynamic-Form.git
Download the JSON file here
- JSON Structure


- Folder Structure
Create a folder structure like below image

Project Structure Files and Code
- DynamicFormModule
- DynamicFormService
- DynamicFormComponent
- DynamicFormFieldComponent
- controlMapper
- BaseControl
1. Dynamic Form Module
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { DynamicFormComponent } from './components/dynamic-form/dynamic-form.component';
import { DynamicFormFieldComponent } from './components/dynamic-form-field/dynamic-form-field.component';
import { LabelControlComponent } from './components/dynamic-controls/label-control/label-control.component';
import { InputControlComponent } from './components/dynamic-controls/input-control/input-control.component';
import { SelectControlComponent } from './components/dynamic-controls/select-control/select-control.component';
import { DateControlComponent } from './components/dynamic-controls/date-control/date-control.component';
const exportComponent = [
LabelControlComponent,
InputControlComponent,
SelectControlComponent,
DateControlComponent
]
@NgModule({
declarations: [
...exportComponent,
DynamicFormComponent,
DynamicFormFieldComponent
],
imports: [
CommonModule,
ReactiveFormsModule,
NgbModule
],
exports: [
DynamicFormComponent
],
entryComponents: [
...exportComponent
]
})
export class DynamicFormModule { }
2. DynamicFormService
import { Injectable } from '@angular/core';
import { Schema } from '../models/schema';
import { FormConfig } from '../models/form-config';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { FieldConfig } from '../models/field-config';
@Injectable({
providedIn: 'root'
})
export class DynamicFormService {
private schema: Schema[] = [{
label: 'Personal',
children: [
{
label: 'left',
children: [
{
label: 'General Information',
children: [
{
label: 'Id',
field: {
label: 'Id',
property: 'id',
type: 'LABEL',
required: false,
value: 1
}
},
{
label: 'firstName',
field: {
label: 'First Name',
property: 'firstName',
type: 'INPUT',
required: true
}
},
{
label: 'middleName',
field: {
label: 'Middle Name',
property: 'middleName',
type: 'INPUT',
required: false
}
},
{
label: 'lastName',
field: {
label: 'Last Name',
property: 'lastName',
type: 'INPUT',
required: false
}
}
]
}
]
},
{
label: 'right',
children: [
{
label: 'Age Details',
children: [
{
label: 'DOB',
field: {
label: 'Date Of Birth',
property: 'dob',
type: 'DATE',
required: false
}
}
]
}
]
}
]
},
{
label: 'Address',
children: [
{
label: 'left',
children: [
{
label: 'Temporary',
children: [
{
label: 'Address Line 1',
field: {
label: 'Address Line 1',
property: 'tempAddressLine1',
type: 'INPUT',
required: false,
value: ''
}
},
{
label: 'Address Line 2',
field: {
label: 'Address Line 2',
property: 'tempAddressLine2',
type: 'INPUT',
required: true
}
},
{
label: 'City / Town',
field: {
label: 'City / Town',
property: 'tempCity',
type: 'INPUT',
required: false
}
},
{
label: 'State / Province / Region',
field: {
label: 'State / Province / Region',
property: 'tempState',
type: 'INPUT',
required: false
}
},
{
label: 'Zip / Postal Code',
field: {
label: 'Zip / Postal Code',
property: 'tempPostalCode',
type: 'INPUT',
required: false
}
},
{
label: 'Country',
field: {
label: 'Country',
property: 'tempCountry',
type: 'INPUT',
required: false
}
}
]
}
]
},
{
label: 'right',
children: [
{
label: 'Permanent',
children: [
{
label: 'Address Line 1',
field: {
label: 'Address Line 1',
property: 'permanentAddressLine1',
type: 'INPUT',
required: false,
value: ''
}
},
{
label: 'Address Line 2',
field: {
label: 'Address Line 2',
property: 'permanentAddressLine2',
type: 'INPUT',
required: true
}
},
{
label: 'City / Town',
field: {
label: 'City / Town',
property: 'permanentCity',
type: 'INPUT',
required: false
}
},
{
label: 'State / Province / Region',
field: {
label: 'State / Province / Region',
property: 'permanentState',
type: 'INPUT',
required: false
}
},
{
label: 'Zip / Postal Code',
field: {
label: 'Zip / Postal Code',
property: 'permanentPostalCode',
type: 'INPUT',
required: false
}
},
{
label: 'Country',
field: {
label: 'Country',
property: 'permanentCountry',
type: 'INPUT',
required: false
}
}
]
}
]
}
]
},
{
label: 'Family',
children: [
{
label: 'left',
children: [
{
label: 'Father',
children: [
{
label: 'First Name',
field: {
label: 'First Name',
property: 'fatherFirstName',
type: 'INPUT'
}
},
{
label: 'Last Name',
field: {
label: 'Last Name',
property: 'fatherLastName',
type: 'INPUT'
}
},
{
label: 'Date of Birth',
field: {
label: 'Date of Birth',
property: 'fatherDOB',
type: 'DATE'
}
}
]
},
{
label: 'Mother',
children: [
{
label: 'First Name',
field: {
label: 'First Name',
property: 'motherFirstName',
type: 'INPUT'
}
},
{
label: 'Last Name',
field: {
label: 'Last Name',
property: 'motherLastName',
type: 'INPUT'
}
},
{
label: 'Date of Birth',
field: {
label: 'Date of Birth',
property: 'motherDOB',
type: 'DATE'
}
}
]
}
]
},
{
label: 'right',
children: [
{
label: 'Child',
children: [
{
label: 'First Name',
field: {
label: 'First Name',
property: 'child1FirstName',
type: 'INPUT'
}
},
{
label: 'Last Name',
field: {
label: 'Last Name',
property: 'child1LastName',
type: 'INPUT'
}
},
{
label: 'Date of Birth',
field: {
label: 'Date of Birth',
property: 'child1DOB',
type: 'DATE'
}
}
]
}
]
}
]
}] as Schema[];
constructor(private formBuilder: FormBuilder) { }
getFormConfig(): FormConfig {
const schema = this.schema;
const formConfig = {
schema: this.schema
} as FormConfig;
const controls: any = {};
this.prepareFormGroup(schema, controls);
formConfig.formGroup = this.formBuilder.group(controls);
return formConfig;
}
prepareFormGroup(schema: Schema[], controls) {
schema.forEach(nav => {
nav.children.forEach(side => {
side.children.forEach(panel => {
panel.children.forEach(control => {
const field = control.field;
/* creating form controls for reactive form */
controls[field.property] = this.getControlConfig(field);
});
});
});
});
}
getControlConfig(field: FieldConfig) {
const validators = [];
if (field.required) {
validators.push(Validators.required);
}
if (validators.length) {
return [field.value, validators];
}
return field.value;
}
}
3.a. DynamicFormComponent
export class DynamicFormComponent implements OnInit {
formConfig: FormConfig; constructor(private formService: DynamicFormService) { } ngOnInit() {
this.formConfig = this.formService.getFormConfig();
}
}
3.b. DynamicFormComponent HTML
<form [formGroup]="formConfig.formGroup" class="mt-2">
<ngb-tabset #tabset>
<!-- ng boosted nav tab -->
<ngb-tab [title]="nav.label" *ngFor="let nav of formConfig.schema;let i=index;">
<!-- ngFor nav-->
<ng-template ngbTabContent>
<ng-container *ngTemplateOutlet="schemaTabContent; context:{nav: nav}"></ng-container>
</ng-template>
</ngb-tab>
</ngb-tabset>
<ng-template #schemaTabContent let-nav="nav">
<div class="row">
<div class="col-md-6" *ngFor="let side of nav.children; let i=index;">
<!-- ngfor side -->
<div class="card mt-4" *ngFor="let panel of side.children">
<!-- ngfor panel-->
<div class="card-header">{{panel.label}}</div>
<div class="card-body bg-light">
<!-- ngFor fields -->
<ng-container *ngFor="let control of panel.children">
<app-dynamic-form-field [field]="control.field" [formGroup]="formConfig.formGroup">
</app-dynamic-form-field>
</ng-container>
</div>
</div>
</div>
</div>
</ng-template>
</form>
4.1. DynamicFormFieldComponent
import { Component, OnInit, Input, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core';
import { FieldConfig } from '../../models/field-config';
import { FormGroup } from '@angular/forms';
import { BaseControl } from '../dynamic-controls/base-control';
import { controlMapper } from './control-mapper';
@Component({
selector: 'app-dynamic-form-field',
templateUrl: './dynamic-form-field.component.html',
styleUrls: ['./dynamic-form-field.component.scss']
})
export class DynamicFormFieldComponent implements OnInit {
@Input() field: FieldConfig;
@Input() formGroup: FormGroup;
@ViewChild('widgetContainer', { read : ViewContainerRef, static: true }) widgetContainer: ViewContainerRef;
constructor(private resolver: ComponentFactoryResolver) { }
ngOnInit() {
// getting widget component or label widget component based on configuration of field
const control = controlMapper[this.field.type];
const widgetFactory = this.resolver.resolveComponentFactory<BaseControl>(control);
const component = this.widgetContainer.createComponent(widgetFactory);
component.instance.field = this.field; // setting field to the control
component.instance.formGroup = this.formGroup; // setting formgroup to the control
}
}
4.2. DynamicFormFieldComponent HTML
<div class="form-group row">
<label [attr.for]="field.property" class="col-4 col-form-label"
[class.required]="field.required">{{field.label}}</label>
<div class="col-8">
<ng-container #widgetContainer></ng-container>
</div>
</div>
5. ControlMapper
import { OnInit } from '@angular/core';
import { FieldConfig } from '../../models/field-config';
import { FormGroup, AbstractControl } from '@angular/forms';
export abstract class BaseControl implements OnInit {
field: FieldConfig;
formGroup: FormGroup;
control: AbstractControl;
constructor() { }
ngOnInit() {
this.control = this.formGroup.get(this.field.property);
}
}
enter code here
6. Base Control
import { OnInit } from '@angular/core';
import { FieldConfig } from '../../models/field-config';
import { FormGroup, AbstractControl } from '@angular/forms';
export abstract class BaseControl implements OnInit {
field: FieldConfig;
formGroup: FormGroup;
control: AbstractControl;
constructor() { }
ngOnInit() {
this.control = this.formGroup.get(this.field.property);
}
}
7. Dynamic Controls
7.1.1 InputControlComponent
import { Component, OnInit } from '@angular/core';
import { BaseControl } from '../base-control';
@Component({
selector: 'app-input-control',
templateUrl: './input-control.component.html',
styleUrls: ['./input-control.component.scss']
})
export class InputControlComponent extends BaseControl implements OnInit {
constructor() {
super();
}
ngOnInit() {
super.ngOnInit();
}
}
7.1.2 InputControlComponent HTML
<div [formGroup]="formGroup">
<input type="text" class="form-control" [attr.id]="field.property" [formControlName]="field.property">
</div>
7.2.1 LabelControlComponent
import { Component, OnInit } from '@angular/core';
import { BaseControl } from '../base-control';
@Component({
selector: 'app-label-control',
templateUrl: './label-control.component.html',
styleUrls: ['./label-control.component.scss']
})
export class LabelControlComponent extends BaseControl implements OnInit {
constructor() {
super();
}
ngOnInit() {
super.ngOnInit();
}
}
7.2.2 LabelControlComponent HTML
{{control.value}}