4

In the following example (plunker), a ui-router state routes to an app component that has a data object and a replace method that replaces this object with a new one using the given value. In its template, it has:

  • an editor component which triggers the replace method through a callback binding ('&')
  • a display component which receives the data object through a 1-way binding ('<'), makes a local copy when the $onChanges lifecycle hook is triggered, and displays the contents of the object

Everything works fine and as expected :-)

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

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

    $stateProvider
    .state('root', {
      url: '/',
      component: 'app'
  });
})

.component('app', {
  controller: function () {
    this.data = { name: 'initial' };
    this.replace = (value) => { this.data = { name: value }; };
  },
  template: `
    <editor on-replace="$ctrl.replace(value)"></editor>
    <display data="$ctrl.data"></display>
  `
})

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

.component('display', {
  bindings: {
    data: '<'
  },
  controller: function () {
    this.$onChanges = (changes) => {
      if (changes.data) {
        this.data = Object.assign({}, this.data);
      }
    };
  },
  template: `
    <p> value : {{ $ctrl.data.name }} </p>
  `
});

I have a problem with the following second example (plunker). It is the exact same setting, except that the app component no longer manages the data itself, but receives a data object through a 1-way binding ('<') that is defined as a resolve of the ui-router state (as can be seen in the comments, I tested both using a global object and method, and interacting through a service). When this resolved object is reassigned, I was expecting the $onChanges hook of the app component to be triggered (as it is for the display component when the data object of the app component is reassigned), but this is not the case. Does anyone has an explanation?

let data = { name: 'initial' };
const replace = (value) => { data = { name: value }; };

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

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

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

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

.component('app', {
  bindings: {
    data: '<'
  },
  controller: function (/*DataService*/) {
    this.$onChanges = (changes) => {
      if (changes.data) {
        this.data = Object.assign({}, this.data);
      }
    };
    this.replace = (value) => { replace(value); }; /*(value) => { DataService.replace(value); };*/
  },
  template: `
    <editor on-replace="$ctrl.replace(value)"></editor>
    <display data="$ctrl.data"></consumer>
  `
})

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

.component('display', {
  bindings: {
    data: '<'
  },
  controller: function () {
    this.$onChanges = (changes) => {
      if (changes.data) {
        this.data = Object.assign({}, this.data);
      }
    };
  },
  template: `
    <p> value : {{ $ctrl.data.name }} </p>
  `
});
Pierre Kraemer
  • 332
  • 1
  • 10

1 Answers1

2

Under the hood, UI-ROUTER creates HTML with a one-time binding to $resolve.data on the parent scope.

<app data="::$resolve.data" class="ng-scope ng-isolate-scope">
  <editor on-replace="$ctrl.replace(value)" class="ng-isolate-scope">
    <input type="text" ng-model="value" class="ng-pristine ng-untouched ng-valid ng-empty">
    <button ng-click="$ctrl.replace(value)"> replace object </button>
  </editor>
  <display data="$ctrl.data" class="ng-isolate-scope">
    <p class="ng-binding"> value : initial </p>
  </display>
</app>

The watcher that invokes the $onChanges hook in the app component watches the $resolve.data property of the parent scope. It does not react to changes to the $ctrl.data propery of the isolate scope. And since it is a one-time binding, the $onChanges hook gets invoked only when the variable gets initialized and not on subsequent changes.


Use a Service with RxJZ

Instead of trying to bend $onChange to do something for which it was not designed, build a service with RxJS Extensions for Angular.

app.factory("DataService", function(rx) {
  var subject = new rx.Subject(); 
  var data = "Initial";

  return {
      set: function set(d){
        data = d;
        subject.onNext(d);
      },
      get: function get() {
        return data;
      },
      subscribe: function (o) {
         return subject.subscribe(o);
      }
  };
});

Then simply subscribe to the changes.

app.controller('displayCtrl', function(DataService) {
  var $ctrl = this;

  $ctrl.data = DataService.get();
  var subscription = DataService.subscribe(function onNext(d) {
      $ctrl.data = d;
  });

  this.$onDestroy = function() {
      subscription.dispose();
  };
});

The DEMO on PLNKR.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • Thank you very much for the insight. In my case, $resolve.data is the global data object (or the one that is get from the service). If I understand well, if the binding of the resolved data was 1-way rather than 1-time binding, the $onChanges hook that I expect would be invoked ? – Pierre Kraemer Jul 28 '16 at 22:12
  • No, the watcher invokes `$onChange` hook when the property on the *parent* scope changes. Changes on the *child* isolated scope of the component are not watched. The `<` binding is one-way from *parent* to *child*. The `$onChange` hook reacts to changes *external* to the component. The resolver functions are only invoked once during the lifetime of a view. A one-time binding is the appropriate choice for doing one-way binding of resolver data. – georgeawg Jul 29 '16 at 03:31
  • 1
    But the `data`object that is resolved _is_ external to the `app` component. It is either a global variable or comes from the `DataService`. So if it was 1-way binded, I do not see why a reassignment of this object (in the global or in the service scope) would not trigger the `$onChanges` hook of the `app` component? – Pierre Kraemer Jul 29 '16 at 06:22
  • There are multiple *variables* referring to the same object. When you update the reference that the *global variable* points to, it does not update what the other variables point to. The resolve.data variable still references the old object. – Chris T Jan 11 '17 at 22:03