これがトリックです:
問題:
ControlValueAccessorインターフェースのメソッドvalidate()は、値が変更された直後に呼び出し、非同期バリデーターを待機しません。もちろん、コントロールは無効で保留中です(検証が進行中のため)。メインフォームも無効で保留中になります。すべてが大丈夫です。
だが。非同期バリデーターが検証を終了してnullを返すと(値は有効であることを意味します)、カスタムコントロールは有効になり、ステータスも有効になりますが、値アクセサーのvalidate()が再度呼び出されていないため、親はまだ無効から保留中のステータスになっています。
Validate()メソッドからオブザーバブルを返そうとしましたが、メインフォームはそれをエラーオブジェクトとして解釈します。
回避策が見つかりました:非同期バリデーターが検証を終了したときに、カスタムコントロールから変更イベントを伝達します。メインフォームにvalidate()メソッドを再度呼び出し、正しい有効なステータスを取得するように強制しています。しかし、汚れて荒れているように見えます。
質問は次のとおりです:親フォームを子カスタムコントロールから非同期バリデーターで管理できるようにするにはどうすればよいですか?同期バリデーターとの相性は抜群です。
すべてのプロジェクトコードはここにあります: https://stackblitz.com/edit/angular-fdcrbl
メインフォームテンプレート:
<form [formGroup]="mainForm">
<child-control formControlName="childControl"></child-control>
</form>
メインフォームクラス:
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("")
});
}
}
カスタムの子コントロールテンプレート:
<div [formGroup]="childForm">
<div class="form-group">
<label translate>Child control: </label>
<input type="text" formControlName="childControl">
</div>
</div>
カスタムの子コントロールクラス:
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
}
私はさまざまなアプローチやトリックを試しました。しかし、AndreiGătejがメインフォームについて言及したように、子コントロールの変更を解除します。
私の目標は、カスタムコントロールを独立させ、検証をメインフォームに移動しないことです。それは私に白髪のペアを要しましたが、私は許容できる回避策を見つけたと思います。
子コンポーネントの検証関数内のメインフォームから制御を渡し、そこで有効性を管理する必要があります。実際には、次のようになります。
validate(control: FormControl): Observable<ValidationErrors | null> {
return this.childForm.statusChanges.pipe(
map(status => {
if (status === "VALID") {
control.setErrors(null);
}
if (status === "INVALID") {
control.setErrors({ invalid: true });
}
// you still have to return correct value to mark as PENDING
return status == "VALID" ? null : { invalid: true };
}),
);
}
問題は、_childForm.statusChanges
_が発生するまでに、非同期バリデーターのサブスクリプションが既にキャンセルされていることです。
これは、_childForm.valueChanges
_がbefore_childForm.statusChanges
_を発行するためです。
_childForm.valueChanges
_が発生すると、登録されているonChanged
コールバック関数が呼び出されます。
_registerOnChange(fn: () => void): void {
this.childForm.valueChanges.subscribe(fn);
}
_
これにより、FormControl(controlChild
)はその値を更新します
_function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
/* ... */
if (control.updateOn === 'change') updateControl(control, dir);
});
}
// Update the MODEL based on the VIEW's value
function updateControl(control: FormControl, dir: NgControl): void {
/* ... */
// Will in turn call `control.setValueAndValidity`
control.setValue(control._pendingValue, {emitModelToViewChange: false});
/* ... */
}
_
updateValueAndValidity
に到達することを意味します:
_updateValueAndValidity(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._setInitialStatus();
this._updateValue();
if (this.enabled) {
this._cancelExistingSubscription(); // <- here the existing subscription is cancelled!
(this as{errors: ValidationErrors | null}).errors = this._runValidator(); // Sync validators
(this as{status: string}).status = this._calculateStatus(); // VALID | INVALID | PENDING | DISABLED
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);
}
}
_
私が思いついたアプローチでは、カスタムコンポーネントがControlValueAccesor
およびAsyncValidator
インターフェースの実装をスキップできます。
_<form [formGroup]="mainForm">
<child-control></child-control>
</form>
_
_ ngOnInit() {
this.mainForm = this.formBuilder.group({
childControl: this.formBuilder.control("", null, [this.appValidator.asyncValidation()])
});
}
_
_<div class="form-group">
<h4 translate>Child control with async validation: </h4>
<input type="text" formControlName="childControl">
</div>
_
_@Component({
templateUrl: "./child-control.component.html",
selector: "child-control",
providers: [
],
viewProviders: [
{ provide: ControlContainer, useExisting: FormGroupDirective }
]
})
export class ChildControlComponent
implements OnInit { /* ... */ }
_
ここでの核心は、viewProviders
内に_{ provide: ControlContainer, useExisting: FormGroupDirective }
_を提供することです。
これは、FormControlName
内で、_@Host
_デコレータを使用して親の抽象コントロールが取得されるためです。これにより、取得する_@Host
_デコレーター(この場合はviewProviders
)で宣言されている依存関係をControlContainer
内で指定できます。FormControlName
は次のように取得します@Optional() @Host() @SkipSelf() parent: ControlContainer