Saturday, 2 February 2019

Angular reactive forms cross field validation

Angular reactive forms cross field validation


Let us understand this with an example. We want to ensure Email and Confirm Email fields have the same value. If they do not match, we want to display a validation message. 

angular reactive cross field validation 

ConfirmEmail field is required and if no value is present it should display the required validation error. 

angular 6 cross field validation 

So in short, here is the requirement 
  • Confirm Email field is required and if a value is present it should match with the Email field value.
  • If no value is entered, it should display required error
  • If a value is present and does not match with Email field, it should display do not match validation error
To validate if Email and Confirm Email fields have same value, we need to compare 2 Form Controls. If you look at a Validator function in Angular, it only accepts either a FormGroup or a FormControl as a parameter. We cannot pass 2 form controls to the validator function, but what we can do is group them using a nested formgroup and then pass that nested formgroup as a parameter to the Validator function.

Changes in the Template (create-employee.component.html) 

<div formGroupName="emailGroup">
  <div class="form-group" [ngClass]="{'has-error': formErrors.email}">
    <label class="col-sm-2 control-label" for="email">Email</label>
    <div class="col-sm-8">
      <input id="email" type="text" class="form-control"
             formControlName="email" (blur)="logValidationErrors()">
      <span class="help-block" *ngIf="formErrors.email">
        {{formErrors.email}}
      </span>
    </div>
  </div>

  <div class="form-group" [ngClass]="{'has-error': formErrors.confirmEmail
                                                || formErrors.emailGroup}">
    <label class="col-sm-2 control-label" for="confirmEmail">
      Confirm Email
    </label>
    <div class="col-sm-8">
      <input id="confirmEmail" type="text" class="form-control"
             formControlName="confirmEmail" (blur)="logValidationErrors()">
      <span class="help-block"
            *ngIf="formErrors.confirmEmail || formErrors.emailGroup">
        {{formErrors.confirmEmail ? formErrors.confirmEmail
          : formErrors.emailGroup}}
      </span>
    </div>
  </div>
</div>
  • Notice email and confirmEmail form controls are nested in a formgroup with name emailGroup
  • Bootstrap has-error class is conditionally added if either confirmEmail or emailGroup properties of the formErrors object are truthy
  • confirmEmail property stores required error - Confirm Email is required.
  • emailGroup property stores do not match error - Email and Confirm Email do not match
  • We do not have these 2 properties on the formErrors object yet. We will add them in the component class in just a bit.
  • Similarly, the span element that displays the validation error is bound to confirmEmail or emailGroup properties of the formErrors object. So the span element is displayed only if either of the properties are truthy.
If the Email form control has a value and if nothing is filled in the confirmEmail form control, we do not want both the required error and do not match error to be displayed. The following interpolation expression, ensures to display the right validation message. 

{{formErrors.confirmEmail ? formErrors.confirmEmail : formErrors.emailGroup}}

Changes in the Component Clas (create-employee.component.ts) : The changes are commented and self-explanatory. 

// Group properties on the formErrors object. The UI will bind to these properties
// to display the respective validation messages
formErrors = {
  'fullName': '',
  'email': '',
  'confirmEmail': '',
  'emailGroup': '',
  'phone': '',
  'skillName': '',
  'experienceInYears': '',
  'proficiency': ''
};

// This structure stoes all the validation messages for the form Include validation
// messages for confirmEmail and emailGroup properties. Notice to store the
// validation message for the emailGroup we are using emailGroup key. This is the
// same key that the matchEmails() validation function below returns, if the email
// and confirm email do not match.
validationMessages = {
  'fullName': {
    'required': 'Full Name is required.',
    'minlength': 'Full Name must be greater than 2 characters',
    'maxlength': 'Full Name must be less than 10 characters.',
  },
  'email': {
    'required': 'Email is required.',
    'emailDomain': 'Email domian should be dell.com'
  },
  'confirmEmail': {
    'required': 'Confirm Email is required.'
  },
  'emailGroup': {
    'emailMismatch': 'Email and Confirm Email do not match.'
  },
  'phone': {
    'required': 'Phone is required.'
  },
  'skillName': {
    'required': 'Skill Name is required.',
  },
  'experienceInYears': {
    'required': 'Experience is required.',
  },
  'proficiency': {
    'required': 'Proficiency is required.',
  },
};

// email and confirmEmail form controls are grouped using a nested form group
// Notice, the validator is attached to the nested emailGroup using an object
// with key validator. The value is our validator function matchEmails() which
// is defined below. The important point to keep in mind is when the validation
// fails, the validation key is attached the errors collection of the emailGroup
// This is the reason we added emailGroup key both to formErrors object and
// validationMessages object.
ngOnInit() {
  this.employeeForm = this.fb.group({
    fullName: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(10)]],
    contactPreference: ['email'],
    emailGroup: this.fb.group({
      email: ['', [Validators.required, emailDomain('dell.com')]],
      confirmEmail: ['', [Validators.required]],
    }, { validator: matchEmails }),
    phone: [''],
    skills: this.fb.group({
      skillName: ['', Validators.required],
      experienceInYears: ['', Validators.required],
      proficiency: ['', Validators.required]
    }),
  });

  this.employeeForm.valueChanges.subscribe((data) => {
    this.logValidationErrors(this.employeeForm);
  });

  this.employeeForm.get('contactPreference').valueChanges.subscribe((data: string) => {
    this.onContactPrefernceChange(data);
  });
}

logValidationErrors(group: FormGroup = this.employeeForm): void {
  Object.keys(group.controls).forEach((key: string) => {
    const abstractControl = group.get(key);
    this.formErrors[key] = '';
    // Loop through nested form groups and form controls to check
    // for validation errors. For the form groups and form controls
    // that have failed validation, retrieve the corresponding
    // validation message from validationMessages object and store
    // it in the formErrors object. The UI binds to the formErrors
    // object properties to display the validation errors.
    if (abstractControl && !abstractControl.valid
      && (abstractControl.touched || abstractControl.dirty)) {
      const messages = this.validationMessages[key];
      for (const errorKey in abstractControl.errors) {
        if (errorKey) {
          this.formErrors[key] += messages[errorKey] + ' ';
        }
      }
    }

    if (abstractControl instanceof FormGroup) {
      this.logValidationErrors(abstractControl);
    }
  });
}

Finally, include the following validator function in create-employee.component.ts file, after the closing curly brace (}) of the CreateEmployeeComponent class. 

// Nested form group (emailGroup) is passed as a parameter. Retrieve email and
// confirmEmail form controls. If the values are equal return null to indicate
// validation passed otherwise an object with emailMismatch key. Please note we
// used this same key in the validationMessages object against emailGroup
// property to store the corresponding validation error message
function matchEmails(group: AbstractControl): { [key: string]: any } | null {
  const emailControl = group.get('email');
  const confirmEmailControl = group.get('confirmEmail');

  if (emailControl.value === confirmEmailControl.value || confirmEmailControl.pristine) {
    return null;
  } else {
    return { 'emailMismatch': true };
  }
}

Angular Reusable Custom Validator

Angular Reusable Custom Validator


In this topic we will discuss how to make a custom validator reusable in Angular.

The built-in validators in angular like the following, are reusable. This means we can use them with any form control on any angular form.  
  • required
  • min
  • max
  • minlength
  • maxlength
  • pattern

To be able to use one of the built-in angular validator, all we have to do is import the Validators class from '@angular/forms' package. 

import { Validators } from '@angular/forms';

Once the Validators class is imported, use the validator functions on the form control that you want to validate. 

fullName: ['', [Validators.required, Validators.minLength(2)]]

All the buil-in validator functions are marked as static functions in the Validators class. This allows us to use the validator functions, without the need to create an instance of the Validators class. 

Along the same lines let's make our emailDomain custom validator function reusable by including it as a static function in a separate class. 

We want to make this validator function available to all form controls on all forms. So, create a shared folder. In the shared folder, create a file with name custom.validators.ts and include the following code. 

import { AbstractControl } from '@angular/forms';

export class CustomValidators {
    static emailDomain(domainName: string) {
        return (control: AbstractControl): { [key: string]: any } | null => {
            const email: string = control.value;
            const domain = email.substring(email.lastIndexOf('@') + 1);
            if (email === '' || domain.toLowerCase() === domainName.toLowerCase()) {
                return null;
            } else {
                return { 'emailDomain': true };
            }
        };
    }
}

Using the reusable Custom Validators : First, Import the CustomValidators class. Just like how we import the built-in Validators class, import the CustomValidators class from custom.validators.ts file. 

import { CustomValidators } from '../shared/custom.validators';

Tie the validator function to a form control that you want to validate. 

email: ['', [CustomValidators.emailDomain('dell.com')]]

Angular reactive form custom validator with parameter

Angular reactive form custom validator with parameter


In our previous topic we discussed creating a custom email domain validator. The following is that validator function. 

function emailDomain(control: AbstractControl): { [key: string]: any } | null {
  const email: string = control.value;
  const domain = email.substring(email.lastIndexOf('@') + 1);
  if (email === '' || domain.toLowerCase() === 'pragimtech.com') {
    return null;
  } else {
    return { 'emailDomain': true };
  }
}

Notice, the domain name 'pragimtech.com' is hard coded. So this custom validator, only works if you want to check if the domain is pragimtech.com. What if you want to check another domain like microsoft.com. We want to make this custom validator reusable with any domain name. We should be able to pass the domain name as a parameter to the emailDomain custom validator function. 

Notice in the example below, we are passing pragimtech.com as the domain name. If you want to check for a different domain, you simply pass that domain name as a parameter. 

email: ['', [emailDomain('pragimtech.com')]]

The following built-in validators have parameters. 
  • min
  • max
  • minlength
  • maxlength
Notice the min() built-in validator function. It takes in a number as a parameter and returns ValidatorFn. 

min(min: number): ValidatorFn;

So, what is ValidatorFn?
ValidatorFn stands from validator function. So this min() function is taking in a number as a parameter and returns a validator function. If you understand the concept of closure in JavaScript, then this is very easy to understand. We discussed closures in detail in Parts 27 and 28 of JavaScript tutorial.

In simple terms, you can thinks of a closure as, a function inside another function i.e an inner function and an outer function. The inner function has access to the outer function’s variables and parameters in addition to it's own variables and parameters.

Now that task at hand for us, is to convert our emailDomain() function to take in the domain name as a parameter and return a validator function. To be able to do this we are going to take the advantage of closures in JavaScript.

ValidatorFn is an interface and the signature of the function it returns is as shown below. It takes the AbstractControl that we want to validate as an input parameter and returns null or ValidationErrors object. Null if the validation succeeds and a ValidationErrors object is the validation has failed. 

(c: AbstractControl): ValidationErrors | null;

Custom Validator with parameter 

function emailDomain(domainName: string) {
  return (control: AbstractControl): { [key: string]: any } | null => {
    const email: string = control.value;
    const domain = email.substring(email.lastIndexOf('@') + 1);
    if (email === '' || domain.toLowerCase() === domainName.toLowerCase()) {
      return null;
    } else {
      return { 'emailDomain': true };
    }
  };
}

Code Explanation 
  • We have 2 functions here. An inner function and an outer function.
  • The outer function has a name (emailDomain), but the inner function does not have a name. It is an anonymous function.
  • The inner anonymous function has access to the outer function parameter domainName.
  • You can have as many parameters as you want in the outer function, then inner function will have access to all of them in addition to it's own parameters.
Passing the value for the custom validator parameter 

email: ['', [Validators.required, emailDomain('dell.com')]]

Finally do not forget to updated the validation error message in the validationMessages structure 

validationMessages = {
  'email': {
    'required': 'Email is required.',
    'emailDomain': 'Email domian should be dell.com'
  },
  'proficiency': {
    'required': 'Proficiency is required.',
  },
};