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:
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>
`
});
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.
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.