'Angular onPush does not update child property from parent

I have a child component that looks like this:

@Component({
  selector: 'app-child',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    {{text}}
  `
})
export class ChildComponent {
  @Input() text = '';

  constructor(public host: ElementRef) { }
}

And a parent component that looks like this:

@Component({
  selector: 'app-parent',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<ng-content></ng-content>`
})
export class ParentComponent {
  @ContentChild(ChildComponent) child: ChildComponent;

  constructor(private cdr: ChangeDetectorRef) { }

  ngAfterContentInit() {
    this.child.text = 'hello';
    this.child.host.nativeElement.addEventListener('click', () => {
      this.child.text = 'from click';
      this.cdr.detectChanges();
    });
  }

The first assign to the text property is working fine, but when I click the button and try to change the text property again nothing is happening.

That's confusing because from what I know: 1. The click event should trigger change detection and the text property is different so it should have been updated. 2. I explicitly called detectChanges() and this should check also the children from what I know.

What am I missing?



Solution 1:[1]

The problem is related to this issue reported on GitHub. It occurs when:

  • The OnPush change detection strategy is used for the child component
  • An input property of the child is changed directly in the parent component code instead of being data-bound in the parent component template

The explanation given by AngularInDepth.com:

The compiler doesn't have a way to generate necessary information for checking the bindings since it can't find these bindings in the template. OnPush is tightly bound to the input bindings. What's important is that Angular checks the second part of the binding (prop in the example below), not the first (i):

<child [i]="prop">

to determine whether the change detection should be run for the child component. And it does so when checking parent component. If you don't show the compiler what parent property should be used to update child input binding, it can't generate necessary information used when checking the parent. So inspecting @Input on child components isn't enough. That's the mechanism of change detection and I don't see any way it could be changed.

One workaround suggested by yurzui in the discussion is to call ChangeDetectorRef.markForCheck in the child component after setting the text property, as shown in this stackblitz. As a matter of fact, it works without calling ChangeDetectorRef.detectChanges in the parent component.

export class ChildComponent {

  private _text = '';

  @Input()
  get text() {
    return this._text;
  }
  set text(val) {
    if (this._text !== val) {
      this.cdRef.markForCheck();
    }
    this._text = val;
  }

  constructor(public host: ElementRef, private cdRef: ChangeDetectorRef) { }
}

Solution 2:[2]

write update method in child component:

@Component({
  selector: 'app-child',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    {{text}}
  `
})
export class ChildComponent {
  @Input() text = '';

  constructor(public host: ElementRef, private cd: ChangeDetectorRef) { }
  update(): void{
    this.cd.markForCheck();
  }
}

call that method in parent:

@Component({
  selector: 'app-parent',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<ng-content></ng-content>`
})
export class ParentComponent {
  @ViewChild(ChildComponent) child: ChildComponent;
  constructor(private cdr: ChangeDetectorRef) { }

  ngAfterContentInit() {
    this.child.text = 'hello';
    this.child.host.nativeElement.addEventListener('click', () => {
      this.child.text = 'from click';
      this.child.update();
    });
  }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Hamid Taebi