Search code examples
angularangular2-changedetection

Angular ChangeDetection NgIf


So i am trying to better understand Angulars ChangeDetection and stumbled into a problem: https://plnkr.co/edit/M8d6FhmDhGWIvSWNVpPm?p=preview

This Plunkr is a simplified version of my applications code and basically has a parent and a child component. Both having ChangeDetectionStrategy.OnPush enabled.

parent.component.ts

@Component({
    selector: 'parent',
    template: `
            <button (click)="click()">Load data</button>
            {{stats.dataSize > 0}}
            <span *ngIf="stats.dataSize > 0">Works</span>
            <child [data]="data" [stats]="stats" (stats)="handleStatsChange()"></child>
        `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent implements OnCheck, OnChanges {

    data = [];
    stats = {
        dataSize: 0
    };

   constructor(private cdr: ChangeDetectorRef) {
   }

    click() {
        console.log("parent: loading data");
        setTimeout(() => {
            this.data = ["Data1", "Data2"];
            this.cdr.markForCheck();
        });
    }

    handleStatsChange() {
        console.log('parent: stats change');
        this.cdr.markForCheck();
    }
}

child.component.ts

import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from "@angular/core";

@Component({
    selector: 'child',
    template: `
        <div *ngFor="let item of data">{{item}}</div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit, OnChanges {

    @Input() data;
    @Input() stats;
    @Output('stats') statsEmitter = new EventEmitter();

    constructor() {
    }

    ngOnInit() {
    }


    ngOnChanges(changes: SimpleChanges): void {
        console.log("child changes: ", changes);

        this.stats.dataSize = changes['data'].currentValue.length;
        this.statsEmitter.emit(this.stats);
    }
}

So parent updates data on button click which triggers ngOnChanges in child. Everytime data changes, child.component changes a value in stats. I want this value, dataSize, to be used in the <span *ngIf="stats.dataSize > 0">Works</span> in parent. For some reason the *ngIf wont be updated. The template {{stats.dataSize > 0}} otherwise updates no problem.

What i noticed: If i remove OnPush on parent, Angular will throw a exception Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.. I guess this comes from *ngIf="stats.dataSize > 0" being false first and now true after the second iteration of change detection in dev mode.

So thats why i tried setting this.cdr.markForCheck(); in parent in handleStatsChange. handleStatsChange will be called in child. This has no consequences thought, exception thrown anyway.

I guess change detection on parent doesnt get triggered because no @Input changed in parent, therefore ngIf doesnt update?? Clicking the button two times will actually show Works. I this because a new digest cycle does now start (triggered by a Event) and parents ChangeDetectorRef is now updating the template?

So why does Angular update {{stats.dataSize > 0}} and throw an error at ngIf?

Any help much appreciated :)


Solution

  • Digging a little more into Lifecycle Hooks Documentation i noticed they provide really nice examples about handling change detection.

    In their counter example in CounterParentComponent they have to update their LoggerService by running a new 'tick', which means delaying execution with setTimeout.

    counter:

    updateCounter() {
        this.value += 1;
        this.logger.tick();
    }
    

    logger:

    tick() {  this.tick_then(() => { }); }
    tick_then(fn: () => any) { setTimeout(fn, 0); }
    

    Thats exactlly what i had to do to get my code working. They also mention this in their docs:

    Angular's unidirectional data flow rule forbids updates to the view after it has been composed. Both of these hooks fire after the component's view has been composed. Angular throws an error if the hook updates the component's data-bound comment property immediately (try it!). The LoggerService.tick_then() postpones the log update for one turn of the browser's JavaScript cycle and that's just long enough.