9

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?

undefined
  • 6,366
  • 12
  • 46
  • 90
  • 2
    I'm not sure, but since `text` is an `@Input` property, Angular probably expects you to be passing this value from parent to child, rather than modifying it directly from the parent. – Frank Modica Apr 16 '18 at 20:01
  • Don't think it's the problem, because the first call is working. – undefined Apr 16 '18 at 20:02
  • Yeah but the first run of change detection tends to be the edge case or exception to the rules, but let's see if someone can explain this in more depth. – Frank Modica Apr 16 '18 at 20:03

2 Answers2

11

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) { }
}
ConnorsFan
  • 70,558
  • 13
  • 122
  • 146
  • 1
    Why do I need to call markForCheck and not detectChanges? – undefined Apr 17 '18 at 05:36
  • Take a look at [this answer](https://stackoverflow.com/a/45396740/1009922) and at the other ones in the same question. My understanding is that change detection is triggered by the click event but the `text` property of the child is not included in it (because of the `OnPush` strategy) unless you call `markForCheck` explicitely. – ConnorsFan Apr 17 '18 at 11:56
  • You can also use @ViewChild(MyChildComponent) to resolve the issue ;) – alexino2 Apr 22 '22 at 03:12
0

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();
    });
  }
Hamid Taebi
  • 357
  • 2
  • 12