如何解决嵌套反应式表单组件的变更检测问题?
How to solve a change detection issue with nested reactive form components?
我有以下(简化的)场景:
- 具有(反应式)表单的组件,其中嵌套组件充当表单控件
- 其中一个嵌套组件称为
SelectComponent
,它提供了 <select>
标签
SelectComponent
实现了 NG_VALUE_ACCESSOR
和 NG_VALIDATORS
- 父组件提供 select 选项作为
SelectComponent
的输入字段
到目前为止效果很好,但是当我想根据以下内容自动更新内部 SelectComponent
中的行为时遇到问题:
- 如果有多个选项或者不需要控件,什么也不做
- 如果只有一个选项并且需要控件,则自动 select(唯一的)选项。
为了实现上述目标,我实施了 ngOnChanges
事件。
这是 Stackblitz 上的一个几乎可以正常工作的最小示例:
https://stackblitz.com/edit/angular-ohdb71?file=src%2Fapp%2Fselect%2Fselect.component.ts
但是,我在主要组件的变化检测方面遇到了问题。正如您所看到的,主组件不会自动将 user.value
更新为 Walter,即使内部 SelectComponent
已经设置了这个值并且也向外部组件宣布了它。因此,主要组件认为该值未设置,并将表单控件标记为错误(必需)。
奇怪的是,每当我将代码 <app-select [formControlName]="'user'" [options]="userOptions"></app-select>
移动到文件顶部时,代码都能正常工作(没有 ExpressionChangedAfterItHasBeenCheckedError
)。
如何解决 SelectComponent
中的这个问题,使外部组件不必再实现任何逻辑?
TLDR:您可以在此处找到您的工作示例:https://stackblitz.com/edit/angular-f8fiux
要让它发挥作用,需要完成几个步骤:
修改writeValue方法。 writeValue 是自定义组件的 input,所以它不应该调用 onChange方法(即组件的output),否则会导致不正确的交互。
writeValue(value: any): void {
this.selectFormControl.setValue(value, { emitEvent: false });
}
这里传递emitEvent: false
是为了不触发selectFormControl
的变化事件。
为了理解值访问器的逻辑,我推荐 this article.
修改ngOnChange方法。它应该设置组件的内部值(通过 writeValue)然后为外部消费者发出它(通过 onChange)。对于 onChange 你必须安排微任务(Promise.resolve)或宏任务(setTimeout),否则数据将在相同的变化检测周期中发出,导致 ExpressionChangedAfterItHasBeenCheckedError
ngOnChanges(simpleChanges: SimpleChanges): void {
if (simpleChanges.options) {
if (this.options.length === 1) {
if (this.selectFormControl.value !== this.options[0]) {
this.writeValue(this.options[0]);
setTimeout(() => {
this.onChange(this.options[0]);
});
}
} else {
this.writeValue(null);
}
}
}
最好不要混用 Reactive Forms 和 Template-driven forms,所以我建议你去掉模板中的 (ngModelChange)="onChange($event)"
因为它是 异步数据流 并且需要更复杂的处理。取而代之的是,您可以在构造函数中添加一个简单的订阅(不要忘记取消订阅,这里我省略了)
constructor(private cdr: ChangeDetectorRef) {
this.selectFormControl.valueChanges.subscribe((value) => {
this.onChange(value);
});
}
您可以在 Angular docs
中找到有关 async 和 sync 流程的详细信息
我有以下(简化的)场景:
- 具有(反应式)表单的组件,其中嵌套组件充当表单控件
- 其中一个嵌套组件称为
SelectComponent
,它提供了<select>
标签 SelectComponent
实现了NG_VALUE_ACCESSOR
和NG_VALIDATORS
- 父组件提供 select 选项作为
SelectComponent
的输入字段
到目前为止效果很好,但是当我想根据以下内容自动更新内部 SelectComponent
中的行为时遇到问题:
- 如果有多个选项或者不需要控件,什么也不做
- 如果只有一个选项并且需要控件,则自动 select(唯一的)选项。
为了实现上述目标,我实施了 ngOnChanges
事件。
这是 Stackblitz 上的一个几乎可以正常工作的最小示例: https://stackblitz.com/edit/angular-ohdb71?file=src%2Fapp%2Fselect%2Fselect.component.ts
但是,我在主要组件的变化检测方面遇到了问题。正如您所看到的,主组件不会自动将 user.value
更新为 Walter,即使内部 SelectComponent
已经设置了这个值并且也向外部组件宣布了它。因此,主要组件认为该值未设置,并将表单控件标记为错误(必需)。
奇怪的是,每当我将代码 <app-select [formControlName]="'user'" [options]="userOptions"></app-select>
移动到文件顶部时,代码都能正常工作(没有 ExpressionChangedAfterItHasBeenCheckedError
)。
如何解决 SelectComponent
中的这个问题,使外部组件不必再实现任何逻辑?
TLDR:您可以在此处找到您的工作示例:https://stackblitz.com/edit/angular-f8fiux
要让它发挥作用,需要完成几个步骤:
修改writeValue方法。 writeValue 是自定义组件的 input,所以它不应该调用 onChange方法(即组件的output),否则会导致不正确的交互。
writeValue(value: any): void {
this.selectFormControl.setValue(value, { emitEvent: false });
}
这里传递emitEvent: false
是为了不触发selectFormControl
的变化事件。
为了理解值访问器的逻辑,我推荐 this article.
修改ngOnChange方法。它应该设置组件的内部值(通过 writeValue)然后为外部消费者发出它(通过 onChange)。对于 onChange 你必须安排微任务(Promise.resolve)或宏任务(setTimeout),否则数据将在相同的变化检测周期中发出,导致 ExpressionChangedAfterItHasBeenCheckedError
ngOnChanges(simpleChanges: SimpleChanges): void {
if (simpleChanges.options) {
if (this.options.length === 1) {
if (this.selectFormControl.value !== this.options[0]) {
this.writeValue(this.options[0]);
setTimeout(() => {
this.onChange(this.options[0]);
});
}
} else {
this.writeValue(null);
}
}
}
最好不要混用 Reactive Forms 和 Template-driven forms,所以我建议你去掉模板中的 (ngModelChange)="onChange($event)"
因为它是 异步数据流 并且需要更复杂的处理。取而代之的是,您可以在构造函数中添加一个简单的订阅(不要忘记取消订阅,这里我省略了)
constructor(private cdr: ChangeDetectorRef) {
this.selectFormControl.valueChanges.subscribe((value) => {
this.onChange(value);
});
}
您可以在 Angular docs
中找到有关 async 和 sync 流程的详细信息