Search code examples
angularangular2-changedetection

What is a true way to update ancestors components if a components tree has OnPush strategy set?


I have tree of components, every component has changeDetection: ChangeDetectionStrategy.OnPush, in that case ancestors components don't detect own local changes that they are making with, for example, observables or setTimeout(). I done this by adding this.ref.markForCheck() call where a component should be updated. But I guess it maybe not a good way to solve the problem.

So what is a true way to do this? Am I right that if I use third party component and it doesn't have this.ref.markForCheck() call it may not work?

Here is my lowest level component's code:

import {
  Component, HostBinding, ViewEncapsulation, AfterContentInit,
  Input, ViewChild, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef,
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import * as $ from 'jquery';

import { DestroyUnsubscribe } from 'services';

@Component({
  selector: 'jf-more',
  encapsulation: ViewEncapsulation.None,
  // changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['more.component.scss'],
  template: `
    <div class="more__open-button" *ngIf="!opened" (click)="onToggleClick()">
      <ng-content select="jf-more-open"></ng-content>
    </div>
    <div class="more__text" [ngClass]="{'more__text_opened': opened}" #text>
      <ng-content></ng-content>
    </div>
    <div class="more__close-button" *ngIf="opened" (click)="onToggleClick()">
      <ng-content select="jf-more-close"></ng-content>
    </div>
  `,
})
@DestroyUnsubscribe()
export class MoreComponent implements AfterContentInit {
  @Input() oneLineHeight = 16;
  @ViewChild('text') text: ElementRef;
  @HostBinding('class.more') mainClass = true;
  @HostBinding('class.more_initiated') initiated: boolean = false;
  @HostBinding('class.more_one-line') oneLine: boolean;
  opened: boolean;

  private subscriptions: Subscription[] = [];

  constructor(private ref: ChangeDetectorRef) {}

  ngAfterContentInit() {
    this.makeLineCheck();
    this.subscriptions.push(
      Observable
        .fromEvent(window, 'resize')
        .debounceTime(500)
        .subscribe(() => this.makeLineCheck())
    );
  }

  onToggleClick() {
    this.opened = !this.opened;
    // this.ref.detectChanges();
    // this.ref.markForCheck();
  }

  private makeLineCheck() {
    this.initiated = false;
    this.opened = true;
    this.oneLine = false;
    // this.ref.detectChanges();
    this.ref.markForCheck();
    console.log('makeLineCheck', Object.assign({}, this));
    setTimeout(() => {
      this.oneLine = ($(this.text.nativeElement).innerHeight() <= this.oneLineHeight);
      this.initiated = true;
      this.opened = false;
      // this.ref.detectChanges();
      this.ref.markForCheck();
      console.log('setTimeout makeLineCheck', Object.assign({}, this));
    });
  }
}

Solution

  • ApplicationRef.tick() or setTimeout(...) invokes change detection for the root node, which then propagates down. If the components in between are set to OnPush and no inputs change, this won't cause change detection in these components though.

    I would use shared services with observables to notify ancestors about changes for them to call markForCheck() for themselves.