ovangle
ovangle

Reputation: 2031

ngOnChanges not firing when input property changed

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';
   }
}

Upvotes: 11

Views: 29205

Answers (4)

Simon_Weaver
Simon_Weaver

Reputation: 146218

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/

Upvotes: 3

Fernando Figueroa
Fernando Figueroa

Reputation: 41

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.

Upvotes: 4

Yong Wang
Yong Wang

Reputation: 7860

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');}

}

Upvotes: 5

ovangle
ovangle

Reputation: 2031

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.

Upvotes: 1

Related Questions