Search code examples
javascriptangulartypescriptangular2-changedetection

Angular OnPush Change Detection Propagation to a Child Components in a ngFor Loop


I am having an issue with onPush Change Detection in an Angular app.

I have created a demo app that illustrates the problem: https://stackblitz.com/edit/angular-vcebqu

The application contains a parent component and a child component.

Both parent and child are using onPush Change Detection.

Both parent and child have inputs broken out into getters and setters, with this.cd.markForCheck(); being used in the setters.


    private _element: any;

    @Output()
    elementChange = new EventEmitter<any>();

    @Input()
    get element() {
        return this._element;
    }

    set element(newVal: any) {
        if (this._element === newVal) { return; }
        this._element = newVal;
        this.cd.markForCheck();
        this.elementChange.emit(this._element);
    }

The parent component creates several child components using a *ngFor loop, like so:


<app-child 
    *ngFor="let element of item.elements; let index = index; trackBy: trackElementBy" 
    [element]="item.elements[index]"
    (elementChange)="item.elements[index]=$event"></app-child>

The problem is, if the data is updated in the parent component, the changes are not being propogated down the the child component(s).

In the demo app, click the 'change' button and notice that the first 'element' in the 'elements' array ( elements[0].order ) is updated in the parent, but the change does not show in the the first child component's 'element'. However, if OnPush change detection is removed from the child component, it works properly.


Solution

  • Since the input passed in to the child component isn't an Array, IterableDiffers won't work. KeyValueDiffers however can be used in this case to watch for changes in the input object and then handle it accordingly (stackblitz link):

      import {
      Component,
      OnInit,
      ChangeDetectionStrategy,
      ChangeDetectorRef,
      KeyValueDiffers,
      KeyValueDiffer,
      EventEmitter,
      Output, Input
    } from '@angular/core';
    
    
    @Component({
      selector: 'app-child',
      templateUrl: './child.component.html',
      styleUrls: ['./child.component.css'],
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ChildComponent implements OnInit {
    
      private _element: any;
    
      @Output()
      elementChange = new EventEmitter<any>();
    
    
      get element() {
        return this._element;
      }
    
      @Input()
      set element(newVal: any) {
        if (this._element === newVal) { return; }
        this._element = newVal;
        this.cd.markForCheck();
        this.elementChange.emit(this._element);
      }
    
      private elementDiffer: KeyValueDiffer<string, any>;
    
      constructor(
        private cd: ChangeDetectorRef,
        private differs: KeyValueDiffers
      ) {
        this.elementDiffer = differs.find({}).create();
      }
    
      ngOnInit() {
      }
    
      ngOnChanges() {
        // or here
      }
    
      ngDoCheck() {
        const changes = this.elementDiffer.diff(this.element);
        if (changes) {
          this.element = { ...this.element };
        }
      }
    }