4

EDIT: I have reformulated this question here: angular 1.5 component, ui-router resolve, $onChanges lifecycle hook

I will illustrate my question with 3 successive examples.

In the first one, a so-called outer component owns a myData object that has a name field. This object is passed to an inner component using one-way binding. The outer component exposes two methods for updating its object: modify that changes the value of the name field, and replace that replaces it with a new object. When modify is called, the object seen in the inner component is updated because one-way binding is done by reference. When replace is called, the object seen in the inner component is updated too because one-way binding watches reference modification, and the $onChanges hook is called. Everything is fine here :-)

angular
.module('app', [])

.component('outer', {
    controller: function () {
      this.myData = {
        name: 'initial'
      };
      this.modify = (value) => { this.myData.name = value; };
      this.replace = (value) => { this.myData = { name: value }; };
    },
  template: `
    <p> outer value : {{ $ctrl.myData.name }} </p>
    <input type="text" ng-model="value">
    <button ng-click="$ctrl.modify(value)"> modify object </button>
    <button ng-click="$ctrl.replace(value)"> replace object </button>
    <hr>
    <inner my-data="$ctrl.myData"></inner>
  `
})

.component('inner', {
  bindings: {
    myData: '<'
  },
  controller: function () {
    this.nbOnChangesCalls = 0;
    this.$onChanges = (changes) => { this.nbOnChangesCalls++; }
  },
  template: `
    <p> inner value : {{ $ctrl.myData.name }} (nb onChanges calls : {{ $ctrl.nbOnChangesCalls }}) </p>
  `
});

In the second one (https://plnkr.co/edit/uEp6L4LuWqTJhwn6uoTN?p=preview), the outer component no longer declares myData object but receives it as a one-way binding that comes from a resolve of a ui-router component routed state. As the modify and replace methods are still done in the same way within the outer controller, everything behaves like in the previous example.

angular
.module('app', ['ui.router'])

.config(($urlRouterProvider, $stateProvider) => {
    $urlRouterProvider.otherwise('/');

    $stateProvider
    .state('root', {
      url: '/',
      component: 'outer',
      resolve: {
        myData: () => ({ name: 'initial' })
      }
  });
})

.component('outer', {
  bindings: {
    myData: '<'
  },
  controller: function () {
    this.nbOnChangesCalls = 0;
    this.$onChanges = (changes) => { this.nbOnChangesCalls++; }
    this.modify = (value) => { this.myData.name = value; };
    this.replace = (value) => { this.myData = { name: value }; };
  },
  template: `
    <p> outer value : {{ $ctrl.myData.name }} (nb onChanges calls : {{ $ctrl.nbOnChangesCalls }}) </p>
    <input type="text" ng-model="value">
    <button ng-click="$ctrl.modify(value)"> modify object </button>
    <button ng-click="$ctrl.replace(value)"> replace object </button>
    <hr>
    <inner my-data="$ctrl.myData"></inner>
  `
})

.component('inner', {
  bindings: {
    myData: '<'
  },
  controller: function () {
    this.nbOnChangesCalls = 0;
    this.$onChanges = (changes) => { this.nbOnChangesCalls++; }
  },
  template: `
    <p> inner value : {{ $ctrl.myData.name }} (nb onChanges calls : {{ $ctrl.nbOnChangesCalls }}) </p>
  `
});

In the third example (https://plnkr.co/edit/nr9fL9m7gD9k5vZtwLyM?p=preview), a service is introduced which owns the myData object and exposes the modify and replace methods. The resolve of the ui-router state now calls the service to get the data object. The outer controller calls the modify and replace methods of the service. Everything is fine until the first call to the replace method. I was expecting the $onChanges hook of the outer component to be triggered when the reference of the object within the service is changed but this is not the case.

angular
.module('app', ['ui.router'])

.factory('DataService', () => {
  let data = { name: 'initial' };
  return {
    getData: () => data,
    modify: (value) => { data.name = value; },
    replace: (value) => { data = { name: value }; }
  }
})

.config(($urlRouterProvider, $stateProvider) => {
    $urlRouterProvider.otherwise('/');

    $stateProvider
    .state('root', {
      url: '/',
      component: 'outer',
      resolve: {
        myData: (DataService) => DataService.getData()
      }
  });
})

.component('outer', {
  bindings: {
    myData: '<'
  },
  controller: function (DataService) {
    this.nbOnChangesCalls = 0;
    this.$onChanges = (changes) => { this.nbOnChangesCalls++; }
    this.modify = (value) => { DataService.modify(value); };
    this.replace = (value) => { DataService.replace(value); };
  },
  template: `
    <p> outer value : {{ $ctrl.myData.name }} (nb onChanges calls : {{ $ctrl.nbOnChangesCalls }}) </p>
    <input type="text" ng-model="value">
    <button ng-click="$ctrl.modify(value)"> modify object </button>
    <button ng-click="$ctrl.replace(value)"> replace object </button>
    <hr>
    <inner my-data="$ctrl.myData"></inner>
  `
})

.component('inner', {
  bindings: {
    myData: '<'
  },
  controller: function () {
    this.nbOnChangesCalls = 0;
    this.$onChanges = (changes) => { this.nbOnChangesCalls++; }
  },
  template: `
    <p> inner value : {{ $ctrl.myData.name }} (nb onChanges calls : {{ $ctrl.nbOnChangesCalls }}) </p>
  `
});

What am I doing wrong here ?

Precision: things are working fine when using modify method. However, I would like to be able to perform local copies of the binded object within the components so as to avoid modifying the original object from there (ensuring a real one-way data flow) and use the $onChanges hook to be informed of object modification (re-assignement).

Edit: I have come up with the following code (https://plnkr.co/edit/DZqgb4aIi09GWiblFcmR?p=preview) where the component that gets the data in the resolve is not the one that triggers the modification in the service. The question is then: why the $onChanges hook is not triggered in the consumer component when the data object that was resolved is reassigned ? How could this component be informed of the modification ?

angular
.module('app', ['ui.router'])

.factory('DataService', () => {
  let data = { name: 'initial' };
  return {
    getData: () => data,
    modify: (value) => { data.name = value; },
    replace: (value) => { data = { name: value }; }
  }
})

.component('editor', {
  controller: function (DataService) {
    this.modify = (value) => { DataService.modify(value); };
    this.replace = (value) => { DataService.replace(value); };
  },
  template: `
    <input type="text" ng-model="value">
    <button ng-click="$ctrl.modify(value)"> modify object </button>
    <button ng-click="$ctrl.replace(value)"> replace object </button>
  `
})

.config(($urlRouterProvider, $stateProvider) => {
    $urlRouterProvider.otherwise('/');

    $stateProvider
    .state('root', {
      url: '/',
      component: 'consumer',
      resolve: {
        myData: (DataService) => DataService.getData()
      }
  });
})

.component('consumer', {
  bindings: {
    myData: '<'
  },
  controller: function () {
    this.nbOnChangesCalls = 0;
    this.$onChanges = (changes) => { this.nbOnChangesCalls++; }
  },
  template: `
    <p> value : {{ $ctrl.myData.name }} (nb onChanges calls : {{ $ctrl.nbOnChangesCalls }}) </p>
  `
});

Ideally, the $onChanges method of the consumer component would be implemented this way:

this.$onChanges = (changes) => {
  if (changes.myData) this.myData = Object.assign({}, this.myData);
  this.nbOnChangesCalls++;
}

In this case, the modify version does not work (as expected because the component do not have a reference on the service data object anymore) and as the $onChanges hook is not triggered (which is my main question here!) the replace version does not work either..

Community
  • 1
  • 1
Pierre Kraemer
  • 332
  • 1
  • 10

0 Answers0