Durgesh Pal
7 min readJan 23, 2020

Angular Dynamic Form With Configurable Controls And Validation

Configurable layout with dynamic controls

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 ?

  1. Number of navigation bars and the label of nav
  2. Number of panels at the single side and the label of panel
  3. Number of fields in the single panel and label
  4. 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
JSON Structure
JSON hierarchy
  • Folder Structure

Create a folder structure like below image

Project Structure

Project Structure Files and Code

  1. DynamicFormModule
  2. DynamicFormService
  3. DynamicFormComponent
  4. DynamicFormFieldComponent
  5. controlMapper
  6. 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}}

Responses (1)

Write a response