Angular Custom Cross Field validation on the Form Group not working as expected for date field

1.3k views Asked by At

I have a form with a Start Date and End Date. The Start date should be from the next day and End Date should be at least 7 days from Start. I am using Angular material datepicker and setting the minDate values for both these fields. I have a custom validator at the FormGroup level that validates the 7 days logic and displays an error message for the End Date field if validation fails.

The validation works fine and an error message is displayed if I manually type in an older date for the End Date. But if I change the Start date to a future date and then select an End date so that the validation fails, the error message is not getting displayed. Basically the Error message only gets displayed if the date is older than the minDate value that was originally set. For any validation failures where the End Date is greater than the minDate, the error is not getting displayed.

My HTML Code -

<form [formGroup]="dateForm" >
  <mat-form-field color="green" appearance="fill" [style.width.px]=300>
    <mat-label>Start Date</mat-label>
    <input matInput [matDatepicker]="picker2" [min]="minDateStart" [max]="maxDate" formControlName="startDate" on/>
    <mat-error>
      <p *ngIf="dateForm.controls.startDate.dirty && dateForm.controls.startDate.errors?.required" class="alert alert-danger">This is a required field</p>
      <p *ngIf="dateForm.controls.startDate.dirty && dateForm.controls.startDate.errors?.invalidStartDate" class="alert alert-danger">
          {{ dateForm.controls.startDate.errors?.invalidStartDate.message}} </p>
    </mat-error>
    <mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
    <mat-datepicker #picker2 color="primary"></mat-datepicker>
  </mat-form-field>
  &nbsp;
  <mat-form-field color="green" appearance="fill" [style.width.px]=300>
    <mat-label>End Date</mat-label>
    <input matInput [matDatepicker]="picker3" [min]="minDateEnd" [max]="maxDate" formControlName="endDate"/>
    <mat-error>
      <p *ngIf="dateForm.controls.endDate.touched && dateForm.controls.endDate.errors?.required" class="alert alert-danger">This is a required field</p>
      <p *ngIf="dateForm.errors?.invalidEndDate" class="alert alert-danger">
        {{ dateForm.errors?.invalidEndDate.message}} </p>
    </mat-error>
    <mat-datepicker-toggle matSuffix [for]="picker3"></mat-datepicker-toggle>
    <mat-datepicker #picker3></mat-datepicker>
  </mat-form-field>
</form>

My TS file -

const DATE_OFFSET_START = 1;
const DATE_OFFSET_END = 7;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'Date Picker Validation Issue';

  minDateStart: Date;
  minDateEnd: Date;
  maxDate: Date;
  dateForm: FormGroup

  constructor(
    private formBuilder: FormBuilder) {
  }

  ngOnInit() {
    const currentDate = new Date();
    const currentYear = currentDate.getFullYear();
    const currentDay = currentDate.getUTCDate();
    const currentMonth = currentDate.getUTCMonth();

    this.minDateEnd = new Date(currentYear, currentMonth, currentDay + DATE_OFFSET_START + DATE_OFFSET_END);
    this.minDateStart = new Date(currentYear, currentMonth, currentDay + DATE_OFFSET_START);
    this.maxDate = new Date(currentYear + 999, currentMonth, currentDay);

    this.dateForm = this.formBuilder.group({
      startDate: [this.minDateStart, [Validators.required, validateStartDate]],
      endDate: [this.minDateEnd, [Validators.required]],
    }, { validators: validateEndDate('startDate', 'endDate') });
  }

}

// Validates the Form Group dates values.
// Returns error message if difference is less than specified limit.
export function validateEndDate(startDateFieldName: string, endDateFieldName: string) {
  return (fg: FormGroup) => {
    if (fg.get(endDateFieldName).value === undefined || fg.get(endDateFieldName).value == null)
      return null;
    let startDate = new Date(fg.get(startDateFieldName).value);
    let endDate = new Date(fg.get(endDateFieldName).value);

    return dateDifference(startDate, endDate) >= DATE_OFFSET_END ? null : {
      invalidEndDate: {
        message: "End Date should be atleast " + DATE_OFFSET_END + " days more than Start date!"
      }
    };
  }
}

validateEndDate is the Custom validator that validates the start and End Date { validators: validateEndDate('startDate', 'endDate') });

And this is the code that displays the error -

      <p *ngIf="dateForm.controls.endDate.touched && dateForm.controls.endDate.errors?.required" class="alert alert-danger">This is a required field</p>
      <p *ngIf="dateForm.errors?.invalidEndDate" class="alert alert-danger">
        {{ dateForm.errors?.invalidEndDate.message}} </p>
    </mat-error>

I am not sure why the error is displayed only when the date is less than minDate value, and not for all validation errors.

1

There are 1 answers

2
HarishVijayamohan On BEST ANSWER

I faced this issue not because of my custom date validator but the problem was with angular material forms. I see that you havent set errorStateMatcher property in <input> tag. When I used this property, everything worked fine. When you want to check if a parent form group is invalid, you can use the errorStateMatcher property of the matInput as described in this doc.

For example:

dueDate field with errorStateMatcher in HTML:

<mat-form-field appearance="fill" class="full-width">
  <mat-label>Due Date</mat-label>
  <input matInput [matDatepicker]="picker"
         formControlName="dueDate"
         id="dueDate"
         [min]="minDate"
         [errorStateMatcher]="matcher">
  <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
  <mat-datepicker #picker touchUi></mat-datepicker>
  <mat-error *ngIf="createTaskForm.errors?.dueDateGtEqRepeatUntil">
    DueDate should not be greater than or equal to Repeat Until Date.
  </mat-error>
  <mat-error *ngIf="createTaskForm.controls?.dueDate.hasError('required')">
    Due date is required.
  </mat-error>
</mat-form-field>

in TS file:

export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const invalidCtrl = control && control.invalid;
    const invalidParent = control && control.parent && control.parent.invalid;
    return (invalidCtrl || invalidParent) && (control.dirty || control.touched);
  }
}

I hope this would fix your issue. For other details on cross validation between two date fields , this video might be useful.