【问题标题】:Angular -- ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. (Nested FormArray)Angular -- ExpressionChangedAfterItHasBeenCheckedError:表达式在检查后已更改。 (嵌套形式数组)
【发布时间】:2017-09-16 00:07:25
【问题描述】:

前言:我意识到这可能是重复的,但是阅读了发现的错误的详细说明here 我仍然不明白我的代码将如何使更改中执行的脏检查无效检测。

我有一个包含 FormArray 的 FormGroup。我想将 FormArray 嵌套到一个子组件中,因为它包含很多它自己的特定业务逻辑。

当我在浏览器中加载组件并运行单元测试时,我收到以下异常:

ParentComponentA.html:2 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
    at viewDebugError (core.es5.js:8426)
    at expressionChangedAfterItHasBeenCheckedError (core.es5.js:8404)
    at checkBindingNoChanges (core.es5.js:8568)
    at checkNoChangesNodeInline (core.es5.js:12448)
    at checkNoChangesNode (core.es5.js:12414)
    at debugCheckNoChangesNode (core.es5.js:13191)
    at debugCheckRenderNodeFn (core.es5.js:13131)
    at Object.eval [as updateRenderer] (ParentComponentA.html:2)
    at Object.debugUpdateRenderer [as updateRenderer] (core.es5.js:13113)
    at checkNoChangesView (core.es5.js:1223

父组件 A:

@Component({
  selector: 'app-parent-component-a',
  templateUrl: './parent-component-a.component.html',
  styleUrls: ['./parent-component-a.component.scss']
})
export class ParentComponentA implements OnInit, OnDestroy {
  activeMediaViewport: string; // Should match a value of MaterialMediaQueries enum
  mediaWatcher: Subscription;
  parentForm: FormGroup;
  childComponentDisplayMode: number; // Should match a value of ComponentDisplayModes enum

  constructor(private formBuilder: FormBuilder, private mediaQueryService: ObservableMedia) {
    const prepareComponentBFormControl = (): FormGroup => {
      return formBuilder.group({
        'code': '',
        'weight': '',
        'length': '',
        'width': '',
        'height': '',
      });
    };

    const prepareParentForm = (): FormGroup => {
      return formBuilder.group({
        // ... omitted other properties
        'childComponentList': formBuilder.array([prepareComponentBFormControl()])
      });
    };

  }

  ngOnInit() {
    this.initializeWatchers();
  }

  ngOnDestroy() {
    this.mediaWatcher.unsubscribe();
  }

  /**
   * Sets intervals and watchers that span the entire lifecycle of the component and captures their results to be used for deregistration.
   */
  private initializeWatchers(): void {
    this.mediaWatcher = this.mediaQueryService
      .subscribe(mediaChange => {
        this.activeMediaViewport = mediaChange.mqAlias;
        this.childComponentDisplayMode = this.calculateComponentDisplayMode(this.activeMediaViewport);
      });
  }
}

组件 A 的 HTML 标记属性

<child-component-b [displayMode]="childComponentDisplayMode"
                   [nestedFormList]="childComponentList">
</child-component-b>

子组件 B:

@Component({
  selector: 'child-component-b',
  templateUrl: './child-component-b.component.html',
  styleUrls: ['./child-component-b.component.scss'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponentB implements OnInit {

  @Input() displayMode: number; // Should match a value of ComponentDisplayModes enum
  @Input() nestedFormList: FormArray;

  mobileDisplays: Array<number>;
  largeDisplays: Array<number>;
  numberOfRowsToAdd: FormControl;

  constructor(private formBuilder: FormBuilder) {
    this.mobileDisplays = [ComponentDisplayModes.TABLET_PORTRAIT, ComponentDisplayModes.PHONE_LANDSCAPE, ComponentDisplayModes.PHONE_PORTRAIT];
    this.largeDisplays = [ComponentDisplayModes.DESKTOP, ComponentDisplayModes.TABLET_LANDSCAPE];
  }

  ngOnInit() {
    // including in this SO post since it references the @Input property
    this.numberOfRowsToAdd = new FormControl(this.defaultRowsToAdd, this.addBuisnessLogicValidator(this.nestedFormList)); 
  }

  private addBuisnessLogicValidator(nestedFormListRef: FormArray): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} => {
    const rowsRemaining = maxLinesAllowed - nestedFormListRef.length;
    const rowsToAdd = control.value;
    const isInvalid = isNaN(parseInt(rowsToAdd, 10)) || rowsToAdd < 0 || rowsToAdd > rowsRemaining;
    return isInvalid ? {'invalidRowCount': {value: control.value}} : null;
  };

} }

组件 B HTML 标记属性

<div *ngFor="let listItem of nestedFormList.controls; index as index"
     [formGroup]="listItem">
</div>

我认为在子组件中使用 *ngFor 可能会在更改检测期间“弄脏”视图的值?

【问题讨论】:

  • 你能用你的问题创建一个 plunker 吗?
  • 我已经阅读了那篇文章@AngularInDepth.com - 这就是我在我的 OP 中链接的内容。我仍然不明白在这种情况下我在做什么会更新任何导致异常的东西。 yurzi 等我有空的时候,我会试着把它放到一个 plunker 里,谢谢。
  • @rawkfist0215,是的,然后尝试创建一个 plunker
  • 我找到了解决方案。创建 plunkr 帮助我隔离了潜在的问题点并缩小了原因。谢谢两位的建议。

标签: angular typescript angular-cli


【解决方案1】:

原来问题出在操作员错误上(看图)。我发现抛出了这个异常,因为我在 HTML &lt;input&gt; 元素上定义了一个工件“必需”属性,而没有说明它是使用 Angular 的 Validators.required 静态方法在 FormControl 验证器定义中所必需的。

将它们定义在一个地方而不是另一个地方会导致值在第一个和第二个更改检测例程之间发生变化。

所以...

          <input mdInput
               formControlName="weight"
               placeholder="Weight"
               type="text"
               aria-label="weight"
               maxlength="6"
               required>

需要从模板中删除“required”和“maxlength”属性并放在FormGroup定义中,即

const prepareComponentBFormControl = (): FormGroup => {
      return formBuilder.group({
        'code': '',
        'weight': ['', Validators.required, Validators.maxlength(6)],
        'length': '',
        'width': '',
        'height': '',
      });
    };

【讨论】:

  • 谢谢!这个解决方案也对我有用。我还在FormGroup 中有一个嵌套的FormArray,它与具有required 属性的&lt;input&gt; 交互。一旦我删除了 required 属性,这个错误就消失了——我想我可以在添加 Validator.required 以匹配后重新引入。
猜你喜欢
  • 2021-09-15
  • 1970-01-01
  • 2017-10-19
  • 1970-01-01
  • 2018-10-23
  • 2019-05-14
  • 2018-11-06
  • 2019-02-19
  • 2018-04-22
相关资源
最近更新 更多