Validation is a very important feature of applications which allows data entry (allows to enter data in application) as invalid data can leave application in inconsistent state, invalid date entry crash application and if data is not validated, it can allow injection attacks on application. In a Web application, validation can be done at server side (i.e., on server like IIS, Apache in language C#, PHP, etc.) and at client side (i.e., in browser windows using JavaScript).
This post is about validation in Angular 2 application, which means it's about validation on client side, not about server side. Angular framework allows to do validation in the following two ways:
- Template Driven Form
- Reactive Form
One of these approaches or a mix of both approaches can be used in application for validation. I am not going to discuss it, as it is provided in detail over here: Forms & Validation. But the following post is going to discuss about custom component, which works with both the validation approach provided by the Angular 2 framework and encapsulates validation code in it.
Custom Validation Component
Let's first create a validation component and make use of it in the application. For a fresh start of application, follow this post: My first Angular 2 app - tutorial. After creating application, create shared module in application by running the below command in integrated terminal of Visual Studio Code.
ng g module shared.
Shared module is where custom validation component will reside, as custom validation component is dumb and it's going to be used by multiple features in application. Run command on integrated terminal creates...
ng g component shared/custom-input
...custom validation component.
custom-input.component.html
<div class="form-group" >
<label class="control-label">{{label}}</label>
<ng-content></ng-content>
<ng-container *ngIf='isError'>
<span class="text-danger" *ngFor="let msg of errorMessages"> {{msg}}</span>
</ng-container>
</div>
</div>
Above is the modification done in component HTML, some of the points to note in HTML are:
ng-content
- which is a very important tag in this HTML and important for validation component. This Angular 2 Framework tag allows us to push any content in HTML when component is loaded. For example:
<custom-input>
<input inputRef class="form-control" type="text" />
</custom-input>
When the above code is written in component, it will become like this:
So input element is put inside ng-content
when component loaded in HTML page.
inputRef
its directive associated with input element, that in turn allows to access input element in component TypeScript (more discussed below) - When custom component is rendered in browser, it looks as below:
So, HTML tags in component will create structure above as shown in the image below:
The image above explains all the tags, which made up component template. Just to note few points:
- Border around the text box controls comes when text box becomes invalid. Border to textbox gets applied by '
has-error
' class, which gets applied isError
becomes true
(in which case, it becomes true
described by code in typescript file below), - "First Name Required"- this message comes when there is an error. For example, in this image, it is error related to required validation. This is passed as input to component described below.
- label,(First Name) - will come from typescript associated with component template. This is passed as input to component described below.
custom-input.component.ts
@Component({
selector: 'custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.css']
})
export class CustomInputComponent implements OnInit {
@Input() label: string;
@Input() validations: { [index: string]: string};
@Input() info: string;
@ContentChild(InputRefDirective) input: InputRefDirective;
get isError() {
return this.input.hasError;
}
get errorMessages() {
const errors = this.input.errors;
const messages = [];
const keys = Object.keys(this.validations);
keys.forEach(key => {
if (errors[key]) {
messages.push(this.validations[key]);
}
});
return messages;
}
ngOnInit() { }
constructor() { }
}
Above is Component Typescript file. Code in file handles all the backend logic to control Component invalid/valid state, putting red border around control when there is error and displaying error message when control is in invalid state.
Type | Name | Description |
Input | | |
| label | Display label text for control, example, First Name in the above image |
| validations | Its indexed object holds validations associated with input tag of component
E.x. {'required' : 'First Name required', 'minlength': 'Minimum length is 3 char'}
Note: indexed property name must need to match with type of validation, example, 'required' key match with required validation |
| info | WIP (Work in progress) |
variable | | |
| input | This component variable allows to get access to input control (which replaces ng-content in template of component) associated with component, for example, the above image in this case 'input textbox'. |
Properties | | |
| isError | Component property which finds input element (with help of input directive) and get element has error or not, which in turn helps to put red border around input control |
| errorMessages | Component property which accesses errors associated input element (with help of input directive) and with the help of input property validations, object find proper error messages and return to display all. |
The important thing to note here is InputRefDirective
, which accesses input control. Below is code for input-ref directive.
input-ref.directive.ts (Created by ng g directive directive/InputRef)
import { Directive } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[inputRef]'
})
export class InputRefDirective {
constructor(private formControl: NgControl) {
}
get hasError() {
return this.formControl.dirty && this.formControl.invalid;
}
get errors() {
if (this.hasError && this.formControl.errors) {
return this.formControl.errors;
}
return '';
}
}
Important part of input-ref
directive is, with the help of constructor injection (by using angular framework injection), NgControl
gets injected in the input-ref
directive. By doing that, directive class gets access to input control.
Type | Name | Description |
Properties | | |
| hasError | Property returns true (when control is dirty and invalid state) or false , to indicate there is an error or not |
| errors | Property returns list of errors associated with input control |
So it's end of creation of validation component and its related directive. Now let's see how to use it in application.
app.component.html
Reactive Form
<form class="needs-validation" [formGroup]="detailForm" novalidate>
<custom-input [label]="'First Name'" [validations]="{required:'First Name required'}">
<input inputRef class="form-control" formControlName="firstName" type="text" />
</custom-input>
<custom-input [label]="'Last Name'" [validations]="{required:'Last Name required'}">
<input inputRef class="form-control" formControlName="lastName" type="text" />
</custom-input>
<div>
<button [disabled]="detailForm.invalid" type="button" class="btn btn-primary">Primary</button>
</div>
</form>
Component.ts for Reactive Form Only
The code below creates form reactive way. The point to note here is that there is not much code to access control and display error (discussed below in advantages).
export class AppComponent implements OnInit {
title = 'app';
detailForm: FormGroup;
constructor(private fb: FormBuilder) {
this.createForm();
}
ngOnInit() {
}
private createForm() {
this.detailForm = this.fb.group({
firstName: new FormControl('', [Validators.required]),
lastName: new FormControl('', [Validators.required]),
});
}
}
Template Driven Form
<form class="needs-validation" #detailForm="ngForm" novalidate>
<custom-input [label]="'First Name'" [validations]="{required:'First Name required'}">
<input inputRef class="form-control" [(ngModel)]="firstName" name="firstName" type="text" />
</custom-input>
<custom-input [label]="'Last Name'" [validations]="{required:'Last Name required'}">
<input inputRef class="form-control" [(ngModel)]="lastName" name="lastName" type="text" />
</custom-input>
<div>
<button [disabled]="detailForm.invalid"
type="button" class="btn btn-primary">Primary</button>
</div>
</form>
It's not full template of application component code, but it's part of template which uses Custom validation component in both ways, Reactive Form and Template Driven Form way. So not much change in both ways, except change forced by reactive form approach and template driven form approach.
Not much in app.component
template to discuss as already discussed before in post. app.component
template makes use of custom validation component and passes all required input values (label, info and validation error).
Below is the image of final form after making all changes:
Advantage of Using Custom Validation Component
- Less code in component template,
Template driven from:
<label class="control-label">First Name</label>
<input
type="text"
name="firstName"
[(ngModel)]="firstName"
minlength="2"
required>
<div *ngIf="firstName.errors?.required && firstName.touched && firstName.touched"
class="label label-danger">
Name is required
</div>
<div *ngIf="firstName.errors?.minlength && firstName.touched && firstName.touched"
class="label label-danger">
Minimum of 2 characters
</div>
Reactive from
component.ts
this.detailForm = this.form.group({
firstName: [null, Validators.compose([Validators.required, Validators.minLength(2)])],
});
component.html
<label class="control-label">First Name</label>
<input
type="text"
name="firstName"
[(ngModel)]="firstName"
minlength="2"
required>
<div *ngIf="detailForm.get('firstName').errors?.required &&
detailForm.get('firstName').touched && detailForm.get('firstName').touched"
class="label label-danger">
Name is required
</div>
<div *ngIf="detailForm.get('firstName').errors?.minlength &&
detailForm.get('firstName').touched && detailForm.get('firstName').touched"
class="label label-danger">
Minimum of 2 characters
</div>
If there is no custom validation component, in both ways (Template driven or Reactive way), one needs to write down separate div
for displaying each error input control has. For example, div
for required error and div
for minlength
.
But with custom validation control, there is just need of passing validations in validations-property of control and it will take care of it.
<custom-input [label]="'First Name'"
[validations]="{required:'First Name required', 'minlength':'Minimum of 2 characters'}">
<input inputRef class="form-control" [(ngModel)]="firstName" name="firstName" type="text" />
</custom-input>
Even there is no need to write extra label
tag for displaying label for input control. This is also taken care by custom validation control label
-property.
- Second major difference is, custom validation control encapsulates code of validation in one place only for full application. That means there is no need to write validation code in template in each and every data entry form.
Wrapping Up
Custom validation control is easy and simple to use. It makes use of some advanced stuff like ng-content, directive to get input control under the hood and encapsulate thing in it. Once control is included in application, it reduces the amount of validation code & effort to write validation code in every data entry form.
Full source : Download source (GitHub)