UPDATE: Issue with async validation successfully solved. But there is another issue with initial validation state. See latest answer.
Here is the trick:
- Have component with implemented ControlValueAccessor interface to be used as custom control.
- This component used as FormControl inside some reactive form.
- This custom control has async validator.
The problem:
Method validate() from ControlValueAccessor interface calls right after value change and do not wait async validator. Of course control is invalid and pending (because validation in progress) and main form also goes to be invalid and pending. Everything is okay.
But. When async validator finish to validate and return null (means value is valid) then custom control going to be valid and status changes to valid also, but parent from still invalid with pending status because validate() from value accessor haven't called again.
I have tried to return observable from the validate() method, but main form interprets it as error object.
I found workaround: propagate change event from custom control when async validator finish to validate. It's forcing main form to call validate() method again and get correct valid status. But it looks dirty and rough.
Question is: What have to be done to make parent form be managed by async validator from child custom control? Must say it works great with sync validators.
All project code can be found here: https://stackblitz.com/edit/angular-fdcrbl
Main form template:
<form [formGroup]="mainForm">
<child-control formControlName="childControl"></child-control>
</form>
Main form class:
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
@Component({
selector: "my-app",
templateUrl: "./app.component.html"
})
export class AppComponent implements OnInit {
mainForm: FormGroup;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.mainForm = this.formBuilder.group({
childControl: this.formBuilder.control("")
});
}
}
Custom child control template:
<div [formGroup]="childForm">
<div class="form-group">
<label translate>Child control: </label>
<input type="text" formControlName="childControl">
</div>
</div>
Custom child control class:
import { Component, OnInit } from "@angular/core";
import { AppValidator } from "../app.validator";
import {
FormGroup,
AsyncValidator,
FormBuilder,
NG_VALUE_ACCESSOR,
NG_ASYNC_VALIDATORS,
ValidationErrors,
ControlValueAccessor
} from "@angular/forms";
import { Observable } from "rxjs";
import { map, first } from "rxjs/operators";
@Component({
templateUrl: "./child-control.component.html",
selector: "child-control",
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: ChildControlComponent,
multi: true
},
{
provide: NG_ASYNC_VALIDATORS,
useExisting: ChildControlComponent,
multi: true
}
]
})
export class ChildControlComponent
implements ControlValueAccessor, AsyncValidator, OnInit {
childForm: FormGroup;
constructor(
private formBuilder: FormBuilder,
private appValidator: AppValidator
) {}
ngOnInit() {
this.childForm = this.formBuilder.group({
childControl: this.formBuilder.control(
"",
[],
[this.appValidator.asyncValidation()]
)
});
this.childForm.statusChanges.subscribe(status => {
console.log("subscribe", status);
});
}
// region CVA
public onTouched: () => void = () => {};
writeValue(val: any): void {
if (!val) {
return;
}
this.childForm.patchValue(val);
}
registerOnChange(fn: () => void): void {
this.childForm.valueChanges.subscribe(fn);
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
isDisabled ? this.childForm.disable() : this.childForm.enable();
}
validate(): Observable<ValidationErrors | null> {
console.log('validate');
// return this.taxCountriesForm.valid ? null : { invalid: true };
return this.childForm.statusChanges.pipe(
map(status => {
console.log('pipe', status);
return status == "VALID" ? null : { invalid: true };
}),
);
}
// endregion
}