angular: Async validators can sometimes cause form validity status to be stuck as 'PENDING'

I’m submitting a … (check one with “x”)

[x] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior Async validation can sometimes hang form validity status. Returned Promises and Observables are sometimes “ignored” and thus, the form is never marked as valid.

Expected behavior The underlying async objects would have to be resolved properly and form validity status must update properly.

Minimal reproduction of the problem with instructions Example Plunkr: http://plnkr.co/edit/nGiwOkCHngVTZKFrJ501?p=preview

Steps to reproduce :

  • I haven’t fully investigated the issue yet, but creating some AsyncValidatorFn returning either a Promise or an Observable will do. Unfortunately, there is no “magic” async function making the test crash.
  • Just type something in the input box, triggering validators and you’ll see that the HTTP call fetching the JSON error object works, that the observable .do() method is called, logging the result in the console, but the returned Observable is never resolved by Angular, and that the form status is stuck to ‘PENDING’.

What is the motivation / use case for changing the behavior? I heavily use Redux and Observables inside my projects and thus, extensively use async validators too. Having this kind of issues is somehow very unconfortable when working with async helpers.

Please tell us about your environment:

  • Operating system: Ubuntu 16.04

  • IDE: Webstorm 2016.3.1

  • package manager: NPM version 3.10.8

  • HTTP server: Express v.4.14.0

  • Angular version: Tested on 2.0.0 (locally) and 2.0.4 (on the Plunkr)

  • Browser: Firefox 50.0.1/Chrome 55.0.2883.75

  • Language: TypeScript 2

  • Node (for AoT issues): node --version = v6.9.1

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 41
  • Comments: 55 (3 by maintainers)

Most upvoted comments

This is happening because the observable never completes, so Angular does not know when to change the form status. So remember your observable must to complete.

You can accomplish this in many ways, for example, you can call the first() method, or if you are creating your own observable, you can call the complete method on the observer.

return control.valueChanges .debounceTime(500) .take(1) .switchMap(() => { return this.userService.usernameExists(username, this.user ? this.user.id : null); }) .map(exists => { return exists ? {‘forbiddenName’: {value: control.value}} : null; }) .first()

@revov Thanks for the analysis!

Honest question, is anyone else surprised that Angular moved up another full major version without mentioning this? I am not trying to bash. I love Angular, and could not have more respect.

I just never would have imagined an issue as central to building web apps as default async form validation would go two+ years and a couple full major version changes without any mention here, let alone a fix.

Because the Angular team is so top shelf, this makes me feel like I’m missing something and this issue isn’t as important as it feels to me. Anyone have any insight on my confusion?

i wonder if this is bug? coz im curious, is this serious? issue created since 2 Dec 2016 and no fix. Lost time to find why this happen … !!!

thanks

the obs.subscribe does not assign the result of the observable into this._status causing the result to be always on PENDING.

@minuz I am seeing the same thing, my control is stuck in PENDING status forever. I was not able to find any line of code that changes that and I was curious if you made any progress on your side.

@blacknight811 This has happened to me when I forget to return a response from the observable. So if you are generating a new observable on each validation check, and let’s say have a filter that checks whether there is anything in control.value, if the filter conditional fails and doesn’t pass to your api, then the control will be marked as pending because that call to filter never finished. Even if another instance passes, that original call will still have never completed as each call to the validator is unique. So make sure the observable is marked as complete each time you hit the validator, even if there is no value.

I’m having exactly the same issue!

When the form is a ‘new’ object, there’s no problem as the user types anything and validation kicks in. However, on edit mode (using the same form), when inserting the value from db at form init, all fields with AsyncValidators are stuck on Pending status.

Drilling down a bit I found something very odd. AbstractControl.prototype.updateValueAndValidity runs this._runValidator() (sync) first. If I define my AsyncValidator as: this.name = new FormControl(values.name, Validators.composeAsync([this.svc.validate)]));

the updateValueAndValidity runs as the Synchronous validator, not as Async. The control itself also does not receive the AsyncValidator.

if I add the validation using this.name = new FormControl(values.name); this.name.setAsyncValidators(this.svc.validate);

then the AsyncValidator gets assigned and the updateValueAndValidity runs the this._runAsyncValidator(emitEvent) but the initial validation gets stuck on Pending status even though the return of the validation is valid.

Edit: This is with Angular v6.1.0, rxjs v6.0.0.

For my situation, where my component was using OnPush change detection strategy, I used ChangeDetectorRef with statusChanges to trigger an update:

@Component({
  selector: 'ws-create-form',
  templateUrl: './create-form.component.html',
  styleUrls: ['./create-form.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CreateFormComponent implements OnInit, OnDestroy {
  @Input() form: FormGroup;
  destroy: Subject<boolean> = new Subject<boolean>();

  constructor(private cdr: ChangeDetectorRef) {}

  ngOnInit() {
    this.form.statusChanges
      .pipe(
        takeUntil(this.destroy),
        distinctUntilChanged()
      )
      .subscribe(() => this.cdr.markForCheck());
  }

  ngOnDestroy() {
    this.destroy.next(true);
    this.destroy.complete();
  }
}

Same issue, using Angular 8

There are several things to note here:

  1. The main use case where developers would experience this issue is “edit” screens where the form is populated with data and initial validation is run.
  2. statusChanges are not emitted only when async validation is run during the initialization of the view (the formControlName and formGroup directives)
  3. Calling updateValueAndValidity() before formControlName and formGroup directives have been initialized will not fix the issue.
  4. Calling updateValueAndValidity() on a FormGroup will not automatically invoke the asyncValidator of a child FormControl (I guess this is by design).

So to me it seems that the most straightforward solution is to delay the first (only?) population of the form until the view has been initialized and form controls have been bound. A simple setTimeout of 0 should do the trick and will be called one time only (as opposed to other workarounds). Given we obtain the initial form value from an input, here is a simple workaround:

ngOnChanges(changes: SimpleChanges) {
  if (changes['initialFormValue']) {
    const delay = changes['initialFormValue'].isFirstChange() ? timer(0).toPromise() : Promise.resolve(null);

    delay.then(() => this.populateForm());
  }
}

based on @krimple sample code, I implemented mine like so:

private _validateNameNotTaken(control: AbstractControl) {
  if (!control.valueChanges || control.pristine) {
    return Observable.of( null );
  }
  else {
    return control.valueChanges
      .debounceTime( 300 )
      .distinctUntilChanged()
      .take( 1 )
      .switchMap( value => this.unitService.isNameTaken(value) )
      .do( () => control.markAsTouched() )
      .map( isTaken => isTaken ? { nameTaken: true } : null );
  }
}

mine has a few extras, like distinctUntilChanged(), and a hack at the end .markAsTouched() because I wanted to show the error message as it changes directly from the first time, and not only when the focus is out of the input.

but anyways, using debounce instead of debounceTime didn’t work for me, so I added || control.pristine in the first If statement. Since I’ll be assuming that the data that was previously saved is valid, then there’s no need to check when it’s just freshly loaded.

This won’t work if you’re loading invalid data from somewhere else.

I am seeing this exact problem also. My form never leaves pending. Angular 6.1.10 && rxjs 6.3.3 Here is my async validator

  private validateRestrictions(control: AbstractControl): Observable<ValidationErrors> {
    const statusID = control.get('status.ID').value;
    if (statusID !== 4) {
      // Only validate restrictions if the status is restricted
      return of(null).pipe(delay(250), take(1));
    }
    return control.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      take(1),
      tap(impl => console.log('validating restrictions async...', impl)),
      switchMap(impl => this.implementationService.checkRestrictionsAreUnique(impl)),
      map(isUnique => (isUnique) ? of(null) : of({ restrictionCombinationExists: true })),
      catchError(error => {
        console.error(error);
        return of(null);
      }),
    );
  }

Also, I extracted my observable into a local variable, and subscribed to it, to see what it emits. And know this observable completes. So I think there must be a bug in the way forms handle their async validators

    const obs$ = control.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      tap(impl => console.log('validating restrictions async...', impl)),
      switchMap(impl => this.implementationService.checkRestrictionsAreUnique(impl)),
      map(isUnique => (isUnique) ? null: ({ restrictionCombinationExists: true })),
      catchError(error => {
        console.error(error);
        return of(null);
      }),
      take(1),
    );

    obs$.subscribe(
      (x) => console.log('emitted: ', x),
      (error) => console.error('ERROR: ', error),
      () => console.log('EFFING COMPLETED')
    );

Yeah, that is a good idea!

On Sun, Aug 5, 2018 at 11:41 AM Thomas Jiang notifications@github.com wrote:

For my situation, where my component was using OnPush change detection strategy, I used ChangeDetectorRef with statusChanges to trigger an update:

@Component({ selector: ‘ws-create-form’, templateUrl: ‘./create-form.component.html’, styleUrls: [‘./create-form.component.scss’], changeDetection: ChangeDetectionStrategy.OnPush })export class CreateFormComponent implements OnInit, OnDestroy { @Input() form: FormGroup; destroy: Subject<boolean> = new Subject<boolean>();

constructor(private cdr: ChangeDetectorRef) {}

ngOnInit() { this.form.statusChanges .pipe( takeUntil(this.destroy) ) .subscribe(() => this.cdr.markForCheck()); }

ngOnDestroy() { this.destroy.next(true); this.destroy.complete(); } }

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/angular/angular/issues/13200#issuecomment-410528463, or mute the thread https://github.com/notifications/unsubscribe-auth/AAAQlpgjhzViZjNbx6_aR-5G8salLD-nks5uNxIigaJpZM4LCfCx .

– Ken Rimple Training and Mentoring Services Chariot Solutions 610-608-3935 @RimpleOnTech

I really struggled with this and subscribing to valueChanges wasn’t working with vamlumbers example.

I ended up with a validation function like this:

	validate = (control: AbstractControl): Observable<ValidationErrors> => {
		return Observable.timer(500).pipe(
			switchMap(() => this.nameExists(control.value)),
			take(1),
			map(isDuplicate => this.stateToErrorMessage(isDuplicate)),
			tap(() => control.markAsTouched()),
			first()
		);
	};

Returning an observable with the control.value prop.

The issue for me is that the value changes are fired before the async validation has completed and thus the last time ValueChanges was fired it has a Pending Status.

My workaround was to just combine the value changes with another observable that I fire once validation was completed.

Just make sure to use a BehaviourSubject for asyncValidationComplete$.

CombineLatest emits an item whenever any of the source Observables emits an item (so long as each of the source Observables has emitted at least one item).

 return control.valueChanges
        .debounceTime(500)
        .take(1)
        .switchMap(() => {
          return this.userService.usernameExists(username, this.user ? this.user.id : null);
        })
        .map(exists => {
          return exists ? {'forbiddenName': {value: control.value}} : null;
        })
        .finally(() => this.asyncValidationComplete$.next(true));
this.userForm.valueChanges
      .combineLatest(this.asyncValidationComplete$)

NOTE: I figured out a workaround for the debounceTime - just use debounce instead:

export function createAsyncDescriptionValidator(taskService: TaskService): AsyncValidatorFn {

  // NOTE : the take(1) and debounceTime currently wreak havoc on the pending state of the form.
  // This is a great academic exercise but you'd have to remove debounceTime in order to get it
  // to work properly. See https://github.com/angular/angular/issues/13200
  return (control: FormControl): Observable<{ invalidPattern: string} | null> => {
    // on initial load, forms sometimes do not have valueChanges available
    // until the initial edits... assumes that incoming data is valid
    if (!control.valueChanges) {
      return Observable.of(null);
    } else {
      return control.valueChanges
        .debounce(() => Observable.interval(1000))
        .take(1)
        .switchMap(value => taskService.validateDescription(value))
        .map(value => value.valid ? null : {invalidPattern: 'Your task cannot contain the word: poison!'});
    }
  };
}

(from one of my class exercises)

Using debounce over debounceTime seems to work fine. It doesn’t unnecessarily make the form pending and it clears as soon as it is done. Guarded my button with:

  <button class="btn btn-primary" 
       [disabled]="taskForm.invalid || (!taskForm.pristine && taskForm.pending)">Save</button>

I believe I found where the bug is:

  private _runAsyncValidator(emitEvent?: boolean): void {
    if (this.asyncValidator) {
      this._status = PENDING;
      const obs = toObservable(this.asyncValidator(this));
      this._asyncValidationSubscription =
          obs.subscribe((errors: ValidationErrors | null) => this.setErrors(errors, {emitEvent}));
    }
  }

the obs.subscribe does not assign the result of the observable into this._status causing the result to be always on PENDING.

I’ll try to do my first PR with this 😄