Issue with Datepicker validation in Angular 5 and Angular Material

I have implemented the most recent version of Angular and Angular Material. I am facing an issue with a datepicker where the validation requirements are not being met as expected. The documentation states that the required attribute should work by default, but it doesn't seem to handle errors in the same way as other form elements.

Here is the code snippet I'm using:

<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>

The validation works when a date is selected, and it gets stored correctly in the property within myService.

However, the validation behavior does not align with my expectations. Even though the input field turns red when left empty, the usual error handling for validation errors does not kick in. This results in the error message not displaying, unlike with other input fields on the page:

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

The *ngIf condition is never met because the datepicker fails to update dob.errors, hence preventing the error message from showing despite the input being styled as invalid.

Does this sound right? Am I overlooking something?

I attempted to implement a custom directive to validate if the selected date indicates the user is over 18 years old:

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 scenario, I tried to link a similar matError to dob.errors.validateAdult to display the error when necessary.

An intriguing aspect is that if I choose a date less than 18 years ago, the entire input, label, etc., receives the default red error styling, indicating some level of progress. However, the error message remains elusive.

Any help or suggestions would be greatly appreciated!

Specifications:

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

Answer №1

Implementing ErrorStateMatcher in Angular Material Forms has been a game-changer for me.

Here is an example of how your code should look:

<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>

In the typescript file:

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();
}

For more information, you can check out this link: https://material.angular.io/components/input/overview

Answer №2

I was able to make this work without resorting to the ErrorStateMatcher, although it did assist me in finding the solution. I'm sharing my approach here for future reference or to support others.

I decided to convert my form from a template-driven one to a reactive form, and I simplified the custom validator directive to a more straightforward validator that is not directive-based.

Below is the code that is now functional:

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 code snippet above is within a couple of nested ngFor loops defining the values of fieldset and control. In this scenario, control corresponds 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);
}

Answer №3

Your #dob variable is duplicated, which could result in unexpected behavior in Angular validation.

Currently, you have

<input #dob='ngModel'

and

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

Please correct the naming convention to avoid any issues.

Answer №4

Make sure to include the following code snippet in your view page:


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

Don't forget to import MatDatepickerModule and MatNativeDateModule into your module as well.

Answer №5

To modify the input reference name, follow the example below. Note that the input element #dobInput is only referenced 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 referred to by #dbo

[matDatepicker]="dob"

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

Answer №6

Have you attempted to assign the *ngIf to your customized validator in this way:

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

If that solution proves effective, you could develop another validator to imitate the mandatory native validation functionality.

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;
  }
}

Subsequently, apply the previous ngIf as follows:

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

While I have not executed a trial, theoretically, it should operate correctly.

Answer №7

Check out this link, and this one. Also, take a look at a functional example using javascript here.


You can also try 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();
  });

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

"What is the best approach for setting up an Azure Web App to host both an Express static site and API

Setting up an Express app was a breeze for me, but when it comes to deploying it on Azure Web App, I'm hitting some roadblocks! The structure of my app is quite simple: a static web app with its own API. Requests to /website.com/api are forwarded to ...

Having trouble with Node.js executing commands in the console

I've been following some tutorials on YouTube to learn how to create a real-time chat using Node.js from the phpacademy channel. Currently, I'm stuck at the step where I need to run my server.js file in the console. When I enter the command ...

Exploring ways to query a mapped array in React Native

Struggling to search a mapped list in react native? While using a Flatlist would be easier, this task is currently causing me major frustration. If anyone has any insights or solutions, please share them! Here's a snippet of the code: import React ...

Angular - Eliminate module during build process

In my current project, there is a module called data-generator that is only used during development mode (when running `npm start`). When building the application (using `npm run build`), this module is not needed. Is there a way to exclude it from the pro ...

Unraveling the Distinctions: Identifying an Admin vs. a User within the API

When working with the express framework, I encounter a situation where I need to restrict access to certain API endpoints. For example, I have this line in the API: router.delete('/user',(req, res) => { //deleting...} Now, I want only an Adm ...

Setting up the karma ng-html2js preprocessor to locate my templates within a specific folder

Currently, I am facing an issue where I need to set the templateUrl: "partials/my-directive.html" However, I find that I have to use templateUrl: "app/partials/my-directive.html for it to be loaded by Karma. This is how my folder structure looks like (fo ...

What is the best way to enhance a React Native component's properties with Flow?

I'm currently working with Flow version 0.54.1 in my React Native project, specifically using version 0.48.3. I have developed a component named PrimaryButton that acts as a wrapper for the native TouchableOpacity component. My goal is to define custo ...

Utilizing key values to access an array and generate a list of items in React.js

This marks my initiation on Stack Overflow, and I extend an apology in advance for any lack of clarity in my explanation due to unfamiliarity with the platform. My current task involves creating a resume with a dynamic worklist feature on my website. The ...

The PHP sorted array loses its order when encoded into JSON and then sorted in JavaScript

In my PHP code, I have two arrays that I need to work with. The first array is sorted using the arsort() function like this: arsort($array1); After sorting, I output the contents of both arrays like so: foreach ($array1 as $key => $val) { $output ...

The module "angular2-multiselect-dropdown" is experiencing a metadata version mismatch error

Recently, I updated the node module angular2-multiselect-dropdown from version v3.2.1 to v4.0.0. However, when running the angular build command, I encountered an "ERROR in Metadata version mismatch for module". Just to provide some context, I am using yar ...

Implementing the expand and collapse functionality to the Discovery sidebar on the DSpace 4.2 xmlui platform

I recently began using DSpace and I am attempting to implement an expand/collapse feature in the Discovery sidebar of DSpace 4.2 xmlui using the Mirage theme. After finding some helpful jquery code, I attempted to add this functionality by placing the js f ...

Modifying an object's label based on a particular value using JavaScript

In my current project involving React.js charts, I am looking to organize data by month. In Django, I have set up a view to display JSON containing the total events per month in the following format: [ { "month": "2022-06-01T00:00:0 ...

Trouble with executing asynchronous AJAX request using when() and then() functions

Here is the code that I am currently using: function accessControl(userId) { return $.ajax({ url: "userwidgets", type: "get", dataType: 'json', data: { userid: userId } }); }; var user ...

Learn how to securely transmit data using basic authentication in Node.js with the node-fetch module

Having trouble with this code. I am facing an issue where a "post" request with basic authentication returns a 200 status, but no response data is received. Surprisingly, when the same code is executed without auth credentials, it works perfectly fine. l ...

Error due to PlatformLocation's location dependency issue

My AppComponent relies on Location (from angular2/router) as a dependency. Within the AppComponent, I am using Location.path(). However, when running my Jasmine test, I encountered an error. Can you help me identify the issue with my Jasmine test and guide ...

The execution of a function within context is not triggered

I have developed a decentralized application (dApp) that utilizes a context folder to interact with a smart contract. Within the context, there is a function named loadAuth which verifies if the user is authenticated and then assigns the account state to u ...

Uninstalling Puppeteer from npm can be done by running a

Some time ago, I had integrated Puppeteer into an Express API on Heroku using their Git CLI. Recently, I decided to remove Puppeteer from the package.json file and went through the npm install process before trying to push to GitHub. However, it appears th ...

Exploring Bootstrap4: Interactive Checkboxes and Labels

My form design relies on Bootstrap, featuring a checkbox and an associated label. I aim to change the checkbox value by clicking on it and allow the label text to be edited by clicking on the label. The issue I'm facing is that both elements trigger t ...

Issue with the react-native-autocomplete-input where the list does not close after selecting an option

After selecting an item from the list in my react-native-autocomplete-input, the list does not close. let Location = (props) => ( <View style={styles.formItem}> <Autocomplete data={props.autocompleteResults.predictions} de ...

"Improve your Angular ngrx workflow by utilizing the sandbox pattern to steer clear of

Currently, I'm trying to determine whether my implementation of the ngrx and sandbox pattern is effective. Here's the issue I'm facing: getFiles(userId: number, companyId: number) { this.fileService.getFiles(userId, companyId).subscribe(re ...