What reasons could there be for an Angular form being considered VALID despite not displaying any mat-error messages, even when it contains

Currently in Angular 11, I am utilizing reactive forms along with a custom validator. My goal is to trigger an error at the form level whenever a user chooses an EndDate that precedes the selected StartDate. This form-level error will serve two purposes: displaying error text on the template and disabling the form submission button.

There are a couple of issues that I am facing:

1. Despite setting an error using the customValidator on the form, the form.status remains as VALID, even when there are form.errors. Why is this happening?

2. When checking for the form's errors in the template, it doesn't seem to work properly. I have a mat-error element with form.hasErrors('endDateTooSoon'), but this error message never appears. What could be causing this unexpected behavior?

This is how the form is initialized:

  shiftEditorForm = new FormGroup({
    shiftTitle: new FormControl('', Validators.required),
    shiftStartDate: new FormControl('', Validators.required),
    shiftStartTime: new FormControl('', Validators.required),
    shiftEndDate: new FormControl('', Validators.required),
    shiftEndTime: new FormControl('', Validators.required),
  }, DateValidators.compareDates('shiftStartDate', 'shiftEndDate', { endDateTooSoon: true})
 );

The following code snippet represents the validator being used:

  static compareDates = (
    dateField1: string,
    dateField2: string,
    validatorField: { [key: string]: boolean }): ValidatorFn => {
      return (control: AbstractControl): { [key: string]: boolean } | null => {
      const date1 = control.get(dateField1)?.value;
      const date2 = control.get(dateField2)?.value;
      if (date1 && date2 && date1 > date2) {
        return validatorField;
      }
      return null;
    };
  }

When the validator is functioning correctly, the form should look like this:

https://i.sstatic.net/wiypQ.png

I'm encountering issues with the template block defining the shiftStartDate formControl:

<mat-form-field class="date-selection"
    appearance="outline">
  <input matInput
    [matDatepicker]="startDate"
    formControlName="shiftStartDate">
  <mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
  <mat-datepicker #startDate></mat-datepicker>
  <mat-error *ngIf="shiftStartDate?.touched && !shiftStartDate?.value">
    Shift start date is required.
  </mat-error>
  <mat-error *ngIf="shiftEditorForm?.hasError('endDateTooSoon')">
    End date must be after start date.
  </mat-error>
</mat-form-field>

Even though I am checking with

*ngIf="shiftEditorForm.hasError()
, the error is not being displayed as intended.

A similar template block exists for the shiftEndDate formControl as well, but unfortunately, the error does not display correctly when the shiftEndDate is set before the shiftStartDate:

https://i.sstatic.net/6cSTt.png

Update: Upon further investigation, I discovered another issue: while the formGroup indicates a status of VALID, the submit button's state (disabled attribute) is indeed being toggled based on the validity check related to the dates. The

[disabled]="this.shiftEditorForm.invalid"
attribute on the button seems to work despite the contradictory status of the form. Quite perplexing!

Answer №1

Issues

After reviewing your situation, it seems like I have successfully replicated the problem in this stackblitz Demo

There are a couple of observations:

  1. Your validation lacks consideration for time
  2. The validation errors you mentioned are not appearing as expected...

We will focus on addressing the second issue

Analyzing the Problem

Upon examining your code, it behaves as intended without any issues...

Further details are provided below:

Add the following code beneath the

<mat-error></mat-error>
tag

<span *ngIf="shiftEditorForm?.hasError('endDateTooSoon')">
    End date must be after start date.
</span>

You will notice that this message displays correctly when the date error occurs

Basically, for the mat-error to appear, the associated input within the <mat-error> must contain an error. As seen from the form, the shiftStartDate becomes valid once user input is provided, hence the <mat-error> remains hidden, which aligns with expectations

Resolving the Issue

If you wish to display the mat-error, you should set the control to invalid using something like

 control.get(dateField1)?.setErrors({higherThanStart: true}) 

Your validatorFn might resemble the following structure

class DateValidators {
    static compareDates = (
    dateField1: string,
    dateField2: string,
    validatorField: { [key: string]: boolean }): ValidatorFn => {
      return (control: AbstractControl): { [key: string]: boolean } | null => {
      const date1 = control.get(dateField1)?.value;
      const date2 = control.get(dateField2)?.value;
      if (date1 && date2 && date1 > date2) {
         control.get(dateField1)?.setErrors({higherThanStart: true}) 
        return validatorField;
      }
      
      control.get(dateField1)?.setErrors({higherThanStart: null})
      return null;
    };
  }
}

With these changes, the error should now be visible... View Demo Here

Answer №2

There happen to be two underlying issues in this scenario.

  1. mat-error elements will only appear if there is an error at the input level on the field -- it must specifically be an error related to the individual formControl. Therefore, when implementing an *ngIf on the mat-input, it should complement the inherent ngIf that validates the presence of an error on that specific form-field. This explains why

    *ngIf="shiftEditorForm?.hasError('')"
    did not function as expected. Since no error was defined at the formControl input level, even when an error existed within the formGroup set by the validator, the mat-error would not be displayed.

  2. Applying the validator at the formGroup level can evaluate multiple fields within the formControl, which is necessary in this case. However, the return values of the validator (the error object or null) only pertain to the formGroup and do not extend to individual formControl inputs. Hence, manual use of setErrors({ [error]: true }) on each date formControl along with returning the error on the form itself becomes imperative.

The following snippet demonstrates the validator code:

import { AbstractControl, ValidatorFn } from "@angular/forms";

export class DateValidators {

  static checkDateRange (
    dateField1: string,
    dateField2: string,
    error: string
  ): ValidatorFn {
      return (control: AbstractControl): { [key: string]: boolean } | null => {
      const date1 = control.get(dateField1);
      const date2 = control.get(dateField2)
      if (date1?.value && date2?.value && date1?.value > date2?.value) {
        date1.setErrors({ [error]: true })
        date2.setErrors({ [error]: true })
        return { [error]: true };
      }
      date1?.setErrors(null);
      date2?.setErrors(null);
      return null;
    };
  }
}

Below is how you can link it to the formGroup:

DateValidators.checkDateRange('startDate', 'endDate', 'endDateTooSoon');

The initial two parameters represent the names of the formControl inputs, while the third denotes the error name to be associated and checked within the template.

Upon uncovering these intricacies, I envisioned sharing a concise explanation. Owen also grasped this concept concurrently, and I acknowledged his response as accurate since he articulately elucidates all aspects. My intention was simply to provide a brief overview.

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

Subtracted TypeScript concept

Is it possible to create a modified type in Typescript for React components? import {Component, ComponentType} from 'react'; export function connect<S, A>(state: () => S, actions: A){ return function createConnected<P>(componen ...

What is the best way to create adaptive images for Ghost (Content API) in Next.js?

I recently launched a blog website following the guidance in this video tutorial: https://www.youtube.com/watch?v=1SYU1GorO6Y. Here's what my blog setup includes: Frontend using Next.js, Backend powered by Ghost CMS (hosted on Heroku), and Deployment ...

ng-repeat table grouping by date

How can I utilize *ngFor to generate an HTML table grouped by date in columns? Here is an example of a JSON List: [{ "id": "700", "FamilyDesc": "MERCEDES", "model": "Mercedes-BenzClasse A", " ...

The browser failed to load the Angular controller

Within our latest project, we successfully implemented microservices with spring boot. The web application has been deployed on a Unix box. However, we are encountering an unusual problem. Although the necessary JS files are contained within the JAR file ...

What is the method to ensure that the option group is selectable?

Is there a way to enable the selection of an option group? <select> <optgroup value="0" label="Parent Tag"> <option value="1">Child Tag</option> <option value="2">Child Tag</option> </optgroup> ...

How can I collaborate on a component in Angular?

I'm currently developing an Angular application that utilizes a map feature powered by the Google Maps API. What I aim to achieve is the ability to freely move around the map to locate specific places, add markers, as well as edit existing markers, am ...

Angular 2 - Can a Content Management System Automate Single Page Application Routing?

I am interested in creating a single-page application with an integrated content management system that allows users to edit all aspects of the site and add new pages. However, I have found it challenging to configure the SPA to automatically route to a n ...

Why is @faker-js/faker not usable in a TypeScript project, showing undefined error, while the older "faker" import still functions correctly?

Currently, my packages.json file includes: "faker": "^5.5.3", "@types/faker": "^5.5.3", I am sticking with version 5.5.3 due to another project dependency (codecept) that requires this specific version. The ...

What is the process of determining if two tuples are equal in Typescript?

When comparing two tuples with equal values, it may be surprising to find that the result is false. Here's an example: ➜ algo-ts git:(master) ✗ ts-node > const expected: [number, number] = [4, 4]; undefined > const actual: [number, number] ...

Adding Objects to an Array in Angular 4

I'm currently facing an issue with adding a new object to an existing array. My Objective: I want to add a newly drawn card into the current array/object. In my app.component.ts file: @Component({ selector: 'app-root', te ...

The method Office.context.mailbox.item.internetHeaders.setAsync has not been configured

I am integrating the Microsoft Office API into Outlook. I'm attempting to add an extra x-header to my email in the composer scope for later identification. To achieve this, I referred to the following documentation: https://learn.microsoft.com/en-us/j ...

The inner workings of Angular 2: uncovering what occurs once we navigate to http://localhost:4200 on our browser

Could anyone provide a detailed explanation of the startup process for an Angular2 project? For example, after creating a sample project using Angular CLI: Run 'ng new my-test-app' Navigate to 'cd my-test-app' Start the server with & ...

Encountering an issue where attempting to access the `toUpperCase` property of undefined in an *ngFor loop leads to an error

I encountered an issue when trying to specify an index in *ngFor, as I received a type error with the following message: <div class='text' *ngFor='let item of blogs; let i = index | async | filter : 'feat' : true'> Rem ...

NextJS: Route Handler encountering Method Not Allowed (405) error when trying to redirect

Current NextJs version is 13.4.3 I have set up a route handler to capture POST requests. For more information on route handlers, please refer to the documentation at [https://nextjs.org/docs/app/building-your-application/routing/router-handlers] In this ...

What is behind the inconsistency of RxJS versions?

Trying to set up a node/angular2 solution on cloud9 has been quite the challenge. Below is my package.json: { "name": "example", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www", "postinstall": "typings install", ...

Executing npm and ng commands via an Ant script on a Windows machine leads to the error message "The specified file could not be found."

When attempting to execute the following Ant script, which runs the "npm" command: <target name ="test"> <exec executable="npm" failonerror="true"> <arg value="install" /> </exec> </target> An error occurs, i ...

Adding a fresh element and removing the initial item from a collection of Objects in JavaScript

I'm working on creating an array of objects that always has a length of five. I want to push five objects initially, and once the array reaches a length of five, I need to pop the first object and push a new object onto the same array. This process sh ...

During the test, an unexpected error occurred: Configuration file error! Module 'karma-remap-istanbul' not found

Whenever I run ng test, I keep encountering the following error message - what could be causing this issue? An unhandled exception occurred: Error in configuration file! Error: Module 'karma-remap-istanbul' cannot be found Below is the content ...

The service has terminated unexpectedly because of signal: Ended prematurely: 9

I'm encountering the error 'Service exited due to signal: Killed: 9' and am unable to launch my app. I've come across information suggesting that this may be caused by memory leaks or a lengthy startup time for the app. In all honesty, ...

Having trouble with importing ResizeSensor library into typescript

In my TypeScript app using webpack, I am attempting to utilize css-element-queries/ResizeSensor. After adding the npm package, which includes a .d.ts file, I encountered an issue when writing the following code: new ResizeSensor(element, cb); Instead of ...