如何解决嵌套反应式表单组件的变更检测问题?

How to solve a change detection issue with nested reactive form components?

我有以下(简化的)场景:

到目前为止效果很好,但是当我想根据以下内容自动更新内部 SelectComponent 中的行为时遇到问题:

为了实现上述目标,我实施了 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

中找到有关 asyncsync 流程的详细信息