I am using controllerAs syntax like so:
.state('operators', {
url: '/operators',
templateUrl: 'app/features/operators/operators.html',
controller: 'OperatorsController',
controllerAs: 'vm'
})
And have a variable in my controller vm.selectedOperators which I want to watch for changes on:
var vm = this;
vm.selectedOperators = [];
$scope.$watch(function() {
return vm.selectedOperators;
}, function(current) {
console.log(current.length);
});
In the view, an expression is used to show the count of items in the variable.
{{vm.selectedOperators.length}} item selected
This variable gets updated from elsewhere (Its being used by a multi-select table directive I have written which is used on the same view) and I see this expression updating fine in the view.
The problem is my watcher in the controller doesn't fire after the variable gets initialised, and I am not sure why.
Can anyone offer any help?
Thanks
UPDATE
I should have mentioned, I have already tried using the expression equivalent like so:
$scope.$watch('vm.selectedOperators', function (current){
console.log(current);
});
But this still does not fire for me
Concept: There is an optional third argument to the $watch function - objectEquality
$watch(watchExpression, listener, [objectEquality]);
You must set it to true when you are trying to watch an object otherwise angular will only match the reference of both the objects. By setting it true angular compares the old object and new using angular.equals
Read more about it here:
https://docs.angularjs.org/api/ng/type/$rootScope.Scope
$scope.$watch(function() {
return vm.selectedOperators;
}, function(current) {
console.log(current.length);
}, true);
Update
As #Martijn Welker pointed out in the comment below, $watchCollection can be useful if only single level equality has to be matched instead of complete object tree.
$scope.$watchCollection(function() {
return vm.selectedOperators;
}, function(current) {
console.log(current.length);
});
Try this (ES6 syntax):
$scope.$watch('vm.selectedOperators', (new, old) => {
console.log('new:', new, 'old:', old);
});
You should be able to see the changes.
Try using Deep Watch Array object..
Deep watch will watch each object in array list. It's little bit expensive in process than normal watch So use if normal watch doesn't work:-
Here is code
$scope.$watch('vm.selectedOperators', function (current) {
console.log(current.length);
},true);
Related
I have an angular component:
module.exports = {
bindings: {
vulnerability: '<',
},
controller: ['$scope', function($scope){
//want to get value of vulnerability from above
$scope.vulnerability // doesn't work
}],
};
which i reference in an ng-repeat
<div ng-repeat="vulnerability in vulnerabilities" class="vulnerability-item">
<vulnerability-item vulnerability="vulnerability"> </vulnerability-item>
</div>
The value of vulnerability isn't going to change once the directive is called so I don't need to watch that value, just to reference it once it gets set.
I did put a $scope.$watch on the property just to see if it worked and it did trigger but I couldn't see the new or old value in the handler of the watch so it didn't help me anyway.
I tried $scope.vulnerability and $ctrl.vulnerability which is how I'd reference that property in the directives template but neither worked. How do I get the bound value that is passed in?
adding this watch:
$scope.$watch('vulnerability', function (oldV, newV) {
console.log('old', oldV)
console.log('new', newV)
})
I get a new undefined and old undefined in the console for each instance of the component although if i change $scope.$watch('vulnerability' to $scope.$watch('nonsense') It still fires the logs once for each component.
If the value isn't supposed to be changed, it should be bound once, vulnerability="::vulnerability". This gives the context to the question (and also saves some time on optimization).
In this case $onInit hook may be used:
...
controller: function() {
this.$onInit = function () {
this.vulnerability
}
},
I have this component in my app:
<actions-bar actions="$ctrl.actionsBarData"></actions-bar>
Here is the controller / component definition:
.component('actionsBar', {
controller: 'actionsBarController',
bindings: {
actions: '<'
}
})
.controller('actionsBarController', function() {
var vm = this;
vm.$onInit = function() {
console.log(vm.actions);
setTimeout(function() {
console.log(vm.actions);
}, 500);
};
});
Take note of the two console.log statements within $onInit. In the browser the first logging statement prints undefined. The second statement, wrapped in setTimeout correctly prints out the this.actions object.
According to the Angular docs:
$onInit() - Called on each controller after all the controllers on an
element have been constructed and had their bindings initialized
If the bindings have been initialized, why is the data not immediately available? Why must I set a delay of 500ms before it is available?
If $onInit isn't the solution here, how can I reliably access the bindings data within my controller? It appears that this is correct lifecycle hook to use, and none of the other hooks appear to be what I want in this case.
This is because while the bindings have been initialized in the controller, they haven't necessarily been initialized where they are coming from. If you are populating your binding with an async call, the object is going to be undefined or empty.
$onChanges allows you to inspect changes that happen to your bindings.
vm.$onChanges = function (changesObj) {
console.log(changesObj.actions);
}
If what I described above is happening, this will fix it for you.
EDIT: a word
I don't understand why $onChanges isn't kicked off when I change a bound primitive in an input. Can someone see what I've done wrong, and explain this in an uncomplicated way? I made a plunkr of a quick test application after I couldn't get it to work in my actual application either.
angular
.module('test', [])
.component('test', {
template: '<child application="vm.application"></child>',
controller: 'testCtrl as vm'
})
.controller('testCtrl', function() {
var vm = this;
vm.$onInit = function () {
vm.application = {
data: {
name: 'Test'
}
}
};
})
.component('child', {
template: '<input type="text" ng-model="vm.application.data.name">',
bindings: {
application: '<'
},
controller: 'childCtrl as vm'
})
.controller('childCtrl', function() {
var vm = this;
vm.$onChanges = function (changes) {
console.log('CHANGED: ', changes);
};
})
The $onChanges method is not called for changes on subproperties of an object. Default changes to objects generally follow this sequence within a components lifetime:
UNINITIALIZED_VALUE to undefined
undefined to {} or { someAttribute: someValue, .. }
({..} to undefined if you delete the object in a parent scope)
In order to watch subproperties you could use the $doCheck method that was added in 1.5.8. It is called on every digest cycle and it takes no parameters. With great power comes great responsibility. In that method you would put logic that detects whether a certain value has been updated or not - the new value will already be updated in the controller's scope, you just need to find a way to determine if the value changed compared to the previously known value.
You could set a previousValueOfObjectAttribute variable on the controller before you start to expect changes to this specific attribute (e.g. when subcomponent B calls an output binding function in component A, based on which the target object - which is an input binding to B - in A changes). In cases where it is not predictable when the change is about to occur, you could make a copy of the specific atributes of interest after any change observed via the $doCheck method.
In my specific use case, I did not explicitly check between an old and new value, but I used a promise (store $q.defer().promise) with the intention that any change I would 'successfully' observe in the $doCheck method would resolve that promise. My controller then looked something like the following:
dn.$doCheck = function () {
if (dn.waitForInputParam &&
dn.waitForInputParam.promise.$$state.status === 0 &&
dn.targetObject.targetAttribute !== false)
dn.waitForInputParam.resolve(dn.targetObject.targetAttribute);
}
dn.listenToInputChange = function () {
dn.waitForInputParam = $q.defer();
dn.waitForInputParam.promise.then(dn.onInputParamChanged);
}
dn.onInputParamChanged = function (value) {
// do stuff
//
// start listening again for input changes -- should be async to prevent infinite $digest loop
setTimeout(dn.listenToInputChange, 1);
}
(w.r.t. promise.$$state.status, see this post).
For all other intents and purposes, watching changes to primitive data types, you should still use $onChanges. Reference: https://docs.angularjs.org/guide/component
It's $onChanges and not $onChange.
Also, the onChange only updates when the parent value is changed, not the child. Take a look at this plunkr. Note the console.log only fires when you type in the first input.
As others said above, Angular does not watch for changes in object properties, however, you can make Angular believe that your object is changed by reference.
It is sufficient to do a shallow copy of the object in order to trigger an $onChanges event:
vm.campaign = angular.extend({}, vm.campaign);
Credits to #gkalpak
Dealing with $onChanges is tricky. Actually, thats why in version 1.5.8 they introduced the $doCheck, similar to Angular 2 ngDoCheck.
This way, you can manually listen to changes inside the object being listened, which does not occur with the $onChanges hook (called only when the reference of the object is changed). Its the same thing, but it gets called for every digest cycle allowing you to check for changes manually (but better then watches).
For more details, see this blog post.
I'm having problems to watch multiple variables in a Service when some changes are made on it by other controllers.
I've the following:
angular.module('carApp').controller('CarListCtrl', function ($scope, CarService) {
$scope.carList = CarService.carList
$scope.initalAmount = CarService.initialAmount
$scope.startDate = CarService.startDate
$scope.$watchGroup(['carList', 'initialAmount', 'startDate'],
function (newValues, oldValues, $scope) {
console.log(newValues);
});
});
Other controllers update the values in the Service all the time but the watchGroup never fires up...
I've create a simple watch targeting the service directly to check if work, and it's working ..., so I imagine that the watchGroup should target the service variable directly but I can't find how to do that....
here is the simple watch that works:
$scope.$watch(function () {
return CarService.carList
}, function (newVal, oldVal) {
console.log(newVal);
});
What should I do to make it works with the multiple service variables?
UPDATE 1:
Just a update... if I try the watchgroup with just one element, for example $scope.$watchGroup(['carList'], ... it works, so I tried with each one and it works every time, but as soon as I add one more element it stop working... very annoying...
Tks again guys!
Just to close this, people from angularjs github help me out: here is the anwser for anyone who need:
Each value inside a watchGroup array can be a expression or a function, so you can use three different functions inside your watchGroup. http://plnkr.co/edit/nMmPt808xAFXqjJ6yEoc?p=preview
$scope.$watchGroup([
function() {
return myService.valueOne()
},
function() {
return myService.valueTwo()
}
], function(newValues, oldValues) {
$scope.valueOne = newValues[0]
$scope.valueTwo = newValues[1]
})
Your first example is possibly not working because your other controllers assign new values to initialAmount and startDate in your service, which means you have different objects with different values in your controller and your startDate. It´probably works with the carList, because you are only adding / removing items, which means it remains the same object in your controller and your service.
This works:
$scope.$watch('cities', function(){
alert("fire");
}, true);
This dosen't work:
$scope.$watchCollection('[cities]', function(){
alert("fire")
}, true);
Any ideas what can be wrong?
$scope.cities is hooked up to some checkboxes.
The documentation of $watchCollection states that it shallow watches the object passed to it and fires whenever the object changes. So in case of arrays, it fires when any of the elements changes, but not when some deep property inside the element changes. Also, it doesn't take a third argument (as $watch does for objectEquality). In your case, the object cities probably never changes in reference.
However, if you had:
$scope.cityList = [cities];
$scope.$watchCollection('cityList', function () { alert('fire'); });
// As a response to some event:
$scope.cityList[0] = newCities;
then the watcher should fire.