
Some time ago, I had the chance to work on Raptor Framework, our product for the dynamic generation of CRUD operation starting from .Net code.
During the porting from .Net Full Framework to .Net Core and Angular, I deal with the refactoring of validation management of Angular forms, using ReactiveForm, replacing the custom management created.
Our framework basically generates a metadata JSON, used from the Angular front-end, for the dynamic generation of the UI. This JSON is generated starting from Data Annotations custom applied to the ViewModel of a .NET application. This is a simplified version:
meta-data
{
"name": "DynamicForm",
"displayName": "DynamicForm",
"type": "object",
"properties": {
"Name": {
"Required": {
"AllowEmptyStrings": { "type": "System.Boolean", "value": false },
"ErrorMessage": { "type": "System.String", "value": "Name is mandatory" },
"ErrorMessageResourceName": {
"type": "System.String",
"value": "ErrorMessageResourceName"
},
"ErrorMessageResourceType": {
"type": "System.Type",
"value": "ErrorMessageResourceType"
},
"RequiresValidationContext": {
"type": "System.Boolean",
"value": false
},
"uiControl": false,
"uiValidation": true,
"type": "Raptor.Core.DataAnnotations.Validation.RequiredAttribute"
},
"StringLength": {
"ErrorMessage": { "type": "System.String", "value": "Name length must be between 3 and 20 characters" },
"ErrorMessageResourceName": {
"type": "System.String",
"value": "ErrorMessageResourceName"
},
"ErrorMessageResourceType": {
"type": "System.Type",
"value": "ErrorMessageResourceType"
},
"MaximumLength": { "type": "System.Int32", "value": 20 },
"MinimumLength": { "type": "System.Int32", "value": 3 },
"RequiresValidationContext": {
"type": "System.Boolean",
"value": false
},
"uiControl": false,
"uiValidation": true,
"type": "Raptor.Core.DataAnnotations.Validation.StringLengthAttribute"
},
"TextBox": {
"ColLg": { "type": "System.Int32", "value": 6 },
"ColMd": { "type": "System.Int32", "value": 6 },
"ColSm": { "type": "System.Int32", "value": 6 },
"ColXs": { "type": "System.Int32", "value": 6 },
"EnabledWhenPropertyName": {
"type": "System.String",
"value": "EnabledWhenPropertyName"
},
"EnabledWhenPropertyValue": {
"type": "System.Object",
"value": "EnabledWhenPropertyValue"
},
"HiddenWhenCreate": { "type": "System.Boolean", "value": false },
"HiddenWhenDelete": { "type": "System.Boolean", "value": false },
"HiddenWhenEdit": { "type": "System.Boolean", "value": false },
"Order": { "type": "System.Int32", "value": 0 },
"ReadOnly": { "type": "System.Boolean", "value": false },
"VisibleWhenPropertyName": {
"type": "System.String",
"value": "VisibleWhenPropertyName"
},
"VisibleWhenPropertyValue": {
"type": "System.Object",
"value": "VisibleWhenPropertyValue"
},
"uiControl": true,
"uiValidation": false,
"type": "Raptor.Core.DataAnnotations.Form.TextBoxAttribute"
},
"uiControlType": "textbox",
"type": "System.String",
"order": 0,
"displayName": "Name",
"placeholder": "NamePlaceHolder"
},
...
}
}
Next call will provide us intead with the data of the entity, whose life cycle is to be managed::
{
"Name": "Name",
"Surname": "Surname",
"BirthDate": null,
"TermsOfService": false,
"Id": 0
}
The result of the elaboration on client-side is the following:

In the demo, that you can find in my GitHub account, I created a simplified version of this process: in the assets folder, I replaced HTTP calls that obtain required JSON, with example files. Thus we can focus exclusively on the dynamic generation of the interface.
FormComponent
We start from the component, that represents the container of the form we want to generate, called FormComponent:
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { forkJoin } from 'rxjs';
import { FormService } from '../services/form.service';
import { RaptorDetailModel } from './form.model';
@Component({
selector: 'app-form',
templateUrl: 'form.component.html'
})
export class FormComponent implements OnInit {
public detailsForm: FormGroup;
public detail: RaptorDetailModel;
constructor(private service: FormService) { }
ngOnInit() {
this.detailsForm = new FormGroup({});
this.loadData();
}
private loadData() {
forkJoin(
this.service.getMetadata(),
this.service.getData()
).subscribe(result => {
this.detail = {} as RaptorDetailModel;
this.detail.viewModelSchema = result[0];
this.detail.viewModelData = result[1];
this.initDetail();
});
}
private initDetail() {
this.detail.viewModelDisplayName = this.detail.viewModelSchema['name'];
this.detail.propertiesDescriptors = [];
this.detail.viewModelProperties = this.detail.viewModelSchema['properties'];
this.detail.viewModelPropertiesKeys = Object.keys(
this.detail.viewModelSchema['properties']
);
this.detail.viewModelPropertiesKeys.forEach(propertyKey => {
this.detail.propertiesDescriptors.push({
key: propertyKey,
schema: this.detail.viewModelProperties[propertyKey],
viewModelData: this.detail.viewModelData,
order: this.detail.viewModelProperties[propertyKey].order,
dependencies: this.detail.viewModelDependencies
});
});
}
public submit() {
if (this.detailsForm.valid) {
alert(JSON.stringify(this.detailsForm.value));
} else {
Object.keys(this.detailsForm.controls).forEach(field => {
const control = this.detailsForm.get(field);
control.markAsTouched({ onlySelf: true });
});
}
}
}
As you can see from the code, after the component creation, the method loadData(), that hands to make two calls to the backend (metadata and data), is called. Since we need both of them to go on, we should wait until both calls finish their job. We use the forkJoin operator for this purpose. If everything is ok, we divide the answers in two properties viewModelSchema (metadata) and viewModelData (data) and invoke the private method initDetail() to initialize the properties, that will contain all information we need.
Information we need are defined in the interface RaptorDetailForm:
- viewModelDisplayName, which contains the name of the entity to edit;
- propertiesDescriptors, which contains metadata on every form property;
- viewModelProperties, which contains data of every property of the form;
- viewModelPropertiesKeys, which contains the keys of every property of the form.
These values will simplify the generation of the interface. The submit() method, as you can imagine, will allow us to manage the transmission of the form to the server, together with data edited by the user.
In this demo, we simulate the data transmission with an alert(), but it’s interesting to look at else branch, to analyze how to display validation errors at the click on the button. The markAsTouched() method of AbstractControl (the basis class of Angular forms checks ) exactly do what it states: it ‘touches’ the control and doesn’t modify it, in order to cause the update, which would normally occur with focus and blur events. In fact, we are forcing the form validation, if it fails.
The component markup is really simple:
<div *ngIf="detail">
<h3>{{detail.viewModelDisplayName}}</h3>
<form class="form-horizontal" novalidate
[formGroup]="detailsForm">
<div class="row">
<app-control-container
[detailsForm]="detailsForm"
*ngFor="let property of detail.propertiesDescriptors"
[viewModelProperty]="property">
</app-control-container>
</div>
<div class="row mt-5">
<button type="button"
class="btn btn-primary col-12"
scrollToInvalidField (click)="submit()">
OK
</button>
</div>
</form>
</div>
As you can see, there’s a title, a form, a button to invoke the submit() method and a for cycle to generate every single control, implemented in the app-control-container component.
ControlContainer
The component ControlContainer deals with the wrapping of effective controls, that made up the form, starting from metadata received from the server. What we are going to do with this control, is to select the component to render, create the AbstractControl to add to the FormGroup and apply requested validations. We will add also some classes, selected from metadata, that will allow the layout of the component, based on the Bootstrap grid system.
@Component({
selector: 'app-control-container',
templateUrl: 'control-container.component.html'
})
export class ControlContainerComponent implements OnInit {
@Input() detailsForm: FormGroup;
@Input() formControl: AbstractControl;
@Input() viewModelProperty: RaptorPropertyDescriptorModel;
@HostBinding('class') controlClass = '';
ngOnInit() {
this.formControl = new FormControl(this.viewModelProperty.viewModelData[this.viewModelProperty.key]);
this.detailsForm.addControl(this.viewModelProperty.key, this.formControl);
this.addValidationsToFormControl();
this.addClassesToControl();
}
}
In the template, a simple ngSwitch allows us to choose the control to display, passing in input the form and associated metadata. We add an invisible label too, for any kind of validation expected in the field we show in case of validation error.
<ng-container [ngSwitch]="viewModelProperty.schema.uiControlType">
<ng-container *ngSwitchCase="'textbox'">
<app-textbox [detailsForm]="detailsForm" [viewModelProperty]="viewModelProperty">
</app-textbox>
</ng-container>
<ng-container *ngSwitchCase="'checkbox'">
<app-checkbox [detailsForm]="detailsForm" [viewModelProperty]="viewModelProperty">
</app-checkbox>
</ng-container>
<ng-container *ngSwitchCase="'date'">
<app-date [detailsForm]="detailsForm" [viewModelProperty]="viewModelProperty">
</app-date>
</ng-container>
</ng-container>
<ng-container *ngIf="this.detailsForm.get(this.viewModelProperty.key).touched || this.detailsForm.get(this.viewModelProperty.key).dirty">
<ng-container *ngFor="let item of validationDictionary">
<small
class="form-text text-danger"
*ngIf="this.detailsForm.hasError(item.validation, this.viewModelProperty.key)" >
{{validations[item.validationGlobal]?.errorMessage}}
</small>
</ng-container>
</ng-container>
Let’s see in detail how validations and Bootstrap class setup for layout will be associated.
Our problem, here, is to map the validations that arrive us from metadata, with Validator of ReactiveForms of Angular. Every element of the form may have one validation or more, we create then an array of ValidatorFn, where we insert all validations associated with the field. The block for takes care to verify that among field properties, there are also those defined in VALIDATION_GLOBALS and, through the method extractValidatorsFromValidationName(), it converts them in the validations provided to the class Validators. At the end of the cycle, we set validations found through the method setValidators() of AbstractControl.
control-container.component.ts
public validations: { [key: string]: RaptorValidationModel } = { };
public validationDictionary: { validation: string, validationGlobal: VALIDATION_GLOBALS }[] = [
{validation: 'required', validationGlobal: VALIDATION_GLOBALS.REQUIRED },
{validation: 'pattern', validationGlobal: VALIDATION_GLOBALS.REGULAR_EXPRESSION },
{validation: 'maxlength', validationGlobal: VALIDATION_GLOBALS.STRING_LENGTH },
{validation: 'minlength', validationGlobal: VALIDATION_GLOBALS.STRING_LENGTH },
{validation: 'email', validationGlobal: VALIDATION_GLOBALS.EMAIL },
];
private addValidationsToFormControl() {
const validators: ValidatorFn[] = [];
for (const key in this.viewModelProperty.schema) {
if (this.viewModelProperty.schema[key].hasOwnProperty(VALIDATION_GLOBALS.VALIDATION_PROPERTY)
&& this.viewModelProperty.schema[key][VALIDATION_GLOBALS.VALIDATION_PROPERTY] === true) {
const validation = this.viewModelProperty.schema[key];
this.validations[key] = {
errorMessage: validation[VALIDATION_GLOBALS.ERROR_MESSAGE].value
};
this.extractValidatorsFromValidationName(validation, key, validators);
}
}
this.detailsForm.controls[this.viewModelProperty.key].setValidators(validators);
}
private extractValidatorsFromValidationName(validation: any, key: string, validators: ValidatorFn[]) {
switch (key) {
case VALIDATION_GLOBALS.EMAIL:
validators.push(Validators.email);
break;
case VALIDATION_GLOBALS.REGULAR_EXPRESSION:
validators.push(Validators.pattern(validation[VALIDATION_GLOBALS.PATTERN].value));
break;
case VALIDATION_GLOBALS.REQUIRED:
validators.push(Validators.required);
break;
case VALIDATION_GLOBALS.STRING_LENGTH:
if (validation[VALIDATION_GLOBALS.MAXIMUM_LENGTH]) {
validators.push(Validators.maxLength(validation[VALIDATION_GLOBALS.MAXIMUM_LENGTH].value));
}
if (validation[VALIDATION_GLOBALS.MINIMUM_LENGTH]) {
validators.push(Validators.minLength(validation[VALIDATION_GLOBALS.MINIMUM_LENGTH].value));
}
break;
}
}
To set the classes, we run thus a cycle on the diagram properties, we verify if they belong to uiControl and that they are part of the classes expected by FORM_GLOBAL. If the result is positive, we modify the class name in order to let it corresponds the Bootstrap one, and assign it to the attribute controlClass that is in HostBinding with class. HostBinding allows us to project this property on the tag component, which action is required by Bootstrap to do its job (for more information, you can consult the article by Michele: Angular Advanced Components: how to create a dashboard).
control-container.component.ts
private addClassesToControl() {
const dataAnnotations = Object.keys(this.viewModelProperty.schema);
dataAnnotations.forEach(annotationKey => {
const jsonAnnotation = this.viewModelProperty.schema[annotationKey];
if (
jsonAnnotation.hasOwnProperty(FORM_GLOBALS.UI_CONTROL) &&
jsonAnnotation[FORM_GLOBALS.UI_CONTROL] === true
) {
if (jsonAnnotation.hasOwnProperty(FORM_GLOBALS.COL_LG)) {
this.addClass(jsonAnnotation, FORM_GLOBALS.COL_LG);
}
if (jsonAnnotation.hasOwnProperty(FORM_GLOBALS.COL_MD)) {
this.addClass(jsonAnnotation, FORM_GLOBALS.COL_MD);
}
if (jsonAnnotation.hasOwnProperty(FORM_GLOBALS.COL_SM)) {
this.addClass(jsonAnnotation, FORM_GLOBALS.COL_SM);
}
if (jsonAnnotation.hasOwnProperty(FORM_GLOBALS.COL_XS)) {
this.addClass(jsonAnnotation, FORM_GLOBALS.COL_XS);
}
}
});
}
private addClass(jsonAnnotation, jsonClass) {
this.controlClass = this.controlClass + ' ' +
this.humanizeClassStyleString(jsonClass, '-') +
'-' +
jsonAnnotation[jsonClass].value;
}
private humanizeClassStyleString(target, separator) {
return target
.replace(/([A-Z])/g, ' $1')
.replace(/([A-Z])/g, function(str) {
return str.toLowerCase();
})
.trim(' ')
.replace(' ', separator);
}
Checks
We only need to define the checks. In the demo, I defined only three for simplicity: the checkbox, the textbox and the date picker. The target is to make the creation of these controls the more stupid as possible, focusing the requested logic in a basic control, that we will call BaseControl:
import { Injectable, OnInit, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { ViewModelProperty } from './controls.model';
@Injectable()
export abstract class BaseControl implements OnInit {
@Input() viewModelProperty: ViewModelProperty;
@Input() detailsForm: FormGroup;
public controlType: string;
public controlKey: string;
public controlLabel: string;
public controlValue: any;
ngOnInit() {
this.controlType = this.viewModelProperty.schema.uiControlType;
this.controlKey = this.viewModelProperty.key;
this.controlLabel = this.viewModelProperty.schema['displayName'];
this.controlValue = this.viewModelProperty.viewModelData[this.viewModelProperty.key];
}
}
As you can see, if we have in input all the needed information, we can just divide them into single properties, much convenient to use. Now, single components, that represent the interface checks, just have to extend this component.
Checkbox
import { Component } from '@angular/core';
import { BaseControl } from '../base-control';
@Component({
selector: 'app-checkbox',
templateUrl: 'checkbox.component.html'
})
export class CheckboxComponent
extends BaseControl {
}
<div class="form-check mt-2 mt-sm-2 mb-2 mr-sm-2 mb-sm-0"
[ngClass]="{'has-danger': !isValid}" [formGroup]="detailsForm">
<label class="form-check-label">
<input [formControlName]="controlKey" type="checkbox" class="form-check-input"> {{controlLabel}}
</label>
</div>
Textbox
import { Component} from '@angular/core';
import { BaseControl } from '../base-control';
@Component({
selector: 'app-textbox',
templateUrl: 'textbox.component.html'
})
export class TextboxComponent
extends BaseControl {
}
Datepicker
import { Component } from '@angular/core';
import { BaseControl } from '../base-control';
@Component({
selector: 'app-date',
templateUrl: './date.component.html',
})
export class DateComponent
extends BaseControl {
}
<div class="form-group mb-1" [formGroup]="detailsForm">
<label >{{controlLabel}}</label>
<div class="input-group">
<input [formControlName]="controlKey" class="form-control" ngbDatepicker #d="ngbDatepicker">
<div class="input-group-append">
<button class="input-group-append btn btn-outline-secondary" (click)="d.toggle()" type="button">
<i class="fa fa-calendar"></i>
</button>
</div>
</div>
</div>
ScrollToInvalidFieldDirective
During the test on complex forms, I realize that, scrolling the page and clicking on the enter button, invisible validation errors don’t let the user understand which was the problem, due to which the click doesn’t return any feedback. I want to add then a functionality that, when invalid fields are present, will automatically scroll down the page, showing the first invalid field.
By a guideline put on the form submit, I intercept the click on its and, with a simple query, I verify if there are elements with ng-invalid class: if so, I scroll to the first element found.
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[scrollToInvalidField]'
})
export class ScrollToInvalidFieldDirective {
@HostListener('click') onClick() {
const elementList = document.querySelectorAll('input.ng-invalid');
const element = elementList[0] as HTMLElement;
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
}
Simple and effective:

You can download the code here https://github.com/AARNOLD87/AngularDymanicForm
See you next!