Javascript Angular2 - 表达式在检查后已更改 - 使用调整大小事件绑定到 div 宽度

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/38930183/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 22:03:11  来源:igfitidea点击:

Angular2 - Expression has changed after it was checked - Binding to div width with resize events

javascriptangularresponsive-designangular2-changedetection

提问by Anthony Day

I have done some reading and investigation on this error, but not sure what the correct answer is for my situation. I understand that in dev mode, change detection runs twice, but I am reluctant to use enableProdMode()to mask the issue.

我对这个错误做了一些阅读和调查,但不确定我的情况的正确答案是什么。我知道在开发模式下,更改检测运行两次,但我不愿意enableProdMode()用来掩盖问题。

Here is a simple example where the number of cells in the table should increase as the width of the div expands. (Note that the width of the div is not a function of just the screen width, so @Media cannot easily be applied)

这是一个简单的示例,其中表格中的单元格数量应随着 div 的宽度扩展而增加。(注意 div 的宽度不仅仅是屏幕宽度的函数,所以 @Media 不能轻易应用)

My HTML looks as follows (widget.template.html):

我的 HTML 如下所示 (widget.template.html):

<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
   <td>Value1</td>
   <td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
   <td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>

This on its own does nothing. I'm guessing this is because nothing is causing change detection to occur. However, when I change the first line to the following, and create an empty function to receive the call, it starts working, but occasionally I get the 'Expression has changed after it was checked error'

这本身没有任何作用。我猜这是因为没有什么会导致发生变化检测。但是,当我将第一行更改为以下内容并创建一个空函数来接收调用时,它开始工作,但有时我会收到“检查错误后表达式已更改”

<div #widgetParentDiv class="Content">
   gets replaced with
      <div #widgetParentDiv (window:resize)=parentResize(10) class="Content">

My best guess is that with this modification, change detection is triggered and everything starts responding, however, when the width changes rapidly the exception is thrown because the previous iteration of change detection took longer to complete than changing the width of the div.

我最好的猜测是,通过这种修改,更改检测被触发,一切都开始响应,但是,当宽度快速变化时,会抛出异常,因为之前的更改检测迭代比更改 div 的宽度需要更长的时间来完成。

  1. Is there a better approach to triggering the change detection?
  2. Should I be capturing the resize event through a function to ensure change detection occurs?
  3. Is using #widthParentDiv to access the width of the div acceptable?
  4. Is there a better overall solution?
  1. 是否有更好的方法来触发更改检测?
  2. 我是否应该通过函数捕获调整大小事件以确保发生更改检测?
  3. 使用 #widthParentDiv 访问 div 的宽度是否可以接受?
  4. 有没有更好的整体解决方案?

For more details on my project please see thissimilar question.

有关我的项目的更多详细信息,请参阅类似问题。

Thanks

谢谢

回答by Mark Rajcok

To solve your issue, you simply need to get and store the size of the divin a component property after each resize event, and use that property in the template. This way, the value will stay constant when the 2nd round of change detection runs in dev mode.

要解决您的问题,您只需div在每次调整大小事件后获取并存储组件属性中的大小,并在模板中使用该属性。这样,当第二轮更改检测在开发模式下运行时,该值将保持不变。

I also recommend using @HostListenerrather than adding (window:resize)to your template. We'll use @ViewChildto get a reference to the div. And we'll use lifecycle hook ngAfterViewInit()to set the initial value.

我还建议使用@HostListener而不是添加(window:resize)到您的模板中。我们将使用@ViewChild来获取对div. 我们将使用生命周期钩子ngAfterViewInit()来设置初始值。

import {Component, ViewChild, HostListener} from '@angular/core';

@Component({
  selector: 'my-app',
  template: `<div #widgetParentDiv class="Content">
    <p>Sample widget</p>
    <table><tr>
       <td>Value1</td>
       <td *ngIf="divWidth > 350">Value2</td>
       <td *ngIf="divWidth > 700">Value3</td>
      </tr>
    </table>`,
})
export class AppComponent {
  divWidth = 0;
  @ViewChild('widgetParentDiv') parentDiv:ElementRef;
  @HostListener('window:resize') onResize() {
    // guard against resize before view is rendered
    if(this.parentDiv) {
       this.divWidth = this.parentDiv.nativeElement.clientWidth;
    }
  }
  ngAfterViewInit() {
    this.divWidth = this.parentDiv.nativeElement.clientWidth;
  }
}

Too bad that doesn't work. We get

太糟糕了,这不起作用。我们得到

Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.

检查后表情发生了变化。以前的值:'假'。当前值:“真”。

The error is complaining about our NgIfexpressions -- the first time it runs, divWidthis 0, then ngAfterViewInit()runs and changes the value to something other than 0, then the 2nd round of change detection runs (in dev mode). Thankfully, there is an easy/known solution, and this is a one-time only issue, not a continuing issue like in the OP:

错误是抱怨我们的NgIf表达式——它第一次运行时divWidth为 0,然后ngAfterViewInit()运行并将值更改为 0 以外的值,然后运行第二轮更改检测(在开发模式下)。值得庆幸的是,有一个简单/已知的解决方案,这是一个一次性的问题,而不是像 OP 那样的持续问题:

  ngAfterViewInit() {
    // wait a tick to avoid one-time devMode
    // unidirectional-data-flow-violation error
    setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  }

Note that this technique, of waiting one tick is documented here: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child

请注意,这里记录了这种等待一个滴答声的技术:https: //angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view- child

Often, in ngAfterViewInit()and ngAfterViewChecked()we'll need to employ the setTimeout()trick because these methods are called after the component's view is composed.

通常情况下,在ngAfterViewInit()ngAfterViewChecked()我们需要使用的setTimeout()伎俩,因为组件的视图是由后被调用这些方法。

Here's a working plunker.

这是一个工作plunker.



We can make this better. I think we should throttle the resize events such that Angular change detection only runs, say, every 100-250ms, rather then every time a resize event occurs. This should prevent the app from getting sluggish when the user is resizing the window, because right now, every resize event causes change detection to run (twice in dev mode). You can verify this by adding the following method to the previous plunker:

我们可以做得更好。我认为我们应该限制调整大小事件,以便 Angular 更改检测只运行,比如每 100-250 毫秒,而不是每次发生调整大小事件时。这应该可以防止应用在用户调整窗口大小时变得缓慢,因为现在,每个调整大小事件都会导致运行更改检测(在开发模式下两次)。您可以通过将以下方法添加到之前的 plunker 来验证这一点:

ngDoCheck() {
   console.log('change detection');
}

Observables can easily throttle events, so instead of using @HostListenerto bind to the resize event, we'll create an observable:

Observables 可以轻松地限制事件,因此@HostListener我们将创建一个 observable ,而不是用于绑定到 resize 事件:

Observable.fromEvent(window, 'resize')
   .throttleTime(200)
   .subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );

This works, but... while experimenting with that, I discovered something very interesting... even though we throttle the resize event, Angular change detection still runs every time there is a resize event. I.e., the throttling does not affect how often change detection runs. (Tobias Bosch confirmed this: https://github.com/angular/angular/issues/1773#issuecomment-102078250.)

这是有效的,但是......在试验过程中,我发现了一些非常有趣的东西......即使我们限制了调整大小事件,每次发生调整大小事件时,Angular 更改检测仍然运行。即,限制不会影响更改检测运行的频率。(Tobias Bosch 证实了这一点:https: //github.com/angular/angular/issues/1773#issuecomment-102078250。)

I only want change detection to run if the event passes the throttle time. And I only need change detection to run on this component. The solution is to create the observable outside the Angular zone, then manually call change detection inside the subscription callback:

我只希望在事件超过节流时间时运行更改检测。我只需要更改检测即可在此组件上运行。解决方案是在 Angular 区域外创建 observable,然后在订阅回调内手动调用更改检测:

constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
  // set initial value, but wait a tick to avoid one-time devMode
  // unidirectional-data-flow-violation error
  setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  this.ngzone.runOutsideAngular( () =>
     Observable.fromEvent(window, 'resize')
       .throttleTime(200)
       .subscribe(_ => {
          this.divWidth = this.parentDiv.nativeElement.clientWidth;
          this.cdref.detectChanges();
       })
  );
}

Here's a working plunker.

这是一个工作plunker.

In the plunker I added a counterthat I increment every change detection cycle using lifecycle hook ngDoCheck(). You can see that this method is not being called – the counter value does not change on resize events.

在 plunker 中,我添加了一个counter,我使用生命周期钩子增加每个更改检测周期ngDoCheck()。你可以看到这个方法没有被调用——计数器值不会在调整大小事件时改变。

detectChanges()will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick()instead (this is commented out in the plunker). Note that tick()will cause ngDoCheck()to be called.

detectChanges()将在此组件及其子组件上运行更改检测。如果您更愿意从根组件运行更改检测(即,运行完整的更改检测检查),请ApplicationRef.tick()改用(这在 plunker 中已注释掉)。请注意,这tick()将导致ngDoCheck()被调用。



This is a great question. I spent a lot of time trying out different solutions and I learned a lot. Thank you for posting this question.

这是一个很好的问题。我花了很多时间尝试不同的解决方案,我学到了很多。感谢您发布这个问题。

回答by Luiz C. Junior

Other way that i used to resolve this:

我用来解决这个问题的其他方法:

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


@Component({
  selector: 'your-seelctor',
  template: 'your-template',
})

export class YourComponent{

  constructor(public cdRef:ChangeDetectorRef) { }

  ngAfterViewInit() {
    this.cdRef.detectChanges();
  }
}

回答by Gopala raja naika

Simply use

只需使用

setTimeout(() => {
  //Your expression to change if state
});

回答by Mohanraj Ponnambalam

The best solution is to use setTimeout or delay on the services.

最好的解决方案是在服务上使用 setTimeout 或延迟。

https://blog.angular-university.io/angular-debugging/

https://blog.angular-university.io/angular-debugging/