关键词:
【中文标题】Angular 反应式表单自定义控件异步验证【英文标题】:Angular reactive form custom control async validation 【发布时间】:2020-04-06 08:48:11 【问题描述】:更新: 异步验证问题已成功解决。但是初始验证状态还有另一个问题。 查看最新答案。
诀窍如下:
具有实现 ControlValueAccessor 接口的组件用作自定义控件。 此组件在某些反应式表单中用作 FormControl。 此自定义控件具有异步验证器。问题:
来自 ControlValueAccessor 接口的方法 validate() 在值更改后立即调用,并且不等待异步验证器。当然,控制是无效的和未决的(因为正在进行验证),主窗体也将是无效的和未决的。一切正常。
但是。当异步验证器完成验证并返回 null (意味着值有效)时,自定义控件将有效并且状态也更改为有效,但父级仍然无效并处于挂起状态,因为来自值访问器的 validate() 没有再次调用。
我试图从 validate() 方法返回 observable,但主窗体将其解释为错误对象。
我找到了解决方法:当异步验证器完成验证时,从自定义控件传播更改事件。它迫使主窗体再次调用 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 不,不幸的是问题仍然悬而未决:( 那么,没有任何解决方案可以在子组件中使用异步验证器吗? angular github repo 上是否有任何官方问题? 【参考方案1】:问题是,当childForm.statusChanges
发出时,异步验证器的订阅已经被取消了。
这是因为childForm.valueChanges
发出之前 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 aserrors: ValidationErrors | null).errors = this._runValidator(); // Sync validators
(this asstatus: 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
接口。
app.component.html
<form [formGroup]="mainForm">
<child-control></child-control>
</form>
### app.component.ts
ngOnInit()
this.mainForm = this.formBuilder.group(
childControl: this.formBuilder.control("", null, [this.appValidator.asyncValidation()])
);
child-control.component.html
<div class="form-group">
<h4 translate>Child control with async validation: </h4>
<input type="text" formControlName="childControl">
</div>
child-control.component.ts
@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
装饰器的帮助下检索的。这允许您在 viewProviders
内部指定要使用 @Host
装饰器声明的依赖项(在本例中为 ControlContainer
)。FormControlName
像这样检索它 @Optional() @Host() @SkipSelf() parent: ControlContainer
StackBlitz.
【讨论】:
【参考方案2】:我尝试了不同的方法和技巧。但是正如 Andrei Gă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 ;
),
);
【讨论】:
实现了你的方法,但父表单在初始化时处于pending
状态(直到它没有被触及)。之后它可以完美运行。似乎childForm.statusChanges
在第一次从 Pending 更改为 Valid 时没有发出值。此处示例:stackblitz.com/edit/angular-bnub6s.【参考方案3】:
有了这样的验证,一切都很容易。只需要在末尾添加 first()/take(1) 即可关闭 observable 并完成订阅。
validate(): Observable<ValidationErrors | null>
return this.childForm.statusChanges.pipe(
filter((status) => status !== 'PENDING'),
map(status =>
return status == "VALID" ? null : invalid: true ;
),
first(),
);
但还有另一个问题。
statusChange 没有在 init 组件上传播状态更改。 Validate 方法已按预期调用,statusChange observable 返回,但异步验证完成后没有任何反应。因此父表单仍处于 PENDING 状态。
但如果您手动执行此操作(只需更改输入值),则 validate 方法中的 statusChange 会正确触发并返回正确的状态。
如果能在这方面得到一些帮助会很好。
这里是更新的代码示例:https://stackblitz.com/edit/angular-cva-async-validation
【讨论】:
这个问题让我发疯了很长时间。当我将updateValueAndValidity
添加到您的writeValue
方法时,我得到了您的示例,如下所示:writeValue(val: any): void if (!val) return; this.childForm.patchValue(val); setTimeout(() => this.childForm.updateValueAndValidity() , 10)
但这仍然很hacky。
@Daco 是的,有一个技巧。状态没有对初始表单值进行更改,因为模板还没有准备好并且没有订阅适用。超时可以解决这个问题,但我建议使用 ChangeDetectorRef 进行更改检测,然后应用新值。您可以在示例 stackblitz.com/edit/… 第 23 行中看到它是如何工作的【参考方案4】:
表单可能会忽略初始值并卡在 PENDING 直到值更改。
发生这种情况是因为状态尚未对初始表单值进行更改,因为模板尚未准备好并且没有订阅适用。
这个问题可以通过 ChangeDetectorRef 解决。 定义表单,检测更改并在此之后应用新值。您可以在示例 https://stackblitz.com/edit/angular-cva-async-validation?file=src%2Fapp%2Fapp.component.ts 第 23 行中看到它是如何工作的
this.mainForm = this.formBuilder.group(
mainControl: this.formBuilder.control(''),
);
// require to set value after view ready
// otherwise initial state of form will freeze on PENDING
this.changeDetector.detectChanges();
this.mainForm.setValue(
mainControl:
childControl: 'test',
,
);
【讨论】:
【参考方案5】:为了完整起见,我也在此处添加我的other answer。该方法略有不同,因为它适用于表单的自定义子控件上的 Directive
。
我还在那里放置了指向完全正常工作的Stackblitz 的链接。
【讨论】:
【参考方案6】:我想我会发布另一个解决方案,我在尝试了这里发布的各种其他解决方案后最终使用了它,以防它可能对某人有所帮助。
在我的例子中,我有多个嵌套的复杂 CVA 组件(4 层深),每个组件都有自己的验证规则,并且在初始化表单时父组件的验证状态没有获取子组件的验证状态(一旦表单是编辑一切都很好)。这是因为父 CVA 组件的验证功能在被检查的 FormGroup/FormArray
的控件由子 CVA 组件验证之前运行。
要使父组件的有效性状态与其子组件保持同步,请跟踪上一次报告的有效状态是由父组件决定的。然后实现DoCheck接口,如果最后上报的有效状态与当前状态不匹配,则调用onChanges通知父Component。
ngDoCheck()
if (this.lastReportedValidState !== this.form.valid)
this.onChanged(this.form.value);
registerOnChange(fn: any): void
this.onChanged = fn;
this.form.valueChanges.subscribe(fn);
validate(control: AbstractControl): ValidationErrors | null
this.lastReportedValidState = this.form.valid;
return this.form.invalid ? invalid: true : null;
【讨论】:
使用自定义验证和动态值的 Angular 表单
...1-04-2501:21:21【问题描述】:我正在尝试创建一个自定义的反应式表单验证,它允许我传递一个数据数组来检查一个字符串是否已经存在。我能够以一种方式做到这一点,因此这将是一个表单级别的验证,但我无法让它在单个表单... 查看详情
Angular 2 - 自定义表单控件 - 禁用
...中使用它时,一切正常。但是当我使用模型驱动的方法(反应式表单)时,表单控件上的disable()方法似乎 查看详情
Angular 反应式表单自定义验证器。仅在选中复选框时启用验证
】Angular反应式表单自定义验证器。仅在选中复选框时启用验证【英文标题】:Angularreactiveformscustomvalidator.EnableValidationonlywhencheckboxchecked【发布时间】:2019-02-2110:27:50【问题描述】:我在验证仅标记的复选框时遇到问题。首先,... 查看详情
如何使用 Angular 重置自定义表单控件
】如何使用Angular重置自定义表单控件【英文标题】:HowcanIresetacustomformcontrolwithAngular【发布时间】:2018-11-0921:54:04【问题描述】:我有一个带有验证的自定义表单控件。我在其中使用了一个独立的FormControl来处理值和一些验证。... 查看详情
FormArray 字段的角度自定义验证(反应式表单)
】FormArray字段的角度自定义验证(反应式表单)【英文标题】:AngularcustomvalidationforFormArrayfields(ReactiveForm)【发布时间】:2022-01-0212:57:53【问题描述】:我是Angular新手,想了解如何为FormArray执行自定义字段验证?FormArray是动态的... 查看详情
反应形式自定义验证器未在 angular6 中显示错误
】反应形式自定义验证器未在angular6中显示错误【英文标题】:reactiveformcustomvalidatorsnotdisplayingtheerrorinangular6【发布时间】:2018-12-2903:41:19【问题描述】:在这个演示中,我为自定义验证器创建了一个示例表单,在这里,我有Fromd... 查看详情
Angular 6. 如何在创建的自定义控件中赋予验证状态这个控件?
】Angular6.如何在创建的自定义控件中赋予验证状态这个控件?【英文标题】:Angular6.HowinthecreatedCustomControltogivenValidatorstatethiscontol?【发布时间】:2018-12-2509:51:48【问题描述】:我为表单创建了一个自定义控件。我的控件是这样的... 查看详情
Angular 11 自定义 ISBN 验证器响应式表单
】Angular11自定义ISBN验证器响应式表单【英文标题】:Angular11CustomISBNValidatorReactiveForms【发布时间】:2022-01-2322:09:59【问题描述】:我正在对ISBN号码进行自定义验证,我已经拥有检查号码并完美运行的功能,可以通过控制台为我... 查看详情
Angular2- RC6 自定义表单控件不起作用
】Angular2-RC6自定义表单控件不起作用【英文标题】:Angular2-RC6CustomFormControlsNotworking【发布时间】:2017-01-1710:02:25【问题描述】:我已将Angular2RC5应用程序更新为RC6。我根据Thoughtram的this教程开发了一些自定义表单控件。在RC5之前... 查看详情
具有响应式表单的自定义控件
...【发布时间】:2019-11-1503:24:46【问题描述】:我正在使用Angular7,带有反应形式的AngularMaterial控件。我使用带有mat-form-field的AngularMaterial创建了自定义文本(matInputtype="text")、数字(matInputtype="number")、选择(mat 查看详情
Angular2自定义表单控件防止发射事件
】Angular2自定义表单控件防止发射事件【英文标题】:Angular2customformcontrolpreventemittingevent【发布时间】:2017-06-2423:40:12【问题描述】:在某些情况下,当我更新Angular2表单控件时,我不想触发valueChanges事件(例如,因为更改来自... 查看详情
Angular 2 - 单元测试绑定到嵌套的自定义表单控件
】Angular2-单元测试绑定到嵌套的自定义表单控件【英文标题】:Angular2-Unittestbindingtonestedcustomformcontrol【发布时间】:2018-01-1723:39:29【问题描述】:我有一个带有选择器app-date-picker的自定义控件。它实现了ControlValueAccessor。我有一... 查看详情
angular5+自定义表单验证器
Angular5+自定义表单验证器CustomValidators标签(空格分隔):Angular首先阐述一下遇到的问题:怎样实现“再次输入密码”的验证(两个controller值相等)(equalTo)怎样反向监听(先输入“再次输入密码”,后输入设置密码)解决思... 查看详情
Angular 2 自定义表单输入
】Angular2自定义表单输入【英文标题】:Angular2customforminput【发布时间】:2016-04-2904:28:29【问题描述】:如何创建与原生<input>标签一样的自定义组件?我想让我的自定义表单控件能够支持ngControl、ngForm、[(ngModel)]。据我... 查看详情
angular自定义验证器添加入模板驱动表单
创建自定义验证器的命令nggeneratedirectiveforbidden-name(自定义床啊金验证器的名称)生成的文件内容import{Directive}from'@angular/core';@Directive({selector:'[appForbiddenName]'})exportclassForbiddenNameDirective{const 查看详情
如何在 Angular 的反应式表单中设置表单控件的值
】如何在Angular的反应式表单中设置表单控件的值【英文标题】:HowtosetvaluetoformcontrolinReactiveFormsinAngular【发布时间】:2019-08-1123:43:55【问题描述】:大家好,我是Angular的新手。实际上,我正在尝试从服务订阅数据,并且该数据... 查看详情
Angular 2:迭代反应式表单控件
】Angular2:迭代反应式表单控件【英文标题】:Angular2:Iterateoverreactiveformcontrols【发布时间】:2017-07-0306:35:03【问题描述】:我想markAsDirtyFormGroup内的所有控件。【问题讨论】:【参考方案1】:发现Object.keys可以处理这个..Object.keys... 查看详情
Angular 5 动态表单中的自定义验证器
】Angular5动态表单中的自定义验证器【英文标题】:CustomvalidatorinAngular5dynamicforms【发布时间】:2018-05-2810:54:18【问题描述】:我正在从Angular5中的可配置json创建一个动态表单。一切正常,但我无法为特定字段添加自定义验证器。... 查看详情