表单组值更改时不会触发更改检测

Posted

技术标签:

【中文标题】表单组值更改时不会触发更改检测【英文标题】:Change detection does not trigger when the formgroup values change 【发布时间】:2019-05-07 12:14:19 【问题描述】:

我创建了一个简单的示例来演示我面临的一个奇怪问题。

Stackblitz - https://stackblitz.com/edit/angular-change-detection-form-group

我有三个组件,它们是:

1 - 应用组件

import  Component, OnInit, ChangeDetectionStrategy  from '@angular/core';
import  FormGroup, FormControl  from '@angular/forms';

@Component(
  selector: 'my-app',
  template: `<hello [form]="form"></hello>
  <hr />
  <button (click)="changeFormValue()">Change Form Value</button>`,
  styleUrls: ['./app.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush

)
export class AppComponent implements OnInit 
  name = 'Angular';

  form: FormGroup;

  ngOnInit() 
    this.form = new FormGroup(
      name: new FormControl('ABC'),
      age: new FormControl('24')
    );
  

  changeFormValue() 
    this.form.setValue(
      name: 'XYZ',
      age: 35
    )
  

2 - 你好组件

import  Component, Input, OnChanges, ChangeDetectionStrategy  from '@angular/core';
import  FormGroup  from '@angular/forms';

@Component(
  selector: 'hello',
  template: `<form [formGroup]="form">
  <app-input [form]="form"></app-input>
  </form>`,
  styles: [``],
  changeDetection: ChangeDetectionStrategy.OnPush
)
export class HelloComponent implements OnChanges 
  @Input() form: FormGroup;

  ngOnChanges(changes) 
    console.log(changes)
  

3 - 输入组件

import  Component, Input, OnInit, OnChanges, ChangeDetectionStrategy  from '@angular/core';
import  FormGroup  from '@angular/forms';

@Component(
  selector: 'app-input',
  template: `Name : <input type="text" [formControl]="nameFormcontrol" /> nameFormcontrol.value <br /><br />
  Age : <input type="text" [formControl]="ageFormcontrol" /> ageFormcontrol.value`,
  styles: [``],
  changeDetection: ChangeDetectionStrategy.OnPush
)
export class InputComponent implements OnInit, OnChanges 
  @Input() form: FormGroup;
  nameFormcontrol;
  ageFormcontrol;

  ngOnInit() 
    this.nameFormcontrol = this.form.get('name');
    this.ageFormcontrol = this.form.get('age');
  

  ngOnChanges(changes) 
    console.log(changes)
  

在hello组件和input组件中,我都将changedetection策略设置为onpush。正如您在上面看到的,我正在应用程序组件中创建一个表单组实例并将其传递给子组件。现在,当我单击应用程序组件上的按钮更改表单值时,它会更改输入字段中的值,但不会更改纯文本。它仅在我从两个子组件中删除推送更改检测时才有效。即使表单组值发生变化,也不会调用 ngOnChanges。

是不是很奇怪。更改检测如何用于输入而不是此处的纯文本?

有人可以解释一下吗?在不删除 onpush 更改检测的情况下,它的解决方法是什么。

【问题讨论】:

为什么要设置变更检测策略?另外,您在哪里找到这种模式 - 将 2 层深的反应式表单传递给输入,然后期望***父级和子级保持同步似乎不符合 Angular 最佳实践。可能值得回顾一下您所写的内容,以确保您不会使用“聪明”的解决方案使问题变得过于复杂。 【参考方案1】:

虽然我不确定这是否是理想的解决方案,但我找到了解决此问题的方法。

我们可以监听表单组值的变化,然后在输入组件中触发变化检测

this.form.valueChanges.subscribe( () => 
  this.cdr.detectChanges()
);

这样,它会连同输入一起更新标签值。

解决办法如下:

https://stackblitz.com/edit/angular-change-detection-form-group-value-change-issue-resolved

我不确定这是否是 Angular 的错误,但很高兴我找到了一些解决方法:)

【讨论】:

您的问题是您的 UI 需要订阅 FormControl 上的更改。它不会那样做,它只是渲染nameFormcontrol.value,然后它就叫它一天,因为它只更新onPush,这意味着它不再检查nameFormcontrol.value 的新值。您需要将值转换为 UI 可以订阅的内容,例如一个可观察的。或者,您可以像以前那样进行手动更改检测。我认为这是一个有效的解决方案。请记住 unsbuscribe 中的 ngOnDestroy 以避免内存泄漏。 这不是解决方法。这正是它的预期工作方式。当您将更改检测更改为 OnPush 时,您是在告诉 Angular,我会在我进行更改时通知您,并手动调用更新周期。【参考方案2】:

你可以做得更好!如果可行,请告诉我:

当您使用响应式表单时,您可以使用一种非常酷的方法,称为updateValueAndValidity();

private changeControlValue(control, value: number) 
    control.setValue(value);
    control.updateValueAndValidity();
  

您也可以在更新添加到表单的验证器时使用此方法,例如:

this.control.setValidators([Validators.required, Validators.minLength(5)]);
control.updateValueAndValidity();

这应该可以解决问题!我认为这是针对 ng-model 使用响应式表单或表单控件的最佳尝试之一。

我不建议在您的表单中使用 valueChanges,因为您可能会忘记取消订阅并造成内存泄漏,即使您记得创建此流程可能很乏味。

请记住,当使用 onPush 更改检测时,Angular 只会将三件事检测为更改:

1- 输入和输出。 2- 用户可以执行的 html 事件,例如(单击)。 3- 订阅等异步事件。

希望对你有所帮助!

【讨论】:

【参考方案3】:

只需使用async管道在模板中订阅控件的valueChanges属性,无需手动触发更改检测和订阅组件中的valueChanges

<input [formControl]="control"/>
<p>control.valueChanges | async</p>

【讨论】:

请注意,您不会获得当前值 - 只有在第一次更改时才会通过valueChanges发出值【参考方案4】:

Angular 仅在变量的内存地址更改时才检测更改。设置该值不会更改内存地址,因此不会命中 ngOnChanges。

数组也是如此。一个简单的推送不会命中 ngOnChanges,必须通过 = 将内存地址更改为新数组。

试试这个:

import  Component, Input, OnInit, OnChanges, ChangeDetectionStrategy  from '@angular/core';
import  FormGroup  from '@angular/forms';

@Component(
  selector: 'app-input',
  template: `
  <div formGroupName="form">
      Name : <input type="text" formControlName="name" /> form.value.name <br /><br />
      Age : <input type="text" formControlName="age" /> form.value.age
  </div>`,
  styles: [``],
  changeDetection: ChangeDetectionStrategy.OnPush
)
export class InputComponent implements OnInit, OnChanges 
  @Input() form: FormGroup;

  ngOnInit() 
  

  ngOnChanges(changes) 
    console.log(changes)
  

【讨论】:

但是它如何在不触发更改检测的情况下更改输入值,为什么不使用纯文本呢?我还是不明白。这对我来说似乎是一个奇怪的问题。您能否发布一个示例,说明如何在不删除 onpush 的情况下解决此问题? 不要传递实际的表单。传递 form.value。 这就是我编写输入组件的方式。我只是从父级传递表单组实例,其余所有事情都在输入组件本身内部得到处理。我的问题还是一样,那为什么还要更新输入值呢? 你可以很容易地传递form.value,然后在子组件上,创建一个空白表单,然后你所做的就是this.form.patchValue(inputVal),一切都很好。 我明白了!但我不想这样做,因为我在输入组件中做了很多我不想改变的东西。这只是我创建的一个示例,用于解释我面临的问题。此外,我想知道为什么更改检测适用于输入而不适用于文本。【参考方案5】:

在自动更改检测 (cd) 运行期间,Angular 会注意到您的 FormGroup 的值已更新并分别更新 UI。

通过将更改检测策略设置为OnPush,您可以停用 Angular 运行的自动更改检测。在这种情况下,只会更新内部值,但 Angular 不会检查 UI 的值。

也许这是对 Angular 的 ChangeDetection 的一个过于肤浅的解释,所以我推荐这个 blog 来更深入地了解这个主题。

ngOnChanges 不会触发,因为你的FormGroup 的对象引用(内存地址)没有改变。 ngOnChanges 仅在您将 primitives 传递给组件的 @Input 时才会触发。这也会触发 Angular 运行的新变更检测。

要更新 UI,您可以通过在父组件中注入 ChangeDetectorRef 并调用 detectChanges() 来手动触发更改检测。

这可能看起来像这样:

constructor(private cd: ChangeDetectorRef) 
...
changeFormValue() 
    this.form.setValue(
        name: 'XYZ',
        age: 35
    );
    // This will trigger the change detection and your input field are updated
    this.cd.detectChanges(); 

我希望这是可以理解的 ;-)

【讨论】:

我之前已经尝试过了。这是行不通的。 stackblitz.com/edit/angular-change-detection-form-group那它怎么会改变输入的值呢? 当我点击“更改表单值”按钮时。它会更改输入内的值,但不会更改输入旁边的纯文本。你能看一下吗? 我试过了。我单击“更改表单值”按钮,输入字段显示值“XYZ”和“35”。 是的,效果很好。输入字段旁边的文本呢?我在不改变的输入字段旁边显示输入的值。 我已添加截图供您参考。请看一下并提出建议。

以上是关于表单组值更改时不会触发更改检测的主要内容,如果未能解决你的问题,请参考以下文章

按下后退时 EditText 不会触发更改

按下后退时,EditText不会触发更改

动态更改复选框不会触发 onChange?

TeamCity 不会触发自动构建

jQuery - 更改 iframe src 时仅在第二次单击时触发

v-model 不会检测到由 jQuery 触发事件所做的更改