ExpressionChangedAfterItHasBeenCheckedError: 表达式在被检查之后已经发生了变化。先前的值是:'undefined'。

145

我知道在stack-overflow上已经有很多相同的问题,并尝试了不同的解决方案来避免运行时错误,但是没有一个适用于我。

错误

组件和HTML代码

export class TestComponent implements OnInit, AfterContentChecked {
    @Input() DataContext: any;
    @Input() Position: any;
    sampleViewModel: ISampleViewModel = { DataContext: : null, Position: null };
    constructor(private validationService: IValidationService, private modalService: NgbModal, private cdRef: ChangeDetectorRef) {
    }

    ngOnInit() {

    }
    ngAfterContentChecked() {

            debugger;
            this.sampleViewModel.DataContext = this.DataContext;
            this.sampleViewModel.Position = this.Position;

    }


<div class="container-fluid sample-wrapper text-center" [ngClass]="sampleViewModel.DataContext?.Style?.CustomCssClass +' samplewidget-'+ sampleViewModel.Position?.Columns + 'x' + sampleViewModel.Position?.Rows">
     //some other html here
</div>
请注意:该组件是使用DynamicComponentLoader动态加载的。
在我解决问题后,我已经确定了几个问题。
首先,通过使用DynamicComponentResolver动态加载该子组件,并传递如下输入值。
 ngAfterViewInit() {
    this.renderWidgetInsideWidgetContainer();

  }


  renderWidgetInsideWidgetContainer() {
    let component = this.storeFactory.getWidgetComponent(this.dataSource.ComponentName);
    let componentFactory = this._componentFactoryResolver.resolveComponentFactory(component);
    let viewContainerRef = this.widgetHost.viewContainerRef;
    viewContainerRef.clear();
    let componentRef = viewContainerRef.createComponent(componentFactory);
    debugger;
    (<IDataBind>componentRef.instance).WidgetDataContext = this.dataSource.DataContext;
    (<IDataBind>componentRef.instance).WidgetPosition = this.dataSource.Position;

  }

即使我将子组件的HTML更改为以下内容,我仍会收到相同的错误。只需添加一个Angular ngclass属性即可。

<div class="container-fluid ds-iconwidget-wrapper text-center" [ngClass]="Sample">

</div>

我的数据绑定和一切都正常。我需要在父组件上做些什么吗?我已经尝试了子组件中的所有生命周期事件。


1
@Maximus 作为入口组件 - Jameel Moideen
1
首先,将 this.renderWidgetInsideWidgetContainer();ngAfterViewInit 移动到 ngOnInit - yurzui
1
@Maximus,感谢您的时间。您的文章太棒了! - Jameel Moideen
1
@JEMI,不客气)我有很多有趣的文章,请查看。 - Max Koretskyi
1
@Maximus 当然。我已经关注你了。你的每一篇文章都非常深入。 - Jameel Moideen
显示剩余10条评论
12个回答

165

你需要告诉 AngularngAfterContentChecked 后更新了内容。 你可以从 @angular/core 导入 ChangeDetectorRef 并调用 detectChanges

import { ChangeDetectorRef } from '@angular/core';

constructor( private cdref: ChangeDetectorRef ) {}   

ngAfterContentChecked() {
    this.sampleViewModel.DataContext = this.DataContext;
    this.sampleViewModel.Position = this.Position;
    this.cdref.detectChanges();
 }

75
ngAfterContentChecked生命周期钩子在子组件/指令绑定更新完成后触发。但是,您正在更新用作ngClass指令绑定输入的属性。这就是问题所在。当 Angular 运行验证阶段时,它检测到有待处理的属性更新并抛出错误。

为了更好地理解错误,请阅读以下两篇文章:

思考一下为什么需要在ngAfterViewInit生命周期钩子中更改属性。任何在ngAfterViewInit/Checked之前触发的其他生命周期都可以工作,例如ngOnInitngDoCheckngAfterContentChecked

因此,要修复它,请将renderWidgetInsideWidgetContainer移动到ngOnInit()生命周期钩子中。


你为什么要在 ngAfterViewInit/Checked 中更新属性? - Max Koretskyi
实际上它没有更新,我正在将我的输入数据设置到一个视图模型中,最终所有属性都通过这个模型像包装类一样绑定。当我直接绑定@Input数据上下文时,我得到了相同的错误。是否有任何生命周期事件的解决方法来摆脱这个错误?让我尝试你回答中提到的ngAfterViewInit/Checked。 - Jameel Moideen
3
尝试使用 ngOnInitngDoCheckngAfterContentChecked。如果仍然无法正常工作,请创建一个 Plunker。 - Max Koretskyi
我尝试了所有的生命周期事件,但不幸的是我无法解决这个问题。但我的问题在故障排除后添加了更多的要点。希望这能帮助你获得一些想法。 - Jameel Moideen
1
只有在我使用引用变量而不是目录之后,ngOnInit 才能为我工作,就像这里所解释的那样:https://dev59.com/aVYM5IYBdhLWcg3wzSog - dgraf
显示剩余5条评论

27
 ngAfterViewInit() {
   setTimeout(() => {
     this.renderWidgetInsideWidgetContainer();
   }, 0);
  }

这是解决这个问题的好方案。


2
没有其他解决方案适用于我的特殊的玄学情况,所以经过几个小时的尝试后,我放弃了并不得不采取这种方法,结果它奏效了。 - svarog
我不知道为什么,但它对我也起作用了。 - Fatih Ersoy
4
你可以把setTimeout()看作是将某些东西放在JS事件队列的末尾。所以即使Timeout设置为“0”,它也会将代码放到要运行的事件的末尾,这意味着Timeout中的代码很可能会在排在队列前面的Angular渲染代码之后运行。最终,setTimeout()感觉像是一个hack解决方案,但当Angular生命周期不按预期工作并且你渴望让它正常工作时,这是一个可以使用的工具。 - Jeff Gilliland
我在加载一个带有复选框列表的对话框组件时遇到了这个问题。通过将我的代码包装到 setTimeout() 函数中,对话框的显示被延迟足够长的时间来消除那个错误。 - AllJs
1
非常遗憾我必须使用这个,但它是唯一有效的! - Malachi Bazar

21

我遇到了问题。

错误:ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'mat-checkbox-checked': 'true'. Current value: 'false'.

这里的问题是更新后的值要等到下一次变更检测周期才会被检测到。

最简单的解决方法是添加变更检测策略。将以下代码添加到您的代码中:

import { ChangeDetectionStrategy } from "@angular/core";  // import

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "abc",
  templateUrl: "./abc.html",
  styleUrls: ["./abc.css"],
})

2
危险:https://github.com/angular/angular/issues/23657#issuecomment-653939615 - Janaka Bandara

13
如果你在使用<ng-content>*ngIf一起时,你很可能会陷入这个循环中。
我发现唯一的解决办法是将*ngIf改为display:none

1
或者可以使用 [hidden]="!condition"。 - Pragati Dugar

6

两种解决方案:

  1. 确保如果你有一些绑定变量,那么将该代码移动到 setTimeout( { }, 0); 中。
  2. 将相关的代码移到 ngAfterViewInit 方法中。

这取决于它将您的代码移动到事件队列中,当当前执行完成时,它将被执行。JavaScript是一种单线程脚本语言,因此它一次只能执行一段代码(由于其单线程性质),每个代码块都会“阻塞”其他异步事件的进度。settimeout代码仅在当前代码完成执行后才会被执行。 - Chetan Laddha
嗯...这个解决了我的问题。我进入组件,将所有绑定的更改放入0秒的超时中,它可以工作。这是一个好的解决方案还是一个hack?在生命周期中,我可能会调用2次更改绑定值的方法。 - darren z

4

*NgIf在这里可能会造成问题,所以要么使用display:none CSS,或者更简单的方法是使用[hidden]="!condition"


4
setTimeout(() => { /* your code here */ }, 0);

我将我的代码包裹在setTimeout中,然后它就起作用了。


2

我曾经遇到了和你一样的问题,通过类似Richie Fredicson的回答解决了它。

当你运行createComponent()时,它是使用未定义的输入变量创建的。 然后在将数据分配给这些输入变量之后,它会改变事情,并导致您子模板中的错误(在我的情况下,因为我在ngIf中使用了输入,一旦我分配了输入数据就会发生更改)。

我在这种特定情况下找到的唯一避免方法是在分配数据后强制进行变更检测,但我没有在ngAfterContentChecked()中执行它。

您的示例代码有点难以理解,但如果我的解决方案适用于您,则应该像这样(在父组件中):

export class ParentComponent implements AfterViewInit {
  // I'm assuming you have a WidgetDirective.
  @ViewChild(WidgetDirective) widgetHost: WidgetDirective;

  constructor(
    private componentFactoryResolver: ComponentFactoryResolver,
    private changeDetector: ChangeDetectorRef
  ) {}

  ngAfterViewInit() {
    renderWidgetInsideWidgetContainer();
  }

  renderWidgetInsideWidgetContainer() {
    let component = this.storeFactory.getWidgetComponent(this.dataSource.ComponentName);
    let componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
    let viewContainerRef = this.widgetHost.viewContainerRef;
    viewContainerRef.clear();
    let componentRef = viewContainerRef.createComponent(componentFactory);
    debugger;
    // This <IDataBind> type you are using here needs to be changed to be the component
    // type you used for the call to resolveComponentFactory() above (if it isn't already).
    // It tells it that this component instance if of that type and then it knows
    // that WidgetDataContext and WidgetPosition are @Inputs for it.
    (<IDataBind>componentRef.instance).WidgetDataContext = this.dataSource.DataContext;
    (<IDataBind>componentRef.instance).WidgetPosition = this.dataSource.Position;
    this.changeDetector.detectChanges();
  }
}

我的情况与那个差不多,只不过我使用的是@ViewChildren而不是@ViewChild,因为我有多个宿主元素。


0

PS:我的错误细节略有不同,我的错误检测到 mat-focused 值从 true 改为了 false

在研究期间,我注意到该错误是因为我尝试将 mat-tab-groupSelectedIndex 属性绑定到标签组的 tab 1 中的 mat-select 字段中所选项目的索引(下面是代码摘录)。

HTML 提取:

  <form [formGroup]="form" (ngSubmit)="onSubmit()">
    <mat-tab-group animationDuration="500ms" [selectedIndex]="titleId.value">
      <mat-tab label="Tab 1">
        <mat-radio-group formControlName="titleId">
          <mat-radio-button value="1">Mr</mat-radio-button>
          <mat-radio-button value="2">Mrs</mat-radio-button>
        </mat-radio-group>
      </mat-tab>
      <mat-tab label="tab 2">
        ...
      </mat-tab>
      <mat-tab label="tab 3">
        ...
      </mat-tab>
    </mat-tab-group>
  </form>

虽然我的代码按预期工作(即根据下拉列表中选择的项目切换选项卡),但忽略这个错误还是相当令人沮丧的。不幸的是,这里提出的任何建议解决方案都不适用于我。然而,我注意到将mat-radio-group移到mat-tab-group组件外部可以解决问题。

新的实现:

  <form [formGroup]="form" (ngSubmit)="onSubmit()">
    <mat-radio-group formControlName="titleId">
      <mat-radio-button value="1">Mr</mat-radio-button>
      <mat-radio-button value="2">Mrs</mat-radio-button>
    </mat-radio-group>
    <mat-tab-group animationDuration="500ms" [selectedIndex]="titleId.value">
      <mat-tab label="Tab 1">
        ...
      </mat-tab>
      <mat-tab label="tab 2">
        ...
      </mat-tab>
      <mat-tab label="tab 3">
        ...
      </mat-tab>
    </mat-tab-group>
  </form>

我原本以为下面的代码可以解决问题,但实际上并没有。可能是我做错了什么。
abc.component.ts 代码片段
  ngAfterViewInit() {
    setTimeout(() => {
      this.cdRef.detectChanges(); /*cdRef injected in constructor*/
    }, 0);
  }

我仍然相信有更好的方法来解决这个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接