Search code examples
angularangular-materialangular-reactive-formscross-validationcustomvalidator

Angular 7 Reactive Form Builder, Custom Cross Validator Not Working


Here is my HTML:

<mat-form-field>
    <input matInput type="number" min="0" max="100" step="1" placeholder="Allowed Attendance (%)" required [formControl]="allowed" name="allowed_attendance">
    <mat-error *ngIf="allowed.invalid">Value must be 0-100</mat-error>
</mat-form-field>
<mat-form-field>
    <input matInput type="number" min="0" max="100" step="1" placeholder="Fined Attendance (%)" required [formControl]="fined" name="fined_attendance">
    <mat-error *ngIf="fined.invalid">This value must be less than allowed attendance</mat-error>
</mat-form-field>
<mat-form-field>
    <input matInput required [formControl]="ldo_form_fill_up" placeholder="Applicable Until" [matDatepicker]="picker" name="ldo_form_fill_up">
    <mat-datepicker-toggle #toggle matSuffix [for]="picker"></mat-datepicker-toggle>
    <mat-datepicker #picker></mat-datepicker>
    <mat-error *ngIf="ldo_form_fill_up.invalid">A valid date is required</mat-error>
</mat-form-field>
<mat-form-field *ngIf="ldo_form_fill_up.valid">
    <input matInput required [formControl]="ldo_payment" placeholder="Payable Until" [matDatepicker]="picker2" name="ldo_payment">
    <mat-datepicker-toggle #toggle2 matSuffix [for]="picker2"></mat-datepicker-toggle>
    <mat-datepicker #picker2></mat-datepicker>
    <mat-error *ngIf="ldo_payment.invalid">This date must be greater than the above</mat-error>
</mat-form-field>

Here are the getters form controls:

get ldo_form_fill_up() {
  return this.form.get('ldo_form_fill_up');
}

get ldo_payment() {
  return this.form.get('ldo_payment');
}

get allowed() {
  return this.form.get('allowed_attendance');
}

get fined() {
  return this.form.get('fined_attendance');
}

Here is the form builder:

this.form = fb.group({
  allowed_attendance: fb.control(70, [Validators.required, Validators.min(0), Validators.max(100)]),
  fined_attendance: fb.control(60, [Validators.required, Validators.min(0), Validators.max(100)]),
  ldo_form_fill_up: fb.control('', Validators.required),
  ldo_payment: fb.control('', Validators.required),
}, {
  validators: customRangeValidator
});

Here is the custom validator:

export function customRangeValidator(group: FormGroup) {
  const date1 = group.controls.ldo_form_fill_up;
  const date2 = group.controls.ldo_payment;
  const attd1 = group.controls.allowed_attendance;
  const attd2 = group.controls.fined_attendance;
  if (date1.value >= date2.value) {
    date2.setErrors({
      customRangeValidator: true
    });
  }
  if (attd2.value >= attd1.value) {
    attd2.setErrors({
      customRangeValidator: true
    });
  }
  return null;
}

What I need: fined_attendance < allowed_attendance and ldo_payment > ldo_form_fill_up

What is going on:

  1. Input 70 in allowed_attendance
  2. Input 60 in fined_attendance
  3. No error for fined_attendance (it is OK).
  4. Change allowed_attendance to 50.
  5. Error is shown for fined_attendance (it is also OK).
  6. Change allowed_attendance to 70 again.
  7. Error is still shown. (it is NOT OK).
  8. But clear and re-input 60 in fined_attendance, Error gone!

So, I am facing trouble shown at step 7. What wrong am I doing?


Solution

  • I think you're only caring about setting the errors and not clearing them.

    In the else conditions you should also clear them. Here's how:

    import { Component } from '@angular/core';
    import { FormBuilder, Validators, FormGroup } from '@angular/forms';
    
    /** @title Simple form field */
    @Component({
      selector: 'form-field-overview-example',
      templateUrl: 'form-field-overview-example.html',
      styleUrls: ['form-field-overview-example.css'],
    })
    export class FormFieldOverviewExample {
    
      form: FormGroup;
    
      constructor(private fb: FormBuilder) { }
    
      ngOnInit() {
        this.form = this.fb.group({
          allowed_attendance: this.fb.control(70, [Validators.required, Validators.min(0), Validators.max(100)]),
          fined_attendance: this.fb.control(60, [Validators.required, Validators.min(0), Validators.max(100)]),
          ldo_form_fill_up: this.fb.control('', Validators.required),
          ldo_payment: this.fb.control('', Validators.required),
        }, { validators: customRangeValidator });
      }
    
      submit() {
        console.log(this.form.value);
      }
    
      get ldo_form_fill_up() {
        return this.form.get('ldo_form_fill_up');
      }
    
      get ldo_payment() {
        return this.form.get('ldo_payment');
      }
    
      get allowed() {
        return this.form.get('allowed_attendance');
      }
    
      get fined() {
        return this.form.get('fined_attendance');
      }
    
    }
    
    export function customRangeValidator(group: FormGroup) {
      console.log('Called');
      const { ldo_form_fill_up, ldo_payment, allowed_attendance, fined_attendance } = group.controls;
      if (fined_attendance.value >= allowed_attendance.value) {
        fined_attendance.setErrors({ customRangeValidator: true });
      } else {
        fined_attendance.setErrors(null);
      }
      if (ldo_form_fill_up.value >= ldo_payment.value) {
        ldo_payment.setErrors({ customRangeValidator: true });
      } else {
        ldo_payment.setErrors(null);
      }
      return null;
    }
    

    Also, in your Template, instead of using [formGroup] why don't you simply use formGroupName. Something like this:

    <form [formGroup]="form">
      <mat-form-field>
        <input matInput type="number" min="0" max="100" step="1" placeholder="Allowed Attendance (%)" required formControlName="allowed_attendance" name="allowed_attendance">
        <mat-error *ngIf="allowed.invalid">Value must be 0-100</mat-error>
      </mat-form-field>
      <mat-form-field>
        <input matInput type="number" min="0" max="100" step="1" placeholder="Fined Attendance (%)" required formControlName="fined_attendance" name="fined_attendance">
        <mat-error *ngIf="fined.invalid">This value must be less than allowed attendance</mat-error>
      </mat-form-field>
      <mat-form-field>
        <input matInput required formControlName="ldo_form_fill_up" placeholder="Applicable Until" [matDatepicker]="picker" name="ldo_form_fill_up">
        <mat-datepicker-toggle #toggle matSuffix [for]="picker"></mat-datepicker-toggle>
        <mat-datepicker #picker></mat-datepicker>
        <mat-error *ngIf="ldo_form_fill_up.invalid">A valid date is required</mat-error>
      </mat-form-field>
      <mat-form-field *ngIf="ldo_form_fill_up.valid">
        <input matInput required formControlName="ldo_payment" placeholder="Payable Until" [matDatepicker]="picker2" name="ldo_payment">
        <mat-datepicker-toggle #toggle2 matSuffix [for]="picker2"></mat-datepicker-toggle>
        <mat-datepicker #picker2></mat-datepicker>
        <mat-error *ngIf="ldo_payment.invalid">This date must be greater than the above</mat-error>
      </mat-form-field>
    
      <br>
    
      <button 
        [disabled]="form.invalid"
        mat-raised-button 
        color="primary"
        (click)="submit()">
        Primary
      </button>
    
    </form>
    

    Here's a Working Sample StackBlitz for your ref.