10

Can you programatically trigger angular's change detection when mutating a component property in angular2?

@Component({
   selector: 'my-component', 
})
class MyComponent implements OnChanges {
   @Input() message: string;

   ngOnChanges(changeRecord) {
      for (var change in changeRecord) {
         console.log('changed: ' + change);
      }
   }

   doSomething() {
     // I want ngOnChanges to be called some time after I set the 
     // message. Currently it is only called if the host element
     // changes the value of [message] on the element.
     this.message = 'some important stuff';
   }
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
ovangle
  • 1,991
  • 1
  • 15
  • 29
  • 3
    To manually run change detection, try using ApplicationRef.tick(). However, I don't believe ngOnChanges() will be called, since the host property didn't change. Why are you changing a child property in the child component that is an input property? – Mark Rajcok Jan 02 '16 at 03:35
  • The code above isn't the actual code, I was just using it to demonstrate the issue. My real code is a unit test and I've obtained an instance of the component using `TestComponentBuilder`. I want the unit test code to modify the value of the `message` property and to check that an event is emitted from the code in `ngOnChanges`. But after a couple of hours head scratching I can't figure out any way to accomplish that. I've also tried modifying the `nativeElement` on the `DebugElement` to no effect. – ovangle Jan 02 '16 at 04:12
  • I was looking for a method like `ComponentFixture.modifyHostBinding(property: string, newValue: any)`, or something similar, but there doesn't seem to be anything like it, so I posed a more generic question. – ovangle Jan 02 '16 at 04:21
  • Did you get the answer? – Murhaf Sousli Apr 25 '16 at 11:34
  • what you want to do here is create a dummy host component like `TestHostComponent` that will bind `message` property as per norm. Then check for the change to the `message` property using fixture.detectChanges() in your test. see related: http://stackoverflow.com/questions/37408801/testing-ngonchanges-lifecycle-hook-in-angular-2 – parsethis Nov 30 '16 at 20:28
  • Yes, this question is outdated now that `@angular/core/testing` was refactored in RC.4 or 5. – ovangle Dec 02 '16 at 08:56

4 Answers4

5

I was having the same issue, and this is a simple but not very elegant workaround I am using. Pass in another property to force trigger ngOnChanges method

<div poll-stat-chart [barData]="barData" [changeTrigger]="changeTrigger"></div>

In the parent component class, whenever you want to manually fire the ngOnChanges method on child component, just modify "changeTrigger" property

ParentComponent Class (poll-stat-chart is the child component)

     @Component({
        directives: [PollStatChartCmp],
        template: `
            <div poll-stat-chart [barData]="barData" [changeTrigger]="changeTrigger">
            </div>
            <button (click)="triggerChild()"></button>
        `
      }
    export class ParentComponent {
        changeTrigger = 1;
        barData = [{key:1, value:'1'}, {key:2, value'2'}];
        triggerChild() {
            this.barData[0].value = 'changedValue';

            //This will force fire ngOnChanges method of PollStatChartComponent
            this.changeTrigger ++ ;           
        }

    }

And then in child component class, add a property [changeTrigger]

    @Component({
        selector: '[poll-stat-chart]',
        inputs: ['barData', 'changeTrigger'],
        template: `
            <h4>This should be a BAR CHAR</h4>
        `
    })
    export class PollStatChartCmp {
        barData;
        changeTrigger;
        constructor(private elementRef: ElementRef) {
            this.render();

        }

        ngOnChanges(changes) {
            console.log('ngOnChanges fired');
            this.render();
        }

        render() { console.log('render fired');}

}
Luca Ritossa
  • 1,118
  • 11
  • 22
Yong Wang
  • 5,812
  • 2
  • 12
  • 13
4

Trying to manually call change detection or spent a lot of time on a workaround for this is way overkilling, why not creating a function to handle the desired mutation and call it in both ngOnChanges and doSomething? something like:

@Component({
  selector: 'my-component',
})
class MyComponent implements OnChanges {
  @Input() message: string;
  viewMessage: string;

  ngOnChanges(changes: { [propertyName: string]: SimpleChange }) {
    for (let propName in changes) {
      if (propName === 'message') {
        this.updateView(this.message);
      }
    }
  }

  doSomething() {
    this.viewMessage = 'some important stuff';
  }

  updateView(message: string) {
    this.viewMessage = message;
  }
}

So viewMessage will be the attribute you'll be using and controlling the template.

RobC
  • 22,977
  • 20
  • 73
  • 80
3

The reason it doesn’t work can be found in the source code.

https://github.com/angular/angular/blob/885f1af509eb7d9ee049349a2fe5565282fbfefb/packages/core/src/view/provider.ts

Where ngOnChanges is called from, and where the SimpleChanges structure is built are very much tied into the component / directive code.

It’s not just a ‘change tracker’ running that looks over every property however it was set, so ngOnChanges only works for bindings set by parent components.

This is where ngDoCheck comes in and possibly KeyValueDiffers.

See also:

https://netbasal.com/angular-the-ngstyle-directive-under-the-hood-2ed720fb9b61 https://juristr.com/blog/2016/04/angular2-change-detection/

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
1

There seems to be no way to modify an input binding on this and have it detected during change detection. However it was possible to fix the unit test I was writing by wrapping the whole component in another component

@ng.Component({
    selector: 'my-host-component',
    template: '<my-component [message]="message" (change)="change.emit($event)"></my-component>'
    directives: [MyComponent]
})
class MyHostComponent {
   message: string;
   change = new EventEmitter<any>();
}

I then ran the test on MyHostComponent, rather than MyComponent.

I've submitted an issue to angular requesting that a method be added to ComponentFixture so that tests like this are easier to write.

ovangle
  • 1,991
  • 1
  • 15
  • 29