Skip to content

Form events with async validators doesn't emit StatusChangeEvent #56999

@blazekv

Description

@blazekv

Which @angular/* package(s) are the source of the bug?

forms

Is this a regression?

No

Description

In special cases emitting status does not still work. It was already reported in #41519 and should be fixed but it is not.

Problem is that Angular is internally calling _runAsyncValidator with own emitEvent flags which could override options like emitEvent which was previously called by using Forms API (pathValue, setValue etc.). Minimal reproduction is in Stackblitz link but there are several important things:

  1. There is asyncValidator with some delay
  2. There is formGroup and formControlName in template (without formControlName directive it will work)
  3. patchValue is called in @input setter - if you call it in ngOnInit it will work - because Angular is calling updateValueAndValidity several times this setup probably leads to scenarion described at the bottom

Actual implementation by @JeanMeche should fix this problem by passing this events into new events stream. I think that implementation is still problematic because the main problem is that something could internally change form status without informing subscribers, but despite this I believe that it still does not work as intended.

Also reichemn point out here #41519 (comment) that documentation is not correct.

In current implementation there is new internal flag shouldHaveEmitted which should have information about if some manual call of API request emitEvent, but problem is that it is based only on emitEvent flag and not also shouldHaveEmitted flag. So if there are multiple calls with updateValueAndValidity and async call with flag emitEvent is cancelled by them information about emit event is lost. If there is only one call with emitEvent false after manual call it is OK.

Therefore this scenario will break that behavior:

  1. Manual call which will trigger updateValueAndValidity - form.patchValue({}, {emitEvent: true})
  2. Internal call which will trigger updateValueAndValidity with emitEvent false - this will cancel previous asyncValidatorSubscription and because emitEvent was true it will continue with shouldHaveEmitted set to true
  3. Internal call which will trigger updateValueAndValidity with emitEvent false - this will cancel previous asyncValidatorSubscription and because emitEvent was false it will continue with shouldHaveEmitted set to false
  4. Result is that previous information that events should be emitted are completly lost by internal calls of framework

It looks like this little change (when also shouldHaveEmitted is retrieved from canceled subscriptions) could fix this. I changed only these two lines
shouldHaveEmitted: shouldHaveEmitted !== false
and
const shouldHaveEmitted = (this._hasOwnPendingAsyncValidator?.emitEvent || this._hasOwnPendingAsyncValidator?.shouldHaveEmitted) || ?? false;

Code with context

_runAsyncValidator(shouldHaveEmitted, emitEvent) {
    if (this.asyncValidator) {
      this.status = PENDING;
      this._hasOwnPendingAsyncValidator = {
        emitEvent: emitEvent !== false,
        shouldHaveEmitted: shouldHaveEmitted !== false
      };
      const obs = toObservable(this.asyncValidator(this));
      this._asyncValidationSubscription = obs.subscribe((errors) => {
        this._hasOwnPendingAsyncValidator = null;
        this.setErrors(errors, {
          emitEvent,
          shouldHaveEmitted
        });
      });
    }
  }
  _cancelExistingSubscription() {
    if (this._asyncValidationSubscription) {
      this._asyncValidationSubscription.unsubscribe();
      const shouldHaveEmitted = (this._hasOwnPendingAsyncValidator?.emitEvent || this._hasOwnPendingAsyncValidator?.shouldHaveEmitted) || ?? false;
      this._hasOwnPendingAsyncValidator = null;
      return shouldHaveEmitted;
    }
    return false;
  }

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/stackblitz-starters-zssofu?file=src%2Fmain.ts

Please provide the exception or error you saw

No response

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 18.1.0
Node: 20.12.2
Package Manager: pnpm 9.2.0
OS: win32 x64

Angular: 18.1.0
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1800.3
@angular-devkit/build-angular   18.1.0
@angular-devkit/core            18.0.3
@angular-devkit/schematics      18.0.3
@schematics/angular             18.0.3
rxjs                            7.8.1
typescript                      5.4.5
zone.js                         0.14.7

Anything else?

No response

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions