简体   繁体   English

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

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

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.这是一个简单的示例,其中表格中的单元格数量应随着 div 的宽度扩展而增加。 (Note that the width of the div is not a function of just the screen width, so @Media cannot easily be applied) (注意 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?使用 #widthParentDiv 访问 div 的宽度是否可以接受?
  4. Is there a better overall solution?有没有更好的整体解决方案?

For more details on my project please see this similar question.有关我的项目的更多详细信息,请参阅类似问题。

Thanks谢谢

To solve your issue, you simply need to get and store the size of the div in a component property after each resize event, and use that property in the template.要解决您的问题,您只需在每次调整大小事件后获取并存储div的大小在组件属性中,并在模板中使用该属性。 This way, the value will stay constant when the 2nd round of change detection runs in dev mode.这样,当第二轮更改检测在开发模式下运行时,该值将保持不变。

I also recommend using @HostListener rather than adding (window:resize) to your template.我还建议使用@HostListener而不是将(window:resize)添加到您的模板中。 We'll use @ViewChild to get a reference to the div .我们将使用@ViewChild来获取对div的引用。 And we'll use lifecycle hook ngAfterViewInit() to set the initial value.我们将使用生命周期钩子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 NgIf expressions -- the first time it runs, divWidth is 0, then ngAfterViewInit() runs and changes the value to something other than 0, then the 2nd round of change detection runs (in dev mode).错误是抱怨我们的NgIf表达式——它第一次运行时, divWidth为 0,然后ngAfterViewInit()运行并将值更改为 0 以外的值,然后运行第二轮更改检测(在开发模式下)。 Thankfully, there is an easy/known solution, and this is a one-time only issue, not a continuing issue like in the OP:值得庆幸的是,有一个简单/已知的解决方案,这是一个一次性的问题,而不是像 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.我认为我们应该限制调整大小事件,以便 Angular 更改检测只运行,比如每 100-250 毫秒,而不是每次发生调整大小事件时。 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:您可以通过将以下方法添加到之前的 plunker 来验证这一点:

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

Observables can easily throttle events, so instead of using @HostListener to bind to the resize event, we'll create an observable: Observables 可以轻松地限制事件,因此我们将创建一个 observable,而不是使用@HostListener绑定到 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.这是有效的,但是......在试验过程中,我发现了一些非常有趣的东西......即使我们限制了调整大小事件,每次发生调整大小事件时,Angular 更改检测仍然运行。 Ie, the throttling does not affect how often change detection runs.即,限制不会影响更改检测运行的频率。 (Tobias Bosch confirmed this: https://github.com/angular/angular/issues/1773#issuecomment-102078250 .) (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 counter that I increment every change detection cycle using lifecycle hook ngDoCheck() .在 plunker 中,我添加了一个counter ,我使用生命周期钩子ngDoCheck()递增每个更改检测周期。 You can see that this method is not being called – the counter value does not change on resize events.你可以看到这个方法没有被调用——计数器值不会在调整大小事件时改变。

detectChanges() will run change detection on this component and its children. detectChanges()将对此组件及其子组件运行更改检测。 If you would rather run change detection from the root component (ie, run a full change detection check) then use ApplicationRef.tick() instead (this is commented out in the plunker).如果您更愿意从根组件运行更改检测(即,运行完整的更改检测检查),请改用ApplicationRef.tick() (这在 plunker 中已注释掉)。 Note that tick() will cause ngDoCheck() to be called.请注意, 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.感谢您发布这个问题。

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();
  }
}

Simply use只需使用

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

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/

Mark Rajcok gave a great answer. Mark Rajcok 给出了很好的答案。 The simpler version (without throttling) would be:更简单的版本(没有节流)是:

ngAfterViewInit(): void {
    this.windowResizeSubscription = fromEvent(window, 'resize').subscribe(() => this.onResize())
    this.onResize() // to initialize before any change
  }

onResize() {
    this.width = this.elementRef.nativeElement.getBoundingClientRect().width;
    this.changeDetector.detectChanges();
  }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM