Change validators dynamically according to conditions

Scenario:

At the start, there is a single text box named Name1, a date picker called DOB1, and a check box labeled Compare. Both Name1 and DOB1 are mandatory. When the checkbox is clicked, two new form controls are dynamically included, named Name2 and DOB2, and at least one of either Name1 or DOB2 becomes required.

Hence, a valid form must have any of the following combinations:

  1. Name1 DOB1 Name2 or //If Name2 is valid then need to remove required validator from DOB2
  2. Name1 DOB1 DOB2 or //If DOB2 is valid then need to remove required validator from Name2
  3. Name1 DOB1 Name2 DOB2

In all these cases, when the form is valid, the submit button should be enabled.

Issue:

I attempted to use setValidators but still cannot determine what I am missing. Upon clicking the checkbox, the form is only considered valid if all four controls are valid, whereas I require just three of them to be valid.

Code:

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <ion-card class="person1">
    <ion-card-content>
      <ion-list lines="full" class="ion-no-margin ion-no-padding">
        <ion-item>
          <ion-label position="stacked">Name / Number <ion-text color="danger">*</ion-text>
          </ion-label>
          <ion-input type="text" formControlName="NameNumber"></ion-input>
        </ion-item>
        <ion-item>
          <ion-label position="stacked">Date of birth<ion-text color="danger">*</ion-text>
          </ion-label>
          <ion-datetime required placeholder="Select Date" formControlName="DateOfBirth"></ion-datetime>
        </ion-item>
      </ion-list>
    </ion-card-content>
  </ion-card>
  <ion-card class="person2" *ngIf="isComparisonChecked">
    <ion-card-content>
      <ion-list lines="full" class="ion-no-margin ion-no-padding">
        <ion-item>
          <ion-label position="stacked">Name / Number <ion-text color="danger">*</ion-text>
          </ion-label>
          <ion-input type="text" formControlName="NameNumber2"></ion-input>
        </ion-item>
        <ion-item>
          <ion-label position="stacked">Date of birth<ion-text color="danger">*</ion-text>
          </ion-label>
          <ion-datetime required placeholder="Select Date" formControlName="DateOfBirth2"></ion-datetime>
        </ion-item>
      </ion-list>
    </ion-card-content>
  </ion-card>
  <ion-item class="compare-section" lines="none">
    <ion-label>Compare</ion-label>
    <ion-checkbox color="danger" formControlName="IsCompare"></ion-checkbox>
  </ion-item>
  <div class="ion-padding">
    <ion-button color="danger" *ngIf="LicensedStatus" [disabled]="!this.profileForm.valid" expand="block"
      type="submit" class="ion-no-margin">Submit</ion-button>
  </div>
</form>

Ts:

profileForm = new FormGroup({
NameNumber: new FormControl('', [Validators.required, Validators.pattern('^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$')]),
DateOfBirth: new FormControl('', Validators.required),
IsCompare: new FormControl(false)
});
...
this.profileForm.get('IsCompare').valueChanges.subscribe(checked => {
if (checked) {
    this.profileForm.addControl('NameNumber2', new FormControl('', [Validators.required, Validators.pattern('^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$')]));
    this.profileForm.addControl('DateOfBirth2', new FormControl('', Validators.required));

    this.profileForm.get('NameNumber2').valueChanges.subscribe(() => {
      if (this.profileForm.get('NameNumber2').valid) {
        this.profileForm.get('DateOfBirth2').clearValidators();
      }
      else {
        this.profileForm.get('DateOfBirth2').setValidators([Validators.required]);
      }
    this.profileForm.get('DateOfBirth2').updateValueAndValidity();
    });

    this.profileForm.get('DateOfBirth2').valueChanges.subscribe(() => {
      if (this.profileForm.get('DateOfBirth2').valid) {
        this.profileForm.get('NameNumber2').clearValidators();
      }
      else {
        this.profileForm.get('NameNumber2').setValidators([Validators.required, Validators.pattern('^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$')]);
      }
    this.profileForm.get('NameNumber2').updateValueAndValidity();
    });
  }
  else {
    this.profileForm.removeControl('NameNumber2');
    this.profileForm.removeControl('DateOfBirth2');
  }
});

What could be the missing piece here?

Update #1:

I made amendments in the above code. If I utilize updateValueAndValidity, an error message is encountered in the console.

https://i.stack.imgur.com/y3NGj.png

Answer №1

Below is the code snippet you can utilize:

this.profileForm.get('DateOfBirth2').setValidators([Validators.required]);
this.profileForm.get('DateOfBirth2').updateValueAndValidity();

Answer №2

It seems like the issue stems from the fact that when updateValueAndValidity() is called, it triggers another valueChanges event, leading to an infinite loop within your subscriptions.

this.profileForm.get('NameNumber2').valueChanges.subscribe(() => {
  // code omitted
  this.profileForm.get('DateOfBirth2').updateValueAndValidity(); // Triggers 'DateOfBirth2' valueChanges 
});

this.profileForm.get('DateOfBirth2').valueChanges.subscribe(() => {
  // code omitted
  this.profileForm.get('NameNumber2').updateValueAndValidity(); // Triggers 'NameNumber2' valueChanges 
});

One way to tackle this problem is by using distinctUntilChanged, as mentioned in previous solutions.

Alternatively, you can utilize a feature of updateValueAndValidity() that allows you to configure its behavior. By passing {emitEvent: false} to updateValueAndValidity(), you can prevent the emission of the valueChanges event and break the loop.

this.profileForm.get('NameNumber2').valueChanges.subscribe(() => {
  // code omitted
  this.profileForm.get('DateOfBirth2').updateValueAndValidity({emitEvent: false}); // Does NOT trigger valueChanges for 'DateOfBirth2'
});

this.profileForm.get('DateOfBirth2').valueChanges.subscribe(() => {
  // code omitted
  this.profileForm.get('NameNumber2').updateValueAndValidity({emitEvent: false}); // Does NOT trigger valueChanges for 'NameNumber2'
});

Answer №3

Implementing the distinctUntilChanged method from rxjs/operators effectively resolves the issue of encountering the Maximum call stack size exceeded error.

Modify the line as shown below:

this.profileForm.get('NameNumber2').valueChanges.pipe(distinctUntilChanged()).subscribe(() => {

The updated code snippet will look like this:

import { distinctUntilChanged } from 'rxjs/operators';

this.profileForm.get('NameNumber2').valueChanges.pipe(distinctUntilChanged()).subscribe(() => {
     if (this.profileForm.get('NameNumber2').valid) {
        this.profileForm.get('DateOfBirth2').clearValidators();
     }
     else {
       this.profileForm.get('DateOfBirth2').setValidators([Validators.required]);
     }
     this.profileForm.get('DateOfBirth2').updateValueAndValidity();
  });

this.profileForm.get('DateOfBirth2').valueChanges.pipe(distinctUntilChanged()).subscribe(() => {
     if (this.profileForm.get('DateOfBirth2').valid) {
        this.profileForm.get('NameNumber2').clearValidators();
     }
     else {
        this.profileForm.get('NameNumber2').setValidators([Validators.required, Validators.pattern('^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$')]);
     }
     this.profileForm.get('NameNumber2').updateValueAndValidity();
});

After making the above changes and running the modified code, I verified that the form is valid and the submit button remains enabled for all mentioned scenarios.

Answer №4

Consider implementing a customValidator that can handle multiple errors and check for errors across the entire form. This way, you can easily track which fields have errors by using an auxiliary function.

  form=new FormGroup({
    name1:new FormControl(),
    date1:new FormControl(),
    compare:new FormControl(),
    name2:new FormControl(),
    date2:new FormControl(),
  }, this.customValidator())

  hasError(error:string)
  {
    return this.form.errors ? this.form.errors.error.find(x=>x==error) : null
  }

  customValidator()
  {
    return (form:FormGroup)=>{
      const errors=[];
      if (!form.value.compare)
      {
        if (!form.value.name1)
            errors.push('name1')
        if (!form.value.date1)
            errors.push('date1')
      }
      else
      {
          ....
      }
      return errors.length ? {error:errors} : null
    }
  }

You can then structure your form as follows:

<form [formGroup]="form">
  <input formControlName="name1"/>
  <span *ngIf="hasError('name1')">*</span>

  <input formControlName="date1"/>
  <span *ngIf="hasError('date1')">*</span>
  <br/>
  <input type="checkbox" formControlName="compare"/>
  <br/>
  <input *ngIf="form.get('compare').value" formControlName="name2"/>
  <span *ngIf="hasError('name2')">*</span>
  <input *ngIf="form.get('compare').value" formControlName="date2"/>
    <span *ngIf="hasError('date2')">*</span>
</form>

An alternative approach is to use a customValidator that always returns null but manually sets errors on specific fields using setErrors method.

  customValidator()
  {
    return (form:FormGroup)=>{
      const errors=[];
      if (!form.value.compare)
      {
        if (!form.value.name1)
            errors.push('name1')
        if (!form.value.date1)
            errors.push('date1')
      }
      else
      {
         ....other logic...
      }
      form.get('name1').setErrors(errors.find(x=>x=='name1')?{error:"required"} : null)
      form.get('date1').setErrors(errors.find(x=>x=='date1')?{error:"required"} : null)
      form.get('name2').setErrors(errors.find(x=>x=='name2')?{error:"required"} : null)
      form.get('date2').setErrors(errors.find(x=>x=='date2')?{error:"required"} : null)
      return null
    }
  }

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

Creating a carousel with material design aesthetics

I'm working on implementing a carousel in my setup using Angular CLI: 6.0.5, Node: 10.1.0, OS: win32 x64, and Angular: 6.0.3. However, I haven't been able to locate documentation for creating the carousel in Angular's Material Design framewo ...

Steps for adding a checkbox to an alertController in Ionic 4

Currently, I am working on Ionic 4 and my goal is to create a checkbox field within the Alert Controller that pulls its options from an array of objects. However, when implementing the code, I am only able to see one checkbox instead of having multiple che ...

Encountered an ERROR when attempting to deploy a next.js app to Azure Static Webapp using GitHub actions for

I am encountering an issue that is causing some frustration. The problem only arises during my github actions build. Interestingly, when I run the build locally, everything works perfectly and I can access the route handler without any issues. However, eve ...

Angular 2 form with ng2-bootstrap modal component reset functionality

I have implemented ng2-bs3-modal in my Angular 2 application. I am now looking for a way to clear all form fields when the close button is clicked. Can anyone suggest the best and easiest way to achieve this? html <button type="button" class="btn-u ...

How to dynamically track changes in Angular2 form fields: Using the formbuilder to

Currently, I'm working with ionic2 and angular2. I have a form constructed using formbuilder but I am looking to monitor any new changes made to a specific input. ionViewDidLoad() { this.purchaseDataForm = this.formBuilder.group({ kms: ['& ...

Filtering an array of objects based on another array of objects in Angular2 through the use of pipes

I'm having trouble understanding how to use the angular pipe to filter an array of objects based on another array of objects. Currently, I have a pipe that filters based on a single argument. I am working with two arrays, array1 and array2, both cont ...

ESLint refuses to be turned off for a particular file

I am in the process of creating a Notes.ts file specifically for TypeScript notes. I require syntax highlighting but do not want to use eslint. How can I prevent eslint from running on my notes file? Directory Structure root/.eslintignore root/NestJS.ts r ...

Using Angular to automatically update the user interface by reflecting changes made in the child component back to the parent component

Within Angular 5, I am utilizing an *IF-else statement to determine if the authorization value is true. If it is true, then template 2 should be rendered; if false, then template 1 should be rendered. Below is the code snippet: <div *ngIf="authorized; ...

The issue persists in VSCode where the closing brackets are not being automatically added despite

Recently, I've noticed that my vscode editor is failing to automatically add closing brackets/parenthesis as it should. This issue only started happening recently. I'm curious if anyone else out there has encountered this problem with their globa ...

Assign a true or false value to every attribute of an object

Imagine we have a scenario with an interface like this: interface User { Id: number; FirstName: string; Lastname: string; Age: number; Type: string; } and a specific method for copying properties based on a flag. ...

"Encountered a problem when trying to import stellar-sdk into an Angular

Our team is currently working on developing an app that will interact with the Horizon Stellar Server. As newcomers in this area, we are exploring the use of Angular 8 and Ionic 4 frameworks. However, we have encountered difficulties when trying to import ...

Cancelling an ongoing AWS S3 upload with Angular 2/Javascript on button click

I'm currently working with Angular 2 and I have successfully implemented an S3 upload feature using the AWS S3 SDK in JavaScript. However, I am now facing a challenge: how can I cancel the upload if a user clicks on a button? I've attempted the ...

What is the method to access the value of a different model in ExpressJs?

I am working on a MEAN stack and have a model for employee information. I want to include the store name where they will work from a separate stores model. Can you review my approach below? Employee Model: var mongoose = require('mongoose'); va ...

Exploring the best practices for integrating Bootstrap into a Webpack and Angular2 project

I am looking to incorporate Bootstrap into my Angular2 project, including both the CSS and JS files. What is the best way to include these files in the project to ensure webpack functions properly? In the previous version using systemjs, it was included i ...

When trying to integrate Angular.ts with Electron, an error message occurs: "SyntaxError: Cannot use import statement

Upon installing Electron on a new Angular app, I encountered an error when running electron. The app is written in TypeScript. The error message displayed was: import { enableProdMode } from '@angular/core'; ^^^^^^ SyntaxError: Cannot use impor ...

Utilizing Jquery Validation to Remove a Class Upon Form Validation Success

In my current registration process, I have a multipart form where each subsequent form is displayed when the next button is pressed without fading effects. Initially, the button appears faded. Here's a simplified version of how I handle the first form ...

Set the variable value by clicking on another component within *ngFor in Angular 2

I am attempting to use *ngFor to pass an object to another component, but only the last object in the table is being passed. The object that was clicked should be displayed instead. How can I solve this issue? <tr data-toggle="control-sidebar" *ngFor=" ...

Adding a QR code on top of an image in a PDF using TypeScript

Incorporating TypeScript and PdfMakeWrapper library, I am creating PDFs on a website integrated with svg images and QR codes. Below is a snippet of the code in question: async generatePDF(ID_PRODUCT: string) { PdfMakeWrapper.setFonts(pdfFonts); ...

transferring information seamlessly in Ionic using Angular router without the need for navigation

I have a list of names that are displayed, and when I click on any name in the list, it should take me to another page without actually navigating to that path. names.page.html: <ion-content> <ion-list *ngFor='let person of people'&g ...

Is it possible to close a tab while the chrome extension popup is still active?

I am currently developing a Chrome extension that reads the content of the current tab and performs a heavy task on the backend. If I were to close the tab while the process is ongoing, how can I allow the user to do so without waiting for the task to fi ...