Search code examples
angulartypescriptangular-reactive-formsangular-forms

Why is an Angular form VALID with no mat-error when it has formGroup.errors?


I'm using reactive forms and a custom validator in Angular 11. I want to flag an error at the form level when the user selects an EndDate that is earlier than the StartDate. I'll use this form-level error to display error text on the template as well as to disable the button to submit the form.

There seem to be two issues here that I can't figure out:

1. When I set an error via the customValidator on the form, the form.status remains VALID even when there are form.errors. Why is this?

2. In the template, checking the form's errors doesn't work. I have a mat-error with form.hasErrors('endDateTooSoon'), and yet this error never displays. What am I missing here? Something odd.

This is the form instantiation.

  shiftEditorForm = new FormGroup({
    shiftTitle: new FormControl('', Validators.required),
    shiftStartDate: new FormControl('', Validators.required),
    shiftStartTime: new FormControl('', Validators.required),
    shiftEndDate: new FormControl('', Validators.required),
    shiftEndTime: new FormControl('', Validators.required),
  }, DateValidators.compareDates('shiftStartDate', 'shiftEndDate', { endDateTooSoon: true})
 );

This is the validator:

  static compareDates = (
    dateField1: string,
    dateField2: string,
    validatorField: { [key: string]: boolean }): ValidatorFn => {
      return (control: AbstractControl): { [key: string]: boolean } | null => {
      const date1 = control.get(dateField1)?.value;
      const date2 = control.get(dateField2)?.value;
      if (date1 && date2 && date1 > date2) {
        return validatorField;
      }
      return null;
    };
  }

This is what the form looks like when the validator works:

enter image description here

(Why is it VALID when an error has been correctly added? )

This is the template block defining the shiftStartDate formControl:

<mat-form-field class="date-selection"
    appearance="outline">
  <input matInput
    [matDatepicker]="startDate"
    formControlName="shiftStartDate">
  <mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
  <mat-datepicker #startDate></mat-datepicker>
  <mat-error *ngIf="shiftStartDate?.touched && !shiftStartDate?.value">
    Shift start date is required.
  </mat-error>
  <mat-error *ngIf="shiftEditorForm?.hasError('endDateTooSoon')">
    End date must be after start date.
  </mat-error>
</mat-form-field>

I'm checking with *ngIf="shiftEditorForm.hasError() and yet it never displays the error.

There's another similar block of the template for the shiftEndDate formControl.

And yet here as I manipulate the datepickers, and set the shiftEndDate to be before the shiftStartDate, nothing happens and the error doesn't display:

No errors appearing

Update: An added wrinkle I found after posting this question. The button to submit the form IS disabled/enabled based on validity of the date check, with the following attribute on the button: [disabled]="this.shiftEditorForm.invalid". Real head-scratcher here: the formGroup shows status as VALID but is returning true for this invalid boolean check.


Solution

  • Problems

    I think I have been able to reproduce your issue in this stackblitz Demo

    I have noticed two things,

    1. Your have not considered time in your validation
    2. As stated in your question, validation errors are not showing...

    The Second error is what we will try to look at

    Problem Analysis

    To understand the problem I will state that your code works totally a expected...

    Below should explain it all

    Add below code under the <mat-error></mat-error> tag

    <span *ngIf="shiftEditorForm?.hasError('endDateTooSoon')">
        End date must be after start date.
    </span>
    

    You will notice that this is being shown without any issues when the date error is thrown

    Simply, for mat-error to show, then the input associated with that <mat-error> must have an error. From the form we notice that the shiftStartDate becomes valid once a user enters a value so <mat-error> will not be shown, totally as expected

    Solution to your problem

    If you need to show the mat-error then you will need to set the control to invalid e.g with

     control.get(dateField1)?.setErrors({higherThanStart: true}) 
    

    Your validatorFn will be something like

    class DateValidators {
        static compareDates = (
        dateField1: string,
        dateField2: string,
        validatorField: { [key: string]: boolean }): ValidatorFn => {
          return (control: AbstractControl): { [key: string]: boolean } | null => {
          const date1 = control.get(dateField1)?.value;
          const date2 = control.get(dateField2)?.value;
          if (date1 && date2 && date1 > date2) {
             control.get(dateField1)?.setErrors({higherThanStart: true}) 
            return validatorField;
          }
          
          control.get(dateField1)?.setErrors({higherThanStart: null})
          return null;
        };
      }
    }
    

    Now the Error will show... See Demo Here