angular: Form statusChanges with async validators doesn't leave PENDING state

🐞 bug report

Affected Package

The issue is caused by package @angular/forms

Is this a regression?

No this has been around since early 2017 but version 11 claims to have fixed this, and yet the bug is still present.

Description

FormGroup and FormControl do not emit first statusChanges when async validators are present, leaving the form in the PENDING state. Issues #14542 and #20424 reported this problem and were closed by #38354, which is available in Angular 11. However, running both of the stackblitz examples from these issues in Angular 11 demonstrates the original problem still exists.

🔬 Minimal Reproduction

https://stackblitz.com/edit/angular-async-status-change

🌍 Your Environment

Angular Version:


Angular CLI: 11.2.8
Node: 14.15.4
OS: darwin x64

Angular: 11.2.9
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1102.8
@angular-devkit/build-angular   0.1102.8
@angular-devkit/core            11.2.8
@angular-devkit/schematics      11.2.8
@angular/cli                    11.2.8
@schematics/angular             11.2.8
@schematics/update              0.1102.8
rxjs                            6.6.7
typescript                      4.1.5

Anything else relevant?

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 41
  • Comments: 43 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Plus

Still

As a hacky workaround you can manually trigger the validation after form initialization:

ngOnInit() {
  this.formGroup = new FormGroup(...etc);
  ...
  setTimeout( () => { 
    this.formGroup['myAsyncValidatedControl'].updateValueAndValidity();
  });
}

Hi @AndrewKushnir

In my case I am validating a FormGroup with an AsyncValidator against a backend endpoint. The form unfortunately stays in pending state, which makes the form.valid check unusable.

There have been several people posting potential workarounds, as for my case none of these seems to be working. I guess the release of Angular 17 is too close for such a breaking change to come in. However there are several issues reported and from what I could find one that should have fixed this but didn’t, in an early major release of Angular (I think v11).

Could you guys at least provide an official way to work around this issue until it lands in a major release?

That would help very much. Thanks!

I’m eyeing at a fix with #55134. We’ll need to run some tests to see how breaking that change would be. If it’s too breaking, it might be available only via the new observable that will land with #54579.

Solve it like this

  ngOnInit() {
    this.form.statusChanges.pipe(takeUntil(this.destroyed), pairwise())
        .subscribe(([prev, next]) => {
          if (prev === 'PENDING' && next !== 'PENDING') {
            this.form.updateValueAndValidity();
          }
        });
  }

@AndrewKushnir I think it’s due to call of _updateTreeValidity with emitEvent parameter as false within _updateDomValue. Maybe _updateTreeValidity should evaluate emitEvent property for each control by look whether it, has an asyncValidator, as it’s done in constructor of FormControl?

OK angular team… Several years and we DO NOT HAVE even working async validators in CVA… and Validator interface.

NICE. At least, we have deprecated class based things and signals()…

Hard to believe.

Can you provide stackblitz with the issue? In my project we are using asyncValidators without major issues. The only issues we have are missing initial status emission and delay/debounce.

Okay sorry you are right @tadamczak. Seems like I’m having another problem in my project. Here in my stackblitz the async validation works like a charm.

@DmitryEfimenko My particular issue is that I’m trying to reuse statusChanges of an inner form where I have async validators when using the Composite ControlValueAccessor pattern, so that I don’t have to pass down the errors to the children (very cumbersome).

So, I’m stuck with this code which doesn’t work in my CVA:

validate(_: FormControl) {
  // Doesn't work, initially the form stays PENDING.
  return this.form.statusChanges.pipe(
    startWith(this.form.status),
    first(status => status !== 'PENDING'),
    map(status => status === 'INVALID' ? { ... } : null)
  );
}

The only solution which came to mind was to do some sort of “polling” via timeout / retry or switchMap/timer, which works but the code is pretty ugly either way, eg:

validate(_: FormControl) {
  return defer(() => this.form.statusChanges.pipe(
    startWith(this.form.status),
    first(status => status !== 'PENDING'),
    map(status => status === 'INVALID' ? 'error' : null)
  )).pipe(
    timeout(500),
    retry(),
  )
}

But it could be abstracted:

// Always returns a status, if pending it checks every X milliseconds
function statusOf(
  control: AbstractControl,
  pollingRate: number = 500
): Observable<FormControlStatus> {
  return control.statusChanges.pipe(
    startWith(control.status),
    switchMap(status => {
      if (status !== 'PENDING') {
        return of(status);
      }
      return timer(0, pollingRate).pipe(
        map(() => control.status),
        takeWhile(status => status === 'PENDING', true),
      )
    }),
    distinctUntilChanged()
  )
}

...

validate(_: FormControl) {
  return statusOf(this.form).pipe(
    first(status => status !== 'PENDING'),
    map(status => status === 'INVALID' ? { ... } : null)
  )
}

Hope this is helpful! (test that code cause I wrote it in 10 minutes, but it seems to be working)

PS. While this kinda works for my usecase I’m getting ExpressionChangedAfterItHasBeenChecked, I’ll see if there’s a solution for that 😃