Javascript – Angular 5, Angular Material: Datepicker validation not working

angularangular-material2javascripttypescriptvalidation

I'm using the latest Angular and latest Angular Material. I've got a datepicker and I want to add some validation. Documents say that the required attribute should work out of the box, but it doesn't seem to handle errors in the same way that other form elements do.

Here is my mark-up:

<mat-form-field class="full-width">
    <input matInput [matDatepicker]="dob" placeholder="Date of birth" [(ngModel)]="myService.request.dob" #dob="ngModel" required app-validateAdult>
    <mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>
    <mat-datepicker #dob></mat-datepicker>
    <mat-error *ngIf="dob.errors && dob.errors.required">Your date of birth is required</mat-error>
</mat-form-field>

This works on the happy-path, so when a date is picked, the date ends up in the expected property in myService.

The validation does not work in the way that I would expect however; in this case, if I click into the field and then out of the field without entering a date, then the input does get red styling, but the usual [controlName].errors object does not get populated. This means that showing an error message in the usual way (the way that works with other inputs that are not date pickers on the same page) does not work:

<mat-error *ngIf="dob.errors && dob.errors.required">Your date of birth is required</mat-error>

The *ngIf is never true because the datepicker never seems to update dob.errors, so the error message is never shown, even when the input is styled as invalid.

Is this right? Have I missed something?

I've also tried adding a custom directive to validate that the date selected with the datepicker indicates that the user is over 18:

export class AdultValidator implements Validator {
  constructor(
    @Attribute('app-validateAdult') public validateAdult: string
  ) { }

  validate(control: AbstractControl): { [key: string]: any } {
    const dob = control.value;
    const today = moment().startOf('day');
    const delta = today.diff(dob, 'years', false);

    if (delta <= 18) {
      return {
        validateAdult: {
          'requiredAge': '18+',
          'currentAge': delta
        }
      };
    }

    return null;
  }
}

In this case I'm trying to use a similar matError (except linked to dob.errors.validateAdult instead) to show the error when appropriate.

The interesting thing with this is that if I pick a date less than 18 years ago, the whole input, label, etc, gets the default red error styling, so something is happening, but I still don't see my error message.

Any suggestions would be much appreciated!

Exact versions:

Angular CLI: 1.6.3
Node: 6.11.0
OS: win32 x64
Angular: 5.1.3
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

@angular/cdk: 5.0.4
@angular/cli: 1.6.3
@angular/flex-layout: 2.0.0-beta.12
@angular/material-moment-adapter: 5.0.4
@angular/material: 5.0.4
@angular-devkit/build-optimizer: 0.0.36
@angular-devkit/core: 0.0.22
@angular-devkit/schematics: 0.0.42
@ngtools/json-schema: 1.1.0
@ngtools/webpack: 1.9.3
@schematics/angular: 0.1.11
@schematics/schematics: 0.0.11
typescript: 2.4.2
webpack: 3.10.0

Best Answer

I use ErrorStateMatcher in my Angular Material Forms, it works perfectly.

You should have a code that looks like that:

<mat-form-field class="full-width">
    <input matInput [matDatepicker]="dob" placeholder="Date of birth" formControlName="dob" required app-validateAdult>
    <mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>
    <mat-datepicker #dob></mat-datepicker>
    <mat-error *ngIf="dob.hasError('required')">Your date of birth is required</mat-error>
</mat-form-field>

And typescript:

import { ErrorStateMatcher } from '@angular/material/core';

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


export class AdultValidator implements Validator {
  dob = new FormControl('', [
    Validators.required
  ]);

  matcher = new MyErrorStateMatcher();
}

You can see here more about it: https://material.angular.io/components/input/overview