Reputation: 175
steps to reproduce:
Now, the question: why name control changes its status? How updateValueAndValidity() in hello.component for unrelated control affects what happens with name control status?
From the docs of updateValueAndValidity it should update only control and its ancestors, not siblings... Please, help me to understand this.
Upvotes: 5
Views: 6647
Reputation: 346
I have this solution for wait asyncValidatos:
const asyncValidationObservables = this.formUtilsService.validateRecursiveForm(this.form);
this.formUtilsService.getObservableValidations(asyncValidationObservables).subscribe((results) => {
if (this.form.valid) {
this.movimientoValidado.emit(this.form.value);
}
});
validateRecursiveForm(abstractControl: FormControl | FormGroup | FormArray | AbstractControl, emitEvent = true): any[] {
const asyncValidations: any[] = [];
if (abstractControl instanceof FormGroup || abstractControl instanceof FormArray) {
for (const control in abstractControl.controls) {
const currentControl = abstractControl.get(control);
const validaciones = this.validateRecursiveForm(currentControl, emitEvent);
if (validaciones.length > 0) {
asyncValidations.concat(validaciones);
}
// Iteramos hacia dentro antes de llamar a las validaciones del padre
console.log('===> asyncValidations ', asyncValidations);
this.validateAbstractControl(currentControl, asyncValidations, emitEvent);
console.log('===> asyncValidations ', asyncValidations);
}
} else if (abstractControl instanceof FormControl) {
console.log('===> abstractControl ', abstractControl);
this.validateAbstractControl(abstractControl, asyncValidations, emitEvent);
}
console.log('===> asyncValidations ', asyncValidations);
return asyncValidations;
}
private validateAbstractControl(
abstractControl: FormControl < any > | FormGroup < any > | FormArray < any > | AbstractControl < any > ,
asyncValidations: any[],
emitEvent: boolean
) {
if (abstractControl.asyncValidator) {
abstractControl.markAsTouched();
asyncValidations.push(abstractControl.asyncValidator(abstractControl));
} else if (abstractControl.validator) {
abstractControl.markAsTouched();
abstractControl.updateValueAndValidity({
emitEvent: emitEvent
});
}
}
getObservableValidations(asyncValidationObservables) {
return (asyncValidationObservables ? .length > 0 ? forkJoin(asyncValidationObservables) : of (null)).pipe(take(1));
}
Upvotes: 0
Reputation: 86
The cause of the behavior that you are noticing is very subtle and not documented.
Reactive Forms are built using the FormControl
, FormArray
, and FormGroup
objects. These classes expose the ability to define and execute validators, disable controls, and change values. If you never bind any of these controls to the form using a directive ([formControl]
, [formControlName]
, [formGroup]
, etc.) you will see the desired/expected behavior from the validators.
However, if you have async validators and bind your controls to the form while those validators are running they will become "stuck" in PENDING
. The underlying status
will be updated upon completion of the async validator but the statusChanges
subject will not emit an event until some other action causes it to run.
This happens in the binding of the directives used above. Like all directives in Angular, the ngOnChanges
hook will be called on instantiation. This is the the implementation of ngOnChanges
for form_group_directive.ts
.
this._checkFormPresent();
if (changes.hasOwnProperty('form')) {
this._updateValidators();
this._updateDomValue();
this._updateRegistrations();
this._oldForm = this.form;
}
The important line above is this._updateDomValue();
. The call to updateDomValue()
will in turn call this.form._updateTreeValidity({emitEvent: false});
. The call to _updateTreeValidity()
will then call this.updateValueAndValidity({onlySelf: true, emitEvent: opts.emitEvent});
. The call to updateValueAndValidity()
will then do the following:
this._setInitialStatus();
this._updateValue();
if (this.enabled) {
this._cancelExistingSubscription();
(this as {errors: ValidationErrors | null}).errors = this._runValidator();
(this as {status: FormControlStatus}).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<FormControlStatus>).emit(this.status);
}
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts);
}
There is a lot going on here, but here are the important parts. First, the call to this._cancelExistingSubscription()
. This will cancel any currently running async validators, leaving the control in PENDING
status. Next, the call to this._runAsyncValidator(opts.emitEvent)
will be called. This will trigger the async validator to run again with an important change. The opts.emitEvent
value passed to the function will now be false
which can be seen from the earlier call to _updateTreeValidity()
. Since this new execution of the validators will not emit an event. The validator will complete, set status
to the appropriate result, but without an event you will not receive that status change from statusChanges
.
This single example is demonstrative of what causes the issue that you are seeing. If at any time updateValueAndValidity()
can be called with emitEvent = true
the statusChanges
subject will get "stuck" in PENDING
.
Now for the specifics of your issue.
why name control changes its status?
I haven't debugged completely through your example, the above example is one cause of what you are seeing. Below could be another:
this.form.setControl(FormFields.Documents, control);
in app.component.ts
.setControl()
calls this._onCollectionChange()
. The [formGroup]
directive sets this to this._updateDomValue()
._updateDomValue()
calls this.form._updateTreeValidity({emitEvent: false});
. This is the same in my example above. Notice that emitEvent
is false
._updateTreeValidity()
calls _updateTreeValidity()
for all its children. This includes your "name" control.How updateValueAndValidity() in hello.component for unrelated control affects what happens with name control status?
I don't think that this affecting the "name" control status. I see console output for this.form.valueChanges
, but not for this.form.get(FormFields.Name).statusChanges
. The subscription to valueChanges
is outputting the status for the "name" control, but that is because the form's value has changed. The validator is never running because it is using valueChanges
which is why it is staying in PENDING
. This may or may not be what you want. Async validator already trigger on value changes of the input. If you want to ensure that it doesn't make too many HTTP calls I might suggest using timer()
from rxjs or setting updateOn
to blur
.
Example of timer:
nameValidatorAsync(): AsyncValidatorFn {
return (control) =>
timer(300).pipe(
switchMap(() =>
this.http.get(FAKE_URL).pipe(map(() => Math.random() > 0.5))
),
map((isValid) => (isValid ? null : { invalidName: true })),
first()
);
}
Example of updateOn
...
[FormFields.Name]: ['', {
validators: [],
asyncValidators: [this.nameValidatorAsync()],
updateOn: 'blur'
}]
...
I don't have any great answers on how to work around these items. I also don't think that they will be fixed any time soon. This ticket has some suggestions for creating an observable for status that may help you.
Upvotes: 7