DP Park
DP Park

Reputation: 865

Angular Async Validator FormControls don't update until blurred

My Async Validator looks like this:

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 problem with my Async Validator is that my FormControls that are added into my FormArray do not pick up on the current 'status' of themselves until they are blurred.

This is my FormArray and my FormControl inside 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 div with the class "url-pending" appears, and then it doesn't disappear - even though the FormControl it depends upon is validated by the backend - until the user blurs the FormControl that the div depends on.

The only other stackoverflow question that is similar to this is this link. I could not fully understand the instructions of that link, and an additional complication I had compared to the poster of the link is that I have an icon in my form shaped as a plus sign so that the user can add more FormControls to the FormArray. I couldn't figure out how to add directives to the FormControls that the user added by interacting with the form.

I will answer my own question because I figured out how to solve this question, but I solved it in a 'hackish' way. If anybody else knows a better answer to this, please reply.

Upvotes: 2

Views: 2966

Answers (3)

AlpenDitrix
AlpenDitrix

Reputation: 320

This has to do with touched state of the form. Until control is untouched, it is treated as "maybe user did not yet finished the input".

So you was almost right about the symptoms, but there's also a simpler better way to "touch" a control. The crucial part is usage of deafault ReactiveForm API to make the control touched. See how it was in my finalize part:

component.ts


public emailControl = new FormControl('',
  /* sync */
  [
    control => control.value && /.+@.+/.test(control.value) ? null : { invalidEmail: true },
  ],
  /* async */
  [
    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 ... */),

    // "Touch" the control since its data was already sent and used somewhere
    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 personally faced this issue when were trying to implement simple /.+@.+/ email validation. The thing is that input should not scream at user "INVALID!!" after the first letter into writing. It should wait until input is presumably "complete": when user continues to the next input field (postal address, nickname, whatever). When the user leaves the control it gets touched: true and all the validation is rendered (it were still computing in the background but it was not rendering exactly due to touched: false state). Now the user may come back to the touched input (and it remains touched, until state is manually reset!) and will see all validation updated in realtime.

In the example above control was invalid until it's not a "valid" email and it does not even try to do the async check (since failed with sync), but it DOES NOT SHOW IT while control it touched: false. But when it finally [email protected], the async check is triggered, "pending operation"-spinner appears and the control itself becomes touched: true and reveals validation results upon resolution.

Upvotes: 0

Justin Wrobel
Justin Wrobel

Reputation: 2029

I ran into a similar issue; however, I was able to resolve it by piping statusChanges to async

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

So in your case:


<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

Upvotes: 1

DP Park
DP Park

Reputation: 865

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

Then I added finalize() to the return of timer$ in Async Validator. Inside the callback of the operator, I made each FormControl of the FormArray focus and then blur.

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

Each FormControl has to be focused first because if the user blurred before the validation ends, then the FormControl never blurs and the 'pending' state continues forever on display (not functionally though).

Upvotes: 1

Related Questions