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

Steps to implement the click functionality on the alert controller and modify the language in Ionic 4

I am currently developing a multilingual Ionic 4 app and have implemented the alert controller to display language options. However, I am facing an issue on how to dynamically change the language based on user selection. Below is my app.component.ts code ...

Creating a module within a component in angular - step by step guide

I am interested in dynamically creating a component inside another component. This will allow me to pass my dynamic HTML template directly to the decorator like this: //code /** * @param template is the HTML template * @param container is @ViewChild(& ...

Customizing the renderInput of the Material UI DatePicker

Recently I integrated material-ui into my React project with TypeScript. I implemented the following code based on the example provided on the official website. import AdapterDateFns from '@mui/lab/AdapterDateFns'; import DatePicker from '@m ...

Creating a personalized menu using Nextron (electron) - Step by step guide

I'm currently in the process of developing an app with Nextron (Electron with Nextjs and Typescript). Although I have the foundation of my Next app set up, I've been encountering issues when attempting to create a custom electron menu bar. Every ...

What is the best way to utilize RxJs for streaming HostListener events?

Although I've found plenty of resources on binding Angular HostListeners, I'm curious about using RxJs to stream it instead: @HostListener('document:click', ['$event']) handleClick(event: Event) { // etc } I want to cre ...

Customize your Kendo Chart in Angular2 by selecting the axis for your data

I need help creating a scatter chart with two separate datasets that have different Y-Axis How can I make sure the second series uses the second Y-Axis in the chart? <kendo-chart [title]="" style="height:290px"> <kendo-chart-series> < ...

What is the most efficient way to dynamically load a script file before proceeding with the rest of the code execution?

In my Angular home.component.ts file, I have added the script loading code as shown below: export class HomeComponent { constructor() { this.loadDynamicScript(); } public loadDynamicScript() { var script = document.createElement(&a ...

Conceal multiple parameters within routing for added security

I have setup my Angular component with a button that triggers an event. Inside this event, I currently have: this.router.navigate('page2') While I am aware that query parameters can be passed inside the URL, I am faced with the challenge of pas ...

When the React Native Expo app is running, the TextInput form is covered by the keyboard

When I launch the app using expo and implement my DateFormInput component, the issue of Keyboard covering TextInput arises. Despite trying packages like "@pietile-native-kit/keyboard-aware-scrollview", "@types/react-native-keyboard-spacer", "react-native-k ...

Comprehending the concepts of Observables, Promises, using "this" keyword, and transferring data within Angular with TypeScript

I am trying to figure out why I keep receiving the error message: "Uncaught (in promise): TypeError: this.dealership is undefined" when working with the authentication.service.ts file. export class AuthenticationService { private currentUserSubject: ...

Is it feasible to alter the TypeScript interface for the default JavaScript object (JSON)?

When dealing with cyclic objects, JSON.stringify() can break (as mentioned in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value) An alternative solution suggested in the same article is to use 'cycle.js&apos ...

Using masonry-layout with Next Js leads to a ReferenceError stating that window is not defined

Implementing the masonry-layout library by David Desandro in my Next app has been a smooth process. You can find the link here. When I apply it, the masonry layout functions perfectly as intended. Here's how I'm incorporating it successfully: imp ...

Send information to the child.component.ts file, not the child template

A scenario I am working on involves passing a value from a parent component to a child component. However, prior to displaying this value in the child.component.html file, I have a requirement to increment it by 2 within the app.component.ts file, and then ...

Tips for detecting the end of a scroll view in a list view

I've encountered an issue with my scrollView that allows for infinite scrolling until the banner or container disappears. What I'm aiming for is to restrict scrolling once you reach the last section, like number 3, to prevent the name part from m ...

ngx: navigate to the specified URL once the user has been successfully logged in

I am faced with a dilemma where I must wait for my authentication server to return my token before calling my APIs. I am looking for a solution to ensure that my authState.token is not null before dispatching LoadMyStuffFromApi. I have implemented two res ...

What is the method for defining a function within a TypeScript namespace?

Suppose there is a namespace specified in the file global.d.ts containing a function like this: declare namespace MY_NAMESPACE { function doSomething(): void } What would be the appropriate way to define and describe this function? ...

Tips for effectively narrowing the `undefined` type

Why am I getting this error message? const func = (a: unknown) => { if (a && typeof a === 'object' && 'b' in a) { a.b; } }; The error message I'm receiving is: Property 'b' does not exist on ty ...

Sorting items in Ag-Grid according to user's preference

Here is an example of a header in ag-grid with custom sorting applied: { headerName: "StudentId", field: "StudentId", width: 140, editable: false, enableRowGroup: true, comparator: (valA, valB, n1, n2, inver ...

Tips for initializing constructor arguments using JSON during object instantiation in TypeScript

Let's consider a scenario where we have the following class: export class PersonInformation { constructor( public firstName: string = "", public lastName: string = "", public middleName: string = "", ) { } } Now, we&a ...

BarChart is failing to exhibit data in tooltips when using dynamic keys

Query Description Hello! I'm currently tackling an issue with a bar chart. Everything is working smoothly, except for the default tooltip, which appears blank when hovering over the bars. My chart utilizes dynamic keys for the legends, and they are f ...