Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 5, Angular Material: Datepicker validation not working

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
like image 957
danwellman Avatar asked Jan 16 '18 14:01

danwellman


2 Answers

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

like image 68
Manon Ingrassia Avatar answered Sep 23 '22 18:09

Manon Ingrassia


I managed to get this working without using the ErrorStateMatcher, although that did help me reach the solution. Leaving here for future reference or to help others.

I converted my form to a reactive form instead of a template-driven form, and I changed the custom validator directive to a simpler validator (non-directive-based).

Here is the working code:

my-form.component.html:

<div class="container" fxlayoutgap="16px" fxlayout fxlayout.xs="column" fxlayout.sm="column" *ngIf="fieldset.controls[control].type === 'datepicker'">
  <mat-form-field class="full-width" fxflex>
    <input matInput 
           [formControlName]="control"
           [matDatepicker]="dob"
           [placeholder]="fieldset.controls[control].label" 
           [max]="fieldset.controls[control].validation.max">
    <mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>
    <mat-datepicker #dob></mat-datepicker>
    <mat-error *ngIf="myForm.get(control).hasError('required')">
      {{fieldset.controls[control].validationMessages.required}}</mat-error>
    <mat-error *ngIf="myForm.get(control).hasError('underEighteen')">
      {{fieldset.controls[control].validationMessages.underEighteen}}
    </mat-error>
  </mat-form-field>
</div>

note: The above code is inside a couple of nested ngFor loops which define the value of fieldset and control. In this example control maps to the string dob.

over-eighteen.validator.ts:

import { ValidatorFn, AbstractControl } from '@angular/forms';
import * as moment from 'moment';

export function overEighteen(): ValidatorFn {
  return (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 {
        underEighteen: {
          'requiredAge': '18+',
          'currentAge': delta
        }
      };
    }

    return null;
  };
}

my-form.component.ts:

buildForm(): void {
  const formObject = {};

  this.myService.request.fieldsets.forEach((controlsGroup, index) => {

    this.fieldsets.push({
      controlNames: Object.keys(controlsGroup.controls)
    });

    for (const control in controlsGroup.controls) {
      if (controlsGroup.controls.hasOwnProperty(control)) {
        const controlData = controlsGroup.controls[control];
        const controlAttributes = [controlData.value];
        const validators = [];

        if (controlData.validation) {
          for (const validator in controlData.validation) {
            if (controlData.validation.hasOwnProperty(validator)) {
              if (validator === 'overEighteenValidator') {
                validators.push(this.overEighteenValidator);
              } else {
                validators.push(Validators[validator]);
              }
            }
          }
          controlAttributes.push(Validators.compose(validators));
        }

        formObject[control] = controlAttributes;
      }
    }
  });

  this.myForm = this.fb.group(formObject);
}
like image 44
danwellman Avatar answered Sep 26 '22 18:09

danwellman