Angular form controls with async validators do not reflect updates until they are unfocused

Here is the structure of my Async Validator:

asyncValidator(service:ApiCallsService):AsyncValidatorFn{
    return (control:FormControl):Promise<ValidationErrors | null> | Observable<ValidationErrors | null> =>{
    let timer$ = timer(2000);
     return timer$.pipe(
      take(1),
      switchMap(()=> {
        let videoId = service.checkUrl(control.value);
        return service.getVideoDescription(videoId).toPromise().then((val:any)=>{

          return (!val.id) ? {"invalidUrl": true} : null;
        })
      })
    )
    }
  }

The issue I am facing with my Async Validator is that the FormControls in my FormArray do not reflect their current 'status' until they are blurred.

Below is my FormArray and the FormControl within it:

<div class="url-con" formArrayName="urls" >
    <div *ngFor="let url of urls.controls; let i=index" class="url-input-con">
        <input  minLength="5" placeholder="Video Url" class="url-input" [formControlName]="i">
        <div class="url-pending" *ngIf="urls.controls[i].pending && !urls.controls[i].valid">Validating...</div>
    </div>
</div>

The "url-pending" div remains visible even after validation, until the associated FormControl is blurred.

I encountered a similar issue discussed in this link. However, I could not fully implement the solution provided. Additionally, my scenario involves adding new FormControls dynamically through an icon shaped as a plus sign, which added to my complexity.

I will post my own answer detailing how I managed to resolve this issue, but I acknowledge that my approach may be considered somewhat unconventional. Any suggestions for a more optimal solution would be greatly appreciated.

Answer №1

I made a unique change by assigning an identifier to the formArray (#formArray) :

<div #formArray class="url-con" formArrayName="urls" >
    <div *ngFor="let url of urls.controls; let i=index" class="url-input-con">
        <input  minLength="5" placeholder="Video Url" class="url-input" [formControlName]="i">
        <div class="url-pending" *ngIf="urls.controls[i].pending && !urls.controls[i].valid">Validating...</div>
    </div>
</div>

Furthermore, I introduced finalize() to the return value of timer$ in the Async Validator function. Within the callback of this operator, I added code to focus and blur each FormControl within the FormArray.

asyncValidator(service:ApiCallsService):AsyncValidatorFn{
   return (control:FormControl):Promise<ValidationErrors | null> | Observable<ValidationErrors | null> =>{
   let timer$ = timer(2000);
    return timer$.pipe(
     take(1),
     switchMap(()=> {
       let videoId = service.checkUrl(control.value);
       return service.getVideoDescription(videoId).toPromise().then((val:any)=>{

         return (!val.id) ? {"invalidUrl": true} : null;
       })
     }),
     finalize(()=>{
         Array.from(this.formArray.nativeElement.children)
              .forEach((val:HTMLElement,ind)=>{
                   (Array.from(val.children)[0] as HTMLElement).focus();
                   (Array.from(val.children)[0] as HTMLElement).blur();
               })         
     })
   )
}
}

To ensure proper functionality, it is crucial for each FormControl to be focused before the user blurs them while validation is ongoing. Failure to do so may result in the 'pending' state being displayed indefinitely, even though there are no functional issues associated with it.

Answer №2

I encountered a similar problem, but I managed to fix it by using the statusChanges function with async

{{field.statusChanges | async}}
or 
...*ngIf="(field.statusChanges | async) === 'PENDING'"...

So in your situation:


<div #formArray class="url-con" formArrayName="urls" >
    <div *ngFor="let url of urls.controls; let i=index" class="url-input-con">
        <input  minLength="5" placeholder="Video Url" class="url-input" [formControlName]="i">
        <div class="url-pending" 
             *ngIf="(urls.controls[i].statusChanges | async) === 'PENDING'">
                Validating...
        </div>
    </div>
</div>

Sources

Answer №3

Regarding the form's touched state, it remains in an "untouched" state until the user completes input. This indicates that the user may not have finished entering information yet.

Your observation about the symptoms was close to accurate, but there is a simpler way to mark a control as "touched". The key is utilizing the default ReactiveForm API to update the control's status. Refer to how this was achieved in the finalize section:

component.ts


public emailControl = new FormControl('',
  /* synchronous */
  [
    control => control.value && /.+@.+/.test(control.value) ? null : { invalidEmail: true },
  ],
  /* asynchronous */
  [
    control => control.value ? this.debouncedCheck(control.value) : of(null)
  ]
);

private debouncedCheck(value): Observable<ValidationErrors> {
  // debounce (although it will be PENDING even during those 500ms!!)
  return timer(500).pipe(

    // Handle validation
    switchMap(() => this.doAsyncCheck(value)),
    tap(checkResponse => /* ... handle validation response ... */),
    catchError(error => /* ... handle runtime errors ... */),

    // Mark the control as touched since its data has been processed and used elsewhere
    finalize(() => this.lookupEmail.markAsTouched()),

  );
}

template.html

<mat-form-field>
    <mat-label>Find by email</mat-label>

    <input matInput [formControl]="emailControl">
    <mat-spinner matSuffix *ngIf="emailControl.pending" [diameter]="15"></mat-spinner>

    <mat-error *ngIf="emailControl.hasError('invalidEmail')">Email is invalid</mat-error>
    <mat-error *ngIf="emailControl.hasError('noRecords')">No records found for email</mat-error>
</mat-form-field>

I encountered this issue when attempting to implement simple email validation with /.+@.+/. One important aspect was ensuring that the input field does not immediately display an "INVALID!!" message after just one letter. It should wait until the user finishes input or moves to another field before displaying validation results. Once the user interacts with the control (leaves and returns), it updates in real-time based on the current validation status.

In the example above, the control remained invalid until a "valid" email was entered, triggering the async check only after the sync validation failed. However, the validation feedback was not displayed while the control was in a touched: false state. It was only upon becoming touched: true that the async check initiated, showing the pending operation spinner and updating validation results accordingly.

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

Leveraging ArangoJS Driver within an Angular2 web platform

Currently, I am in the process of working on a project that involves Angular2 and Typescript (v1.8.10). Our aim is to incorporate data from an ArangoDB database into the web application. Ideally, I would like to utilize the arangojs driver for this task. H ...

collection of assurances and the Promise.all() method

Currently, I am dealing with an array of Promises that looks like this: let promisesArray = [ service1.load('blabla'), service2.load(), // throws an error ]; My goal is to execute all these Promises and handle any errors that occur, as ...

Encountered an issue in Angular 2 when the property 'then' was not found on type 'Subscription'

I have been attempting to call a service from my login.ts file but I am encountering various errors. Here is the code snippet in question: login.ts import { Component } from '@angular/core'; import { Auth, User } from '@ionic/cloud-angular ...

angular2-seed-advanced encountered an error: RangeError - The maximum call stack size has been exceeded

Attempting to launch the angular-seed-advanced project without modifications on various platforms has been successful for web and desktop (Linux/Windows). However, when trying to run it on Android (emulator and actual device), the following error occurred: ...

Transforming Image Annotation from Angular 1 to Angular 4: Overcoming Conversion Challenges and Comparing Equivalents from 1.x to 4

After exploring various options, I am still struggling to resolve the conversion error that occurs when trying to convert my Angular 1.x script for image annotation into Angular 4. The equivalent of angular 1.x code in Angular 4 is not readily available. H ...

Angular - service workers leading to unsuccessful uploads

When uploading files using Uppy (XHRUpload) in my Angular 6 app, the process is smooth on localhost with the service worker disabled. However, enabling the service worker results in successful uploads only for small files, while larger files fail to upload ...

Interpolating strings with Angular does not result in binding

My goal is to populate the template using string interpolation. However, when I attempt to reference the variable in the template, I receive the following error: core.js:1350 ERROR TypeError: Cannot read property 'status' of undefined. HTML ...

Error Encounter: Lost Shared UI Context - Executing Protractor Tests on Headless Chrome

I am currently working on running protractor tests locally with Chrome in headless mode. After reviewing the documentation, I have added the necessary options to my configuration file: capabilities: { browserName: 'chrome', shardTestFile ...

Every time my Canvas loads, it consistently fills up the entire width but neglects to occupy the complete height

My Canvas is only taking full width, but not full height. Here's my code snippet in an attempt to make it both full width and full height: export class AimComponent implements OnInit { @ViewChild('canvas') myCanvas: ElementRef; public ...

Having trouble getting Flex 1:1 to function properly in conjunction with the Ionic grid. Looking to evenly allocate space using a 50:50 ratio

.scss .content { ion-grid { height: 100% !important; } .row1 { flex: 1 !important; } .row2 { flex: 1 !important; } } .html <ion-content class="content"> <ion-grid> <ion-row class="row1 ...

Tips for implementing defaultProps in a versatile functional component using component injection in TypeScript

I have a functional component called Button, which has a prop named as to specify an HTML element or another component. My goal is to set the default value of the as prop to the Link component from react-router. This would require the to prop of Link to be ...

Constantly visible scrolling feature on material side navigation

Is there a way to keep the scroll bar in the sidenav always visible, even when the content is within the Y axis limits? This would prevent the scroll bar from fading in and out every time I open or close one of the mat-menu-items that function as accordio ...

Is there a way to organize items in an array alphabetically according to a predetermined key value?

I have an array of objects containing countries with various values for each country. My goal is to list them alphabetically. // globalBrands { [ { id: 1, title: 'Argentina', content: [{url: 'w ...

Anguar 9 is experiencing issues with loading datatable pagination, search, and sorting functionalities

My problem lies in the pagination, search, and sorting functions not loading properly in my data table despite the data binding correctly. I have provided my html, component, and service files for reference. .html <table class="table table-striped ...

Angular: Effective communication between components through routing and Observable binding ultimately results in the advancement of ngtsc(233

I designed an Angular Component named "crear-pedido" that exhibits a catalog of items (using row of products) and my aim is for the user to have the ability to click on the values in the ID column and navigate the application to a subordinate component kno ...

Encountering issues with deploying an Angular 8 website using a specific configuration

My current project is built on Angular 8, and I am in the process of publishing it locally before deploying it. When running the build step, I specify an environment name called internalprod: src ├───app ├───environments │ environme ...

The dramatist strategically positioning the cursor at the conclusion of an input field

Currently, I am utilizing playwright for my testing purposes and have encountered a specific issue that I am seeking assistance with. The behavior I need to test is as follows: Applying the bold style to existing text within my input field Verifying that ...

Is there a way for me to maintain a consistent layout across all pages while also changing the content component based on the URL route in Next.js?

I'm currently working with Typescript and Next.js My goal is to implement a unified <Layout> for all pages on my website. The layout comprises components such as <Header>, <Footer>, <Sidenav>, and <Content>. Here is the ...

Calculating the value of a property based on another property using Typescript

When working with TypeScript, we often create constant objects like the example below: export const AppConstants = { VAL1: 'val1', VAL2: 'val2' } Now, the question arises - can we reference one individual property from another wi ...

The @IsEnum function does not support converting undefined or null values to objects

When I tried to use the IsEnum class validator in the code snippet below: export class UpdateEvaluationModelForReportChanges { @IsNotEmpty() @IsEnum(ReportOperationEnum) // FIRST operation: ReportOperationEnum; @IsNotEmpty() @IsEnum(Evaluatio ...