Angular dynamic forms

Petar Kojchevski
4 min readSep 26, 2021

Create dynamic forms from JSON

There are situations where You need to create a dynamic form with data from DB.

In this example, I’ll explain how I did it when I was working on one project.

Dynamic form is used as follows:

<div class="container"><dynamic-form [config]="detailsForm"></dynamic-form></div>

Components are created with the directive and ComponentFactoryResolver

export class DynamicFieldDirective implements Field, OnChanges, OnInit {@Input()config!: FieldConfig;@Input()group!: FormGroup;component!: ComponentRef<Field>;constructor(private resolver: ComponentFactoryResolver,private container: ViewContainerRef) { }ngOnChanges() {if (this.component) {this.component.instance.config = this.config;this.component.instance.group = this.group;}}ngOnInit() {if (!components[this.config.element]) {const supportedTypes = Object.keys(components).join(', ');throw new Error(`Trying to use an unsupported type (${this.config.element}).Supported types: ${supportedTypes}`);}const singleComponent = this.resolver.resolveComponentFactory<Field>(components[this.config.element]);this.component = this.container.createComponent(singleComponent);this.component.instance.config = this.config;this.component.instance.group = this.group;}}

Model for the json element(field config) is shown bellow:

export interface FieldConfig {disabled?: boolean,label?: string,name: string,options?: string[],placeholder?: string,errors?: Array<any>,type: 'text' | 'number' | 'select' | 'email' | 'textarea' | 'password',element: 'input' | 'select' | 'button' | 'textarea',value?: any,validators: object}

Here in element and type, You can add more elements and types.

In the dynamic form component we are creating all the form groups, controls, validators, and error messages:

ngOnInit() {this.form = this.createGroup();this.form.statusChanges.subscribe((val) => {if(val === "INVALID")for(const [key, value] of Object.entries(this.form.controls)) {if(value.status === 'INVALID') {this.getErrorMessage(value as FormControl, this.config.filter(c => c.name === key)[0])}}});}createGroup() {const group = this.fb.group({});this.controls?.forEach((control: any) =>group.addControl(control.name, this.createControl(control)))return group;}createControl(config: FieldConfig | undefined) {if(!config) return this.fb.control({})const { disabled, validators, value } = config;return this.fb.control({ disabled, value }, this.getValidators(validators))}handleSubmit(event: Event) {event.preventDefault();event.stopPropagation();this.submit.emit(this.value);}setDisabled(name: string, disable: boolean) {if (this.form.controls[name]) {const method = disable ? 'disable' : 'enable';this.form.controls[name][method]();return;}this.config = this.config.map((item: any) => {if (item.genericName === name) {item.disabled = disable;}return item;});}setValue(name: string, value: any) {this.form.controls[name].setValue(value, { emitEvent: true });}getValidators(validator: object) {const validators = []for(const[key, value] of Object.entries(validator)) {switch(key) {case 'required':if(value) {validators.push(Validators.required)}break;case 'minLength':validators.push(Validators.minLength(value))break;case 'maxLength':validators.push(Validators.maxLength(value))break;case 'min':validators.push(Validators.min(value))break;case 'max':validators.push(Validators.max(value))break;case 'email':if(value) {validators.push(Validators.email)}}}return validators}getErrorMessage(control: FormControl, config: any) {let message = ""if(control?.hasError('required')) {message = 'You must enter a value'if(config?.errors && config.errors.filter(((item: any) => item[config.name] === message)).length === 0) {config.errors.push({[config.name]:message})}}if(control?.hasError('minlength')) {message = `It must have minimum ${config?.validators['minLength']} characters`if(config?.errors && config.errors.filter(((item: any) => item[config.name] === message)).length === 0) {config.errors.push({[config.name]:message})}}if(control?.hasError('maxlength')) {message = `It must have maximum ${config?.validators['minLength']} characters`if(config?.errors && config.errors.filter(((item: any) => item[config.name] === message)).length === 0) {config.errors.push({[config.name]:message})}}if(control?.hasError('email')) {message = `It must be valid email`if(config?.errors && config.errors.filter(((item: any) => item[config.name] === message)).length === 0) {config.errors.push({[config.name]:message})}}if(control?.hasError('max')) {message = `It must be number smaller then ${config?.validators.max}`if(config?.errors && config.errors.filter(((item: any) => item[config.name] === message)).length === 0) {config.errors.push({[config.name]:message})}}if(control?.hasError('min')) {message = `It must be number bigger then ${config?.validators.min}`if(config?.errors && config.errors.filter(((item: any) => item[config.name] === message)).length === 0) {config.errors.push({[config.name]:message})}}}

Dynamic form component HTML:

<formclass="dynamic-form"[formGroup]="form"(submit)="handleSubmit($event)"><ng-container*ngFor="let field of config;"dynamicField[config]="field"[group]="form"errors="config.errors"></ng-container><div class="dynamic-field form-button"><buttonmat-raised-buttoncolor="primary"[disabled]="form?.invalid"type="submit">Submit</button></div></form>

And we write components for input, select…

<div [formGroup]="group"><mat-form-field appearance="outline" floatLabel="never" *ngIf="config.type === 'text'"><mat-label> {{config?.label}} </mat-label><input matInput [type]="config?.type"[attr.placeholder]="config?.placeholder"[formControlName]="config?.name"[value]="config?.value"/><mat-error><div *ngFor="let err of config.errors">{{err[config?.name]}}</div></mat-error></mat-form-field><mat-form-field appearance="outline" floatLabel="never" *ngIf="config.type === 'number'"><mat-label> {{config?.label}} </mat-label><input matInput [type]="config?.type"[attr.placeholder]="config?.placeholder"[formControlName]="config?.name"[value]="config?.value"/><mat-error><div *ngFor="let err of config.errors">{{err[config?.name]}}</div></mat-error></mat-form-field><mat-form-field appearance="outline" floatLabel="never" *ngIf="config?.type === 'textarea'"><mat-label> {{config?.label}} </mat-label><textarea matInput[attr.placeholder]="config?.placeholder"[formControlName]="config?.name"[value]="config?.value"></textarea><mat-error><div *ngFor="let err of config.errors">{{err[config?.name]}}</div></mat-error></mat-form-field><mat-form-field appearance="outline" floatLabel="never" *ngIf="config?.type === 'email'"><mat-label> {{config?.label}} </mat-label><input matInput[type]="config?.type"[attr.placeholder]="config?.placeholder"[formControlName]="config?.name"[value]="config?.value"/><mat-error><div *ngFor="let err of config.errors">{{err[config?.name]}}</div></mat-error></mat-form-field></div>
<div [formGroup]="group"><mat-form-field><mat-label> {{ config?.label }}</mat-label><mat-select[formControlName]="config?.name!" [compareWith]="compareFn"><mat-option *ngFor="let option of config?.options" [value]="option">{{option}}</mat-option></mat-select><mat-error><div *ngFor="let err of config.errors"><p>{{err[config.name]}}</p></div></mat-error></mat-form-field></div>

The whole project You can find on my GitHub:

I hope I’ll help You with this topic and You’l find it usefull.

--

--

Petar Kojchevski

Engineer, Web developer, working part time as a freelancer