16

I have a simple custom directive with an input, that I'm binding to in my component. But for whatever reason, the ngOnchanges() method doesn't fire when changing a child property of the input property.

my.component.ts

import {Component} from 'angular2/core';
import {MyDirective} from './my.directive';

@Component({
    directives: [MyDirective],
    selector: 'my-component', 
    templateUrl: 'Template.html'
})

export class MyComponent {
    test: { one: string; } = { one: "1" }

    constructor( ) {
        this.test.one = "2";
    }
    clicked() {
        console.log("clicked");
        var test2: { one: string; } = { one :"3" };
        this.test = test2; // THIS WORKS - because I'm changing the entire object
        this.test.one = "4"; //THIS DOES NOT WORK - ngOnChanges is NOT fired=
    }
}

my.directive.ts

import {Directive, Input} from 'angular2/core';
import {OnChanges} from 'angular2/core';

@Directive({
    selector: '[my-directive]',
    inputs: ['test']
})

export class MyDirective implements OnChanges {
    test: { one: string; } = { one: "" }

    constructor() { }

    ngOnChanges(value) {
        console.log(value);
    }
}

template.html

<div (click)="clicked()"> Click to change </div>
<div my-directive [(test)]="test">

Can anyone tell me why?

Dynde
  • 2,592
  • 4
  • 33
  • 56

1 Answers1

16

In fact, it's a normal behavior and Angular2 doesn't support deep comparison. It's only based on reference comparison. See this issue: https://github.com/angular/angular/issues/6458.

That said they are some workarounds to notify the directive that some fields in an object were updated.

  • Referencing the directive from the component

    export class AppComponent {
      test: { one: string; } = { one: '1' }
      @ViewChild(MyDirective) viewChild:MyDirective;
    
      clicked() {
        this.test.one = '4';
        this.viewChild.testChanged(this.test);
      }
    }
    

    In this case, the testChanged method of the directive is called explicitly. See this plunkr: https://plnkr.co/edit/TvibzkWUKNxH6uGkL6mJ?p=preview.

  • Using an event within a service

    A dedicated service defines testChanged event

    export class ChangeService {
      testChanged: EventEmitter;
    
      constructor() {
        this.testChanged = new EventEmitter();
      }
    }
    

    The component uses a service to trigger the testChanged event:

    export class AppComponent {
      constructor(service:ChangeService) {
        this.service = service;
      }
    
      clicked() {
        this.test.one = '4';
        this.service.testChanged.emit(this.test);
      }
    }
    

    The directive subscribes to this testChanged event in order to be notified

    export class MyDirective implements OnChanges,OnInit {
      @Input()
      test: { one: string; } = { one: "" }
    
      constructor(service:ChangeService) {
        service.testChanged.subscribe(data => {
          console.log('test object updated!');
        });
      }
    }
    

Hope it helps you, Thierry

Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Neither of these seem to work for me. I tried explicitly setting the testChange emitter to @Ouput() and tried being type-specifc with the value, but it just doesn't fire the ngOnchanges in the directive. – Dynde Jan 13 '16 at 10:59
  • Which version of Angular2 do you use because I made it work with the beta0 one? – Thierry Templier Jan 13 '16 at 11:03
  • I'm also on beta.0. Your testChange declaration is invalid for me as well (have to either assign or forego the type declaration and go straight to: testChange = new EventEmitter();) Am I supposed to subscribe in some special way in my directive for it to register the eventEmitter? – Dynde Jan 13 '16 at 11:07
  • No, the event is detected by Angular itself and it triggers the `onChanges` method... – Thierry Templier Jan 13 '16 at 11:19
  • I hust saw in your code that you don't use an `@Input` for your `test` field within the `MyDirective` component... – Thierry Templier Jan 13 '16 at 11:22
  • That's because I declared the inputs in the inputs array in the @Component decorator (I have also tried it with the @Input()) – Dynde Jan 13 '16 at 12:19
  • Doh - of course your code works, I wasn't paying attention. In your code you specifically set this.test to a NEW object - just like the code I have that works. When you ONLY change the child property, it doesn't work – Dynde Jan 13 '16 at 12:52
  • Oh my god! You're right... I investigated a bit more and get some feedbacks from angular github. I updated my answer accordingly. – Thierry Templier Jan 13 '16 at 14:21
  • 3
    I see... Well, that's disappointing. Honestly, I think it should be possible to decide whether or not you need deep-comparison. The performance can't be that bad, if pretty much every other js framework supports it. – Dynde Jan 14 '16 at 09:42