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.
- Initial status of FormGroup and FormControl is "PENDING"
- Both status changes to "VALID" or "INVALID"
- statusChange is not emitted
Expected behavior
statusChange should always be emitted.
Minimal reproduction of the problem with instructions
- Create a FormGroup with the FormBuilder and add a FormControl with asyncValidator(e.g. with a TimeOut Promise of 2sec).
- 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 FormGroup
s. 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
:
angular/packages/forms/src/model.ts
Lines 446 to 468 in ec445b5
When the async validator is run, the observable is saved:
angular/packages/forms/src/model.ts
Lines 487 to 489 in ec445b5
If the async validator is called again (because updateValueAndValidity
has been called again), then the old observable is cancelled.
angular/packages/forms/src/model.ts
Line 451 in ec445b5
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 1
ms 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 1
ms 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(),
);
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
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();
}
});
}
I think I am facing this issue too.
Here is my Stackblitz : https://stackblitz.com/edit/angular-bkfctw?devtoolsheight=33&file=src/app/hello.component.ts
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.