14

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
danwellman
  • 9,068
  • 8
  • 60
  • 88
  • maybe try to use ErrorStateMatcher to make errors appear instantly. https://material.angular.io/components/input/overview – Shaniqwa Jan 25 '18 at 13:26
  • Could you add details and console log? or error screen as well. – Cruzer Jan 25 '18 at 14:27
  • @vbRocks there is no error screen or console errors, the page "works", and even the field the picker is attached to gets red error styling when picking a date less than 18 years ago, it's just that the `errors` object for the control does not get updated – danwellman Jan 25 '18 at 15:27
  • Maybe is a typo but the is quote left after model reference... at [(ngModel)]="myService.request.dob #dob="ngModel" – Leonardo Neninger Jan 25 '18 at 15:43
  • Well spotted @LeonardoNeninger! But alas, was just a typo here on SO, but thank you in any case! – danwellman Jan 25 '18 at 15:48

7 Answers7

8

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

Manon Ingrassia
  • 270
  • 3
  • 14
  • 1
    Thank you, I will try this, although it may take some time as I will need to switch to reactive-forms instead of template-form – danwellman Jan 25 '18 at 15:31
6

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);
}
danwellman
  • 9,068
  • 8
  • 60
  • 88
1

You have the #dob duplicated. That can have a undesired behavior in angular validation.

You have

<input #dob='ngModel'

and

<mat-datepicker #dob></mat-datepicker>

Please fix the naming convention and see what happen.

  • Thanks, it seemed this was needed to tie the control to the value in my model. Without this, the error message was still not showing, and also my model was not being updated when a date was picked. But I will try it without – danwellman Jan 25 '18 at 15:30
1

You can change name of the input reference like below.. Notice that input element #dobInput is referenced only in mat-error.

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

The picker is referenced by #dbo

[matDatepicker]="dob"

<mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>
1

Add this in your view page:

      <mat-form-field>
     <input matInput [matDatepicker]="dp"   placeholder="Employement date" >
   <mat-datepicker-toggle matSuffix [for]="dp"></mat-datepicker-toggle>
   <mat-datepicker #dp></mat-datepicker>
    </mat-form-field>

Just import MatDatepickerModule,MatNativeDateModule in your module

Guvaliour
  • 397
  • 1
  • 5
  • 17
0

Did you try to set the *ngIf to your custom validator like below:

 <mat-error *ngIf="dob.errors && dob.errors.validateAdult">Your date of birth 
 is less than 18 ?</mat-error>

If that works, you can create another validator to simulate the required native validation behavior.

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

  validate(control: AbstractControl): { [key: string]: any } {
    let value = control.value;
    if (!value || value == null || value.toString().length == 0) {
        return requireValidator: {
      'requiredAge': 'This field is required',
    }
    }

    return null;
  }
}

And then use the previous ngIf as below:

 <mat-error *ngIf="dob.errors && dob.errors.requireValidator">Your date of 
birth is less than 18 ?</mat-error>

I did not test it but logically it should work.

Nour
  • 5,609
  • 3
  • 21
  • 24
  • Thanks. Yes, I did set `*ngIf` on my `mat-error` to `dob.errors.validateAdult` - this is not the problem. With breakpoints I can see that in cases where a date less than 18 years is selected, the custom directive is working correctly, it is just not updating `errors` for some reason. I think it is issue with the datepicker, because with regular `matInput` elements, built-in and custom validation is working as expected – danwellman Jan 23 '18 at 14:26
  • Actually i am using my own directive to tell the input to check the validation on blur, but if you are using angular 5, the update on blur event is built in now. so maybe the problem is the controls did not get change detection to update their error. please follow this answer here https://stackoverflow.com/questions/33866824/angular2-formcontrol-validation-on-blur hope it will solve your problem :). – Nour Jan 23 '18 at 14:34
  • Thanks again, I did think of this before posting the question and made use of the `ChangeDetectorRef` to trigger manual change detection, but that didn't work either. I'm convinced this is a problem with the datepicker itself – danwellman Jan 24 '18 at 08:44
-3

Take a look on this, and this. And please see a working javascript based example here.


You may also test this (angular based) datepicker:

HTML:

<div ng-app="example" ng-controller="AppCtrl">
  <md-datepicker ng-model="myDate" md-placeholder="Enter date"></md-datepicker>
</div>

JS:

angular
  .module('example', ['ngMaterial'])
  .config(function($mdDateLocaleProvider) {
    $mdDateLocaleProvider.formatDate = function(date) {
      return moment(date).format('DD/MM/YYYY');
    };

    $mdDateLocaleProvider.parseDate = function(dateString) {
      var m = moment(dateString, 'DD/MM/YYYY', true);
      return m.isValid() ? m.toDate() : new Date(NaN);
    };
  })
  .controller('AppCtrl', function($scope) {
    $scope.myDate = new Date();
  });
A. STEFANI
  • 6,707
  • 1
  • 23
  • 48
  • Thanks, but none of the links are relevant. I don't want to use a different date-picker, I don't want to use Bootstrap or jQuery. I'm also not using AngularJS. The question isn't how to do age-based validation, the question is how to do validation *with the current material datepicker* – danwellman Jan 22 '18 at 10:01