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

前端 未结 4 652
清歌不尽
清歌不尽 2020-12-01 02:10

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 twic

相关标签:
4条回答
  • 2020-12-01 02:30

    Simply use

    setTimeout(() => {
      //Your expression to change if state
    });
    
    0 讨论(0)
  • 2020-12-01 02:31

    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. 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. We'll use @ViewChild to get a reference to the div. And we'll use lifecycle hook ngAfterViewInit() to set the initial value.

    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). Thankfully, there is an easy/known solution, and this is a one-time only issue, not a continuing issue like in the 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

    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.

    Here's a working 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:

    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:

    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.)

    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:

    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.

    In the plunker I added a counter that 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.

    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.


    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.

    0 讨论(0)
  • 2020-12-01 02:37

    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();
      }
    }
    
    0 讨论(0)
  • 2020-12-01 02:49

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

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

    0 讨论(0)
提交回复
热议问题