Saturday 2 February 2019

Move validation logic to the component class in reactive form

                Move validation logic to the component class in reactive form


In this topic we will discuss, how to move the logic to show and hide validation messages from the view template into the component class

create-employee.component.html : Consider the following HTML in CreateEmployeeComponent view template. This HTML is for the "Full Name" field. The logic to add or remove has-error class is in the template. Also the validation message and the logic to show and hide it is also in the template at the moment. 

<div class="form-group"
      [ngClass]="{'has-error': ((employeeForm.get('fullName').touched ||
                                employeeForm.get('fullName').dirty) &&
                                employeeForm.get('fullName').errors)}">
  <label class="col-sm-2 control-label" for="fullName">Full Name</label>
  <div class="col-sm-8">
    <input id="fullName" type="text" class="form-control"formControlName="fullName">
    <span class="help-block" *ngIf="((employeeForm.get('fullName').touched ||
                                      employeeForm.get('fullName').dirty) &&
                                      employeeForm.get('fullName').errors)">
      <span *ngIf="employeeForm.get('fullName').errors.required">
        Full Name is required
      </span>
      <span *ngIf="employeeForm.get('fullName').errors.minlength ||
                    employeeForm.get('fullName').errors.maxlength">
        Full Name must be greater than 2 characters and less than 10 characters
      </span>
    </span>
  </div>
</div>

Now, let's move all of this into the component class. Modify the HTML as shown below. Notice, now we are binding to formErrors.fullName property. All the complex logic is moved to the component class. Notice the HTML here is much less than what we have had before. 

<div class="form-group" [ngClass]="{'has-error': formErrors.fullName}">
  <label class="col-sm-2 control-label" for="fullName">Full Name</label>
  <div class="col-sm-8">
    <input id="fullName" type="text" class="form-control"formControlName="fullName">
    <span class="help-block" *ngIf="formErrors.fullName">
      {{formErrors.fullName}}
    </span>
  </div>
</div>

Changes in create-employee.component.ts file : The changes are commented and self-explanatory 

formErrors = {
  'fullName''',
  'email''',
  'skillName''',
  'experienceInYears''',
  'proficiency'''
};

validationMessages = {
  'fullName': {
    'required''Full Name is required.',
    'minlength''Full Name must be greater than 2 characters.',
    'maxlength''Full Name must be less than 2 characters.',
  },
  'email': {
    'required''Email is required.'
  },
  'skillName': {
    'required''Skill Name is required.',
  },
  'experienceInYears': {
    'required''Experience is required.',
  },
  'proficiency': {
    'required''Proficiency is required.',
  },
};

ngOnInit() {

  this.employeeForm = this.fb.group({
    fullName: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(10)]],
    email: ['', Validators.required],
    skills: this.fb.group({
      skillName: ['', Validators.required],
      experienceInYears: ['', Validators.required],
      proficiency: ['', Validators.required]
    }),
  });

  // When any of the form control value in employee form changes
  // our validation function logValidationErrors() is called
  this.employeeForm.valueChanges.subscribe((data) => {
    this.logValidationErrors(this.employeeForm);
  });

}

logValidationErrors(group: FormGroup = this.employeeForm): void {
  Object.keys(group.controls).forEach((key: string) => {
    const abstractControl = group.get(key);
    if (abstractControl instanceof FormGroup) {
      this.logValidationErrors(abstractControl);
    } else {
      this.formErrors[key] = '';
      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] + ' ';
          }
        }
      }
    }
  });
}

The only problem at the moment is that when a control loses focus, our validation is not triggered. This is because valueChanges observable does not emit an event when the control loses focus. It only emits an event when the value changes. 

One work around for this is to bind to the blur event and call validation function (logValidationErrors()) manually. 

<input id="fullName" type="text" class="form-control"
        formControlName="fullName" (blur)="logValidationErrors()">

Here is the HTML for Email, Skill Name, Experience and Proficiency input elements. 

<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="well">
  <div formGroupName="skills">

    <div class="form-group" [ngClass]="{'has-error': formErrors.skillName}">
      <label class="col-sm-2 control-label" for="skillName">
        Skill
      </label>
      <div class="col-sm-4">
        <input type="text" class="form-control" id="skillName"formControlName="skillName"
               (blur)="logValidationErrors()" placeholder="C#, Java, Angular etc...">
        <span class="help-block" *ngIf="formErrors.skillName">
          {{formErrors.skillName}}
        </span>
      </div>
    </div>

    <div class="form-group" [ngClass]="{'has-error': formErrors.experienceInYears}">
      <label class="col-sm-2 control-label" for="experienceInYears">
        Experience
      </label>
      <div class="col-sm-4">
        <input type="text" class="form-control" id="experienceInYears"
               formControlName="experienceInYears" placeholder="In Years"
              (blur)="logValidationErrors()">
        <span class="help-block" *ngIf="formErrors.experienceInYears">
          {{formErrors.experienceInYears}}
        </span>
      </div>
    </div>

    <div class="form-group" [ngClass]="{'has-error': formErrors.proficiency}">
      <label class="col-md-2 control-label">Proficiency</label>
      <div class="col-md-8">
        <label class="radio-inline">
          <input type="radio" value="beginner" formControlName="proficiency"
                 (blur)="logValidationErrors()">Beginner
        </label>
        <label class="radio-inline">
          <input type="radio" value="intermediate" formControlName="proficiency"
                 (blur)="logValidationErrors()">Intermediate
        </label>
        <label class="radio-inline">
          <input type="radio" value="advanced" formControlName="proficiency"
                 (blur)="logValidationErrors()">Advanced
        </label>
        <span class="help-block" *ngIf="formErrors.experienceInYears">
          {{formErrors.proficiency}}
        </span>
      </div>
    </div>
  </div>
</div>

No comments:

Post a Comment