angular / angular

Deliver web apps with confidence 🚀

Home Page:https://angular.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

FormGroup & FormControl statusChanges are not emitted on creation

MickL opened this issue · comments

I'm submitting a ...

[X] bug report
[ ] feature request
[ ] support request

Current behavior
FormGroup and FormControl don't emit statusChange after first creation (while untouched / pristine?) with asyncValidator.

  1. Initial status of FormGroup and FormControl is "PENDING"
  2. Both status changes to "VALID" or "INVALID"
  3. statusChange is not emitted

Expected behavior
statusChange should always be emitted.

Minimal reproduction of the problem with instructions

  1. Create a FormGroup with the FormBuilder and add a FormControl with asyncValidator(e.g. with a TimeOut Promise of 2sec).
  2. Subscribe to FormGroup.statusChange or FormGroup.controls.key.statusChange

Angular version
2.4.1

Pls provide a plunkr

https://plnkr.co/edit/BvnnfxHK3mgNVIVI7Yhs?p=preview

=> Emitted status is not set to "VALID" on first validation on init. Only if u start typing the status is emitted correctly.

This is not a bug, the type of statusChanges is an EventEmitter which is a class that extends Subject which does not emit an initial value. If you want it to emit then call the control.updateValueAndValidity() method after subscribing to get the initial value

I know there is no initial value that is fine. But then the status changes from "PENDING" to "VALID" or "INVALID" which is not emitted.

Thanks for the hint to use updateValueAndValidity(). This updates to "PENDING" but then still the status-change is not emitted as described above and would still be "PENDING". I updated the Plunkr to make the bug more obvious.

I also noticed the async validator is called 4 times on FormBuilder.group(). This would cause 4 server requests.

I updated my plunker, just check the console.

Opened another issue for this here: #14551

+1 to this. There would seem to be a need to modify angular/packages/forms/src/model.ts, and have statusChanges fire on a change of pending. The plunker linked above properly demonstrates this issue, which I'm also having in my project: no way to tell on initialization of the component whether or not the form has finished its pending status.

I encounter the same issue. On page load a canActivate method checks the status of my FormGroups. As I have some async validator functions, I have to wait until the status of my FormGroup is not PENDING anymore. This does never happen, due to the above mentioned bug.

As a workaround, I now dirty check the status (not so cool):

asyncCanActivate(form: FormGroup): Observable<boolean> {

    const result = new Subject();

    const interval = setInterval(() => {
      if (form.status === 'VALID') {
        result.next(true);
        result.complete();
        clearTimeout(timeout);
        clearInterval(interval);
      }
    }, PermissionService.STATUS_UPDATE_RATE);

    const timeout = setTimeout(() => clearInterval(interval), PermissionService.MAX_UPDATE_INTERVAL);

    return result;
}

I worked around this in my async validator by calling the parent control's updateValueAndValidity on the next tick after the asynchronous call completes.

This causes the parent control (the FormGroup) to emit a VALID status, but it doesn't cause the control itself to emit anything. If you call the control's updateValueAndValidity, that makes the async validator run again, which isn't ideal already, and worse, it creates an infinite loop. So if you need the control itself to emit, not its parent, then this doesn't help much.

  console.log('myAsyncValidator is called!');
  
  return new Promise (resolve => {
    setTimeout(() => {
      console.log('myAsyncValidator finished!')
      setTimeout(() => c.parent.updateValueAndValidity()) // Workaround
      if(c.value != '123') {
        resolve({"isNot123": true});
      }
      else
        resolve(null)
    }, 2500);
  });
}

Here's a fork of the plunker: https://plnkr.co/edit/QykVJrgM8j3EA51VU9Xl?p=preview

I guess I also don't know how we're supposed to be handling this:

I have a directive that in ngOnInit() will subscribe to statusChanges, and when the state switches between Valid/Invalid it'll render the validation message appropriately.

This has been working fine since the async validator runs as the user changes the fields; however, I've run into a situation where I'm async validating fields as they're created in the form group (the form is created based on an existing object that may be invalid).

In my directive I can see the initial state of the control is 'PENDING', but once that completes my statusChanges subscription is not hit: is the only resolution to do some sort of setTimeout hack if it's initially pending and wait for that to complete?

edit

Here's my workaround for now:

           this.formControl.statusChanges.subscribe((a) => {
                this.statusChanged(a);
            });

            //TODO: remove this at some point
            if (this.formControl.pending) {
                let setInitialStatus = () => {
                    if (this.formControl.pending) {
                        t = setTimeout(setInitialStatus, 50);
                    } else {
                        this.statusChanged(this.formControl.valid ? 'VALID' : 'INVALID');
                        clearTimeout(t);
                    }
                };

                let t = setTimeout(setInitialStatus, 50);
            }

Any update on this?

This is an issue we'd love to see fixed for GCP as well, in particular the partial case: #20314

@naomiblack Are you using a workaround for the moment which you could let us know about?

I've looked into this as I really needed a fix.

The only place that the async validator is executed from is updateValueAndValidity:

updateValueAndValidity(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._setInitialStatus();
this._updateValue();
if (this.enabled) {
this._cancelExistingSubscription();
(this as{errors: ValidationErrors | null}).errors = this._runValidator();
(this as{status: string}).status = this._calculateStatus();
if (this.status === VALID || this.status === PENDING) {
this._runAsyncValidator(opts.emitEvent);
}
}
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter<any>).emit(this.value);
(this.statusChanges as EventEmitter<string>).emit(this.status);
}
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts);
}
}

When the async validator is run, the observable is saved:

const obs = toObservable(this.asyncValidator(this));
this._asyncValidationSubscription =
obs.subscribe((errors: ValidationErrors | null) => this.setErrors(errors, {emitEvent}));

If the async validator is called again (because updateValueAndValidity has been called again), then the old observable is cancelled.

this._cancelExistingSubscription();

There is a parameter, emitEvent, which defaults to true when not provided. Some internal functions set this to false when calling updateValueAndValidity. emitEvent indicates whether events should be emitted from statusChanges and valueChanges observables.

This means that if an internal function calls updateValueAndValidity soon after you've called it and this internal function has set emitEvent to false, no event will be emitted to the statusChanges observable.

On loading the application, it seams that a few internal functions call updateValueAndValidity with emitEvent set to false, hence the problem people seam to be having.

I don't know how to fix this. I don't know the angular code base and any fix I attempt will probably be considered a hack.

My fix for this is using setTimeout with a 1ms delay and manually calling updateValueAndValidity within the timeout. This means that all the internal calls to updateValueAndValidity have been made and the final call to this method is mine (which doesn't have emitEvent set to false). The internal calls to updateValueAndValidity seem to be made when the form is shown. So if you have an *ngIf, the setTimeout needs to be done at whatever point the ngIf evaluates to true.

For example I have an ngIf on my form, which is set to someObservable | async. In my component code I have this:

this.someObservable = this.someService.getSomeData();

this.someObservable.subscribe(() => {
  setTimeout(() => {
    this.someForm.get("someField").updateValueAndValidity();
  }, 1);
});

tl;dr

My suggestion (and what I am doing at the moment) is using setTimeout with about a 1ms delay and manually calling updateValueAndValidity within the timeout. The setTimeout needs to be done after the form is shown. So if you are using an ngIf, the timeout needs to be done after this evaluates to true.

I've run into the same error, and @kiancross' workaround with setTimeout seems to work for now, but it's really ugly and definitely needs a fix!

The setTimeout solution does appear to be a temp fix but it's ugly and shouldn't be required. Please fix!

The cleanest way I found to circumvent this issue is as follows:

// Workaround
this.form.statusChanges.pipe(
    // @todo Temporary async validator fix until
    // https://github.com/angular/angular/issues/13200 and
    // https://github.com/angular/angular/issues/14542 are addressed.
    merge(interval(250).pipe(map(() => this.form.status))),
    distinctUntilChanged(), // Don't keep emitting the same status
).subscribe(s => {
});

It allows you to just remove the merge operator when this issue is fixed and your code will keep working.

I also made this lettable operator to reuse this temporary fix.

import { FormGroup } from '@angular/forms';

import { MonoTypeOperatorFunction } from 'rxjs/interfaces';
import { interval } from 'rxjs/observable/interval';
import { distinctUntilChanged, map, merge } from 'rxjs/operators';

// @todo Temporary async validator fix until
// https://github.com/angular/angular/issues/13200 and
// https://github.com/angular/angular/issues/14542 are addressed.
export const fixFormStatus: (form: FormGroup) => MonoTypeOperatorFunction<string> =
    form => source => source.pipe(
        merge(interval(250).pipe(map(() => form.status))),
        distinctUntilChanged(),
    );
commented

Any update on this?

I've created a library to temporarily fill the hole. https://www.npmjs.com/package/angular-form-status-workaround

I think I had same issue - if I set a value to form programmatically, async validator is used, but statusChanges is never fired... The async validator should work for both input types - user input and programmatic input.
I don't like solution with intervals, so here is mine, maybe it will be helpful for somebody:

createUniqueValidator(): AsyncValidatorFn {
    const keeper = new Subject(); // this helps to prevent triggering multiple validations
    return (control: AbstractControl): Observable<{ [key: string]: any } | null> => {
      keeper.next();
      return timer(700) // wait 700 ms to start validation (user finishes typing)
        .pipe(
          filter(() => !!(control.value)), // do not validate null values
          takeUntil(keeper), // if keeper running, skip this validation
          switchMap( () => this.service.checkUnique(control.value), // call backend validation
          map(found => notUnique ? {'UNIQUE' : true} : null), // map backend result (in my case boolean) to json with errors
          finalize(() => control.setErrors(control.errors)) // this stupid call is the the trick which triggers statusChanges, status of control is changed from PENDING to VALID / INVALID
        );
    }
  }

v2.41 - v9.0.0
angular team no plan to fix it ?

I also have this problem.
Dear Angular team, could you please fix it 😍

The original plunkr doesn't work anymore. I copied the code to a new stackblitz: https://stackblitz.com/edit/angular-tv8yss

Hi, it looks like this issue is related to #20424 and might be fixed by #20806. Thank you.

I have similar issue when user does not input anything and click submit button, my validators do not fire error messages when the message component is relying on listening the input's statusChanges.

My solution is calling the following function when the submit button is clicked, and it works:

    validateAllFields(formGroup: FormGroup) {
        Object.keys(formGroup.controls).forEach(field => {
            const control = formGroup.get(field);
            if (control instanceof FormControl) {
                control.markAsTouched({ onlySelf: true });
                control.updateValueAndValidity();
            }
        });
    }

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.