Generic Validation Service for Reactive Forms

Introduction 

 
In every Angular application, there are multiple components where we need common tasks related to validating forms, such as:
  • Having regex expressions that are used in inputs across the app.
  • Matching two form fields, like password and confirm password.
  • Displaying form error messages. 
Writing code to get the message in every component is time-consuming. In this article, we will learn about creating a common validation service to help validate a reactive form and display form error messages.
 
There are two types of forms in Angular: Template Driven Forms and Reactive Forms. Today, we will focus on Reactive forms. 
 

Reactive Form Validation


Reactive forms are also known as model-driven forms. This means that the HTML content changes depending on the code in the component. In reactive forms, we add validator functions directly to the form control model in the component class. Angular then calls these functions whenever the value of the control changes. 
 
Advantages of using Reactive Forms:
  • It's easier to write unit tests in reactive forms.
  • It's easier to handle validation in reactive forms
Disadvantage of using Reactive Forms:
  • It requires more coding implementation in the component 
Here, we will focus on this disadvantage and write a common ValidationService so that we require less code in the component.
 
Step 1 - Setup

Create the Angular App.
  1. ng new form-validation   
Install Bootstrap and import the css in style.css
  1. npm i bootstrap  
  1. @import '~bootstrap/dist/css/bootstrap.min.css'  
Import ReactiveFormsModule in app.module.ts 
  1. import {ReactiveFormsModule} from '@angular/forms'  
  2.   
  3. @NgModule({  
  4.    imports: [  
  5.       ...  
  6.       ReactiveFormsModule  
  7.    ]  
  8.   
  9. export class AppModule { }  
Step 2 - Create Validation Service.
  1. @Injectable({    
  2.   providedIn: 'root'    
  3. })    
  4. export class ValidationService {    
  5.   constructor() { }    
  6. }     
We will add one object and three functions to this service.
 

Regex Object

 
Various Form Inputs have regex validation which we can use by Validators.pattern(). There can be multiple components that are using the same regex pattern. Therefore, we will add an object to have all regex values in a single place.
  1. public regex =  
  2. {  
  3.    email: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$' ,  
  4.    pin : '^[0-9]{6}$'  
  5. }   

Function to Validate Matching of Input and Confirm Input fields

 
There are many times when we want to match 2 form inputs, like Password and Confirm Password or AccountNo and ConfirmAccountNo. To do that, we have added a common function named matchConfirmItems() where we can pass the formControl Names that need to be matched. This function will make the Confirm field invalid if the items do not match.
  1. matchConfirmItems(controlName: string, confirmControlName: string) {  
  2.    return (formGroup: FormGroup) => {  
  3.       const control = formGroup.controls[controlName];  
  4.       const confirmControl = formGroup.controls[confirmControlName];  
  5.       if (!control || !confirmControl) {  
  6.          return null;  
  7.       }  
  8.       if (confirmControl.errors && !confirmControl.errors.mismatch) {  
  9.          return null;  
  10.       }  
  11.       if (control.value !== confirmControl.value) {  
  12.          confirmControl.setErrors({ mismatch: true });  
  13.       } else {  
  14.          confirmControl.setErrors(null);  
  15.       }  
  16.    }  
  17. }   

Getting Validation Errors

 
Every form input can have some error we want to display. To get the error for all the inputs at a time we have added a function in the validation service so that we don't have to write it in all components.
 
Here, we will pass the formGroup and validationMessages Object from the component. The sample validationMessages object is added in the next step.
  1. getValidationErrors(group: FormGroup, validationMessages: Object): any {    
  2.      var formErrors = {};    
  3.     
  4.      Object.keys(group.controls).forEach((key: string) => {    
  5.         const abstractControl = group.get(key);    
  6.     
  7.         formErrors[key] = '';    
  8.         if (abstractControl && !abstractControl.valid &&    
  9.            (abstractControl.touched || abstractControl.dirty)) {    
  10.     
  11.            const messages = validationMessages[key];    
  12.     
  13.            for (const errorKey in abstractControl.errors) {    
  14.               if (errorKey) {    
  15.                  formErrors[key] += messages[errorKey] + ' ';    
  16.               }    
  17.            }    
  18.         }    
  19.     
  20.         if (abstractControl instanceof FormGroup) {    
  21.            let groupError = this.getValidationErrors(abstractControl, validationMessages);    
  22.            formErrors = { ...formErrors, ...groupError }    
  23.         }    
  24.      });    
  25.      return formErrors    
  26.   }     
Source code of the complete validation service:
 
 
Step 3 - Add the component 
 
Design the form using formgroup and formBuilder.
  1. constructor(private fb: FormBuilder, private _validation: ValidationService) {}  
  2. this.signUpForm = this.fb.group({  
  3.   Name: ['', [Validators.required]],  
  4.   Email: ['', [Validators.required, Validators.pattern(this._validation.regex.email)]],  
  5.   Password: ['', [Validators.required]],  
  6.   ConfirmPassword: ['', [Validators.required]],  
  7. },  
  8. {  
  9.   validator: this._validation.matchConfirmItems('Password''ConfirmPassword'),  
  10. });   
There are different types of default validations we can use, like required validation, minlength validation, maxlength validation and pattern matching validation etc.
 
In the above formGroup, there are three types of validation,
  • Validators.required
    This is a built-in validation type we have attached to 'Name', 'Password' and 'Confirm Password' formControl.
  • Validators.pattern
    This is also a built-in validation type where we can add a regex pattern. Now inputs like email, phone, etc can be used in multiple forms. Therefore in the validationService we have added an object for all validation regex. We have attached it to the 'Email' formControl.
  • Password and Confirm Password Match
    We have added validator to the formgroup and passed formControlName 'Password' and 'ConfirmPassword' to matchConFirmItems function of Validation Service. This will make the formControl 'ConfirmPassword' invalid when both of these do not match.
Create an object to store the validation messages for each form control.
  1. validationMessages = {  
  2.       'Name': {  
  3.          'required''Name is required.',  
  4.       },  
  5.       'Email': {  
  6.          'required''Email is required.',  
  7.          'pattern''Please provide valid Email ID'  
  8.       },  
  9.       'Password': {  
  10.          'required''Password is required.'  
  11.       },  
  12.       'ConfirmPassword': {  
  13.          'required''Confirm Password is required.',  
  14.          'mismatch''Password and Confirm Password do not match'  
  15.       }  
  16.    };  
Create an object to store the form validation error and update it whenever any form input is changed.
  1. formErrors = {};    
  2. ngOnInit(){    
  3.   this.signUpForm.valueChanges.subscribe(    
  4.       value => {    
  5.          this.logValidationErrors()    
  6.       }    
  7.    );    
  8. }     
  9. logValidationErrors() {    
  10.       this.formErrors = this._validation.getValidationErrors(this.signUpForm, this.validationMessages);    
  11. }   
We will display the validation errors in the template. We will also call the logValidationErrors() on blur of each input.
  1. <form [formGroup]="signUpForm" (ngSubmit)="onSubmit()">    
  2.      <div class="form-group">    
  3.         <label>Name:</label>    
  4.         <input type="text" class="form-control" formControlName="Name" (blur)="logValidationErrors()">    
  5.         <span class="text-danger" *ngIf="formErrors.Name">    
  6.            {{formErrors.Name}}    
  7.         </span>    
  8.      </div>    
  9.      <div class="form-group">    
  10.         <label>Email:</label>    
  11.         <input type="text" class="form-control" formControlName="Email" (blur)="logValidationErrors()">    
  12.         <span class="text-danger" *ngIf="formErrors.Email">    
  13.            {{formErrors.Email}}    
  14.         </span>    
  15.      </div>    
  16.      <div class="form-group">    
  17.         <label>Password:</label>    
  18.         <input type="text" class="form-control" formControlName="Password" (blur)="logValidationErrors()">    
  19.         <span class="text-danger" *ngIf="formErrors.Password">    
  20.            {{formErrors.Password}}    
  21.         </span>    
  22.      </div>    
  23.      <div class="form-group">    
  24.         <label>ConfirmPassword:</label>    
  25.         <input type="text" class="form-control" formControlName="ConfirmPassword" (blur)="logValidationErrors()">    
  26.         <span class="text-danger" *ngIf="formErrors.ConfirmPassword">    
  27.            {{formErrors.ConfirmPassword}}    
  28.         </span>    
  29.      </div>    
  30.      <div class="row">    
  31.         <div class="col-md-12">    
  32.            <button type="submit" class="btn btn-success" [disabled]="!signUpForm.valid">Submit</button>    
  33.         </div>    
  34.      </div>    
  35. </form>     
Find the complete code in Stackbiltz Demo and GitHub Repo