angularjs $watch cannot detect change of Property in an object - javascript

I'm setting watch for $scope object will not trigger $watch change event unless the whole value is changed
Example :
//Some where in .run function i set rootScope and set $watch too.
$rootScope.config = {date:'11/4/2015',moreValues:'etc'};
//setting $watch
$rootScope.$watch('config',function(new,old) {
console.log('config value changed :)',new);
});
//----->Inside Controller----------
//NOw THIS WILL NOT TRIGGER $watch
$rootScope.config.date = 'another day';
//Same as this, it will also not trigger $watch
var temp = $rootScope.config;
temp.date = 'another day';
$rootScope.config = temp;
//YET THIS WILL WORK JUST FINE :) AND WILL TRIGGER $watch
var temp = angular.copy($rootScope.config);
temp.date = 'another day';
$rootScope.config = temp;
can someone tell me why is this behavior? is there a better way to trigger $watch on change of object Property?

You can use $watchCollection, or pass a third param as true
$rootScope.$watch('config',function(value,old) {
console.log('config value changed :)',value);
}, true);
or
$rootScope.$watchCollection('config',function(value,old) {
console.log('config value changed :)',value);
});
Scope $watch() vs. $watchCollection() In AngularJS

Related

How to update the scope value without using $watch

I have using custom directive
<users stats="stats"></users>
When we change the scope object from main controller, i have updating directive scope value also
app.directive('users', function(){
var directive = {};
directive.restrict = "E";
directive.templateUrl = "templates/partials/users_stats.html";
directive.scope = {stats:'='};
directive.controller = "userStatsCtrl"
return directive;
});
So, Inside the directive controller, i am doing some func. like
app.controller("userStatsCtrl", function($scope){
$scope.$watch('stats', function(newValue) {
if (angular.isDefined(newValue)) {
.....
}
});
})
So here, i am using $watch to listen the scope, If it is update, I will do the some perform.
my question is, I dont want to use watch to listen the scope, If scope gets updated, i need to do some perform.
So how to update scope value without using scope.$watch
When ever "scope.stats" value get changed in your main controller, you can broadcast an event and receive the same event in your directive and do operation what ever you want.
Example code for broadcast an event:
$scope.$broadcast('yourEventName');
Receive an event in directive:
$scope.$on('yourEventName', function(){
});
how about trying:
<users stats="stats" ng-change="onUserStatChange($event)"></users>
or
<users stats="stats" ng-blur="onUserStatChange($event)"></users>
and then in controller:
$scope.onUserStatChange = function(event) {
//event.whatever
}

AngularJS two controllers with shared model, controller 2 not seeing change to model

Hitting the ceiling of my Angular knowledge and I have been going around in circles on this.
Basically I have video player and chapter list directives, each with a controller. The controllers use the same model service which looks like this:
.service('VideoPlayerModel', function(){
var model = this;
model.chapters = {
chapterPos: 0,
targetChapter:null,
data: []
},
model.getVideoData = function() {
return model.videoData;
};
model.setVideoData = function(vData){
...
...
...
};
});
In the video player controller as the time of the player updates it finds the needed chapter data and updates the model.chapters data like this:
updateChapter: function(currentTime){
var chapters = VideoPlayerModel.chapters;
var chaptersCtrl = videoPlayerCtrl.chapters;
if (chapters.nextChapter.start <= currentTime) {
chapters.chapterPos = chapters.chapterPos + 1;
chaptersCtrl.setChapter(); //This finds and sets the Target Chapter
}
},
After setChapter runs I call console.log(VideoPlayerModel.chapters) and I can see the data model has updated with a result like this:
Object {chapterPos: 1, targetChapter: Object, data: Array[6], nextChapter: Object}
However the watch in the ChapterListCtrl doesn't fire and any of the onscreen items displaying the ChapterPos still show just the initial val of 0.
The controller looks like this:
.controller("ChapterListCtrl", ['$scope', 'VideoPlayerModel', function($scope, VideoPlayerModel) {
$scope.chapters = VideoPlayerModel.chapters;
$scope.$watch(function() { return VideoPlayerModel.chapters; }, function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
}])
I have tried different ways and ended up with this, not sure if I am in the complete wrong direction now. Can anyone please help?
You don't need to use $watch, $broadcast or $on. This is best solved by regular JavaScript thinking.
Your problem is $scope.chapters = newValue; That is where you break the binding that your controllers use by introducing a new object unrelated to your service.
What you should to instead is to think about your service model.chapters = {..} and say hey! This is THE ONE object that I will use. And if I want to change the data in this object anywhere, I will switch the data inside the object and NOT assign a new object to the reference I use.
To do this I use the following methods:
transferDataList = function (from, to) {
/*
http://stackoverflow.com/questions/1232040/empty-an-array-in-javascript
*/
to.length = 0;
for (var i = 0; i < from.length; i++) { to.push(from[i]); }
};
transferDataMap = function (from, to) {
/*
http://stackoverflow.com/questions/684575/how-to-quickly-clear-a-javascript-object
*/
var member;
for (member in to) { delete to[member]; }
for (member in from) { to[member] = from[member]; }
};
And when I want to change the data in my object I DON'T do:
$scope.chapters = newValue;
Instead I do:
transferDataMap(newValue, $scope.chapters);
Or:
transferDataList(newValue, $scope.chapters);
This way you will keep your binding and your Angular interfaces will always be updated.
You can use $broadcast() and $on() function to achieve your requirement.
$broadcast() will flush an event to all it's child controller. So, you can $broadcast() an event with your new value to all controllers when you set a new value to your shared model.
Add a broadcast method in your shared service.
model.setVideoData = function(vData){
UpdateYourModel();
// Inform that your model is updated
$rootScope.$broadcast('modelUpdated');
}
And now you can add a listener for the event modelUpdated in all your controllers.
$scope.$on('modelUpdated', function () {
$scope.controllerModel = VideoPlayerModel.getVideoData(); // Update your controller model
}
And also, inject $rootScope to your service,
.service("VideoPlayerModel", ["$rootScope", function($rootScope){
// define your service here.
}] );
That's all !!!
I hope this will help you.
Try changing your watcher to:
$scope.$watch('chapters', function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
Alternatively if that doesn't achieve what you want, you can enable a deep watch by passing the third argument:
$scope.$watch('chapters', function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
}, true);
Your watcher doesn't fire because it always returns the same chapters which Angular considers as not-changed because it checks by reference. Your watcher can also be refactored as:
$scope.$watch(function() { return VideoPlayerModel.chapters.length; }, function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});

Dynamic Updates and AngularJS

I have this...
<script> var num = 22;</script>
Then inside of the controller block...
<span>{{somenumber}}</span>
In the controller...
$scope.somenumber = num;
This all works as expected.
How would I go about having it all update if the value of the num variable changes? So, I'd have some code (from socket.io or AJAX) change num to 65. Right now, it still says 22.
I'd take a look at this
num is a primitive type (Number). So When you're assigning it to the $scope you're copying it. What you need to do is reference it instead. I'd fix it the following way.
<script>var value = {num: 22} </script>
$scope.value = value;
<span> {{value.num}} </span>
If your ajax call is not through $http.(outside angular - wherever you set value.num) you'll need to invoke a digest cycle. The easiest way to do that is in an angular service like $timeout.
Think of the scope as
$scopeHAS model instead of $scopeAS model
You could use $watch followed by $apply:
Controller
$scope.somenumber = num;
$scope.$watch(function() {
return num;
}, function(newValue, oldValue) {
$scope.somenumber = newValue;
});
// fake external change to the 'num' variable
setTimeout(function() {
num = 33;
$scope.$apply();
}, 3000);
Here's a working example: http://plnkr.co/edit/rL20lyI1SgS6keFbckJp?p=preview
If your external change is happening outside the scope of a single controller, I would use $rootScope inside a run callback:
angular.module('exampleApp', []).run(function($rootScope) {
// same as above, just with $rootScope
});

AngularJS - triggering $watch synchronously

I am $watching an object for changes and setting $scope.changed = true, however there are circumstances where i want to set it to false right after I've altered the data programatically:
$scope.$watch('data', function() {
$scope.changed = true;
}, true);
function loadData(data) {
$scope.data = data;
// I want to run $scope.$apply() here so the $watch is triggered
// before changed is set back to false
$scope.$apply();
$scope.changed = false;
}
// in DOM:
<div ng-click="loadData(newData)">
If I run loadData manually using a non-angular event it works fine (of course $applying the scope again afterwards), but if I run it from something like the above ngClick then it errors out with "$apply already in progress".
Using a $timeout works in most circumstances, but there are some places where I really want it to happen synchronously.
function loadData(data) {
$scope.data = data;
// what I want to avoid:
$timeout(function() {
$scope.changed = false;
})
}
Is it possible to apply scope changes synchronously, or am I doing change handling wrong?
Thanks.
If you're not doing something really special you can use angular.equals to check if two objects are equal. Use that in combination with angular.copy so you have a copy of the original data.
$scope.isDirty = function () {
return !angular.equals(initialData, $scope.data);
}
Plunker
Doing it this way you don't need to worry about the order of your $watch functions and the code will be much easier to understand.
Performance wise, this might be heavier though. You could optimize it by changing so that isDirty only is changed when the data is changed.
ngclick will occupy the $apply handler, so you can't run process which need $apply in an event handler function,just $timeout all action in a function like :
function loadData(data){
$timeout(function(){
$scope.data = data;
$scope.$apply();
$scope.changed = false;
});
}
Try this, but not tested...
You can use a second $scope variable to keep track of whether the change came from a particular function and based on that, enable the $scope.changed variable assignment in your $watch.
Add a variable to serve as a marker in your loadData() function. In this case, we'll call it $scope.explicitChange:
function loadData(data) {
$scope.data = data;
// I want to run $scope.$apply() here so the $watch is triggered
// before changed is set back to false
$scope.$apply();
$scope.changed = false;
$scope.explicitChange = true;
}
Check if $scope.explicitChange has been set in your $watch:
$scope.$watch('data', function(){
if(!$scope.explicitChange){
$scope.changed = true;
}
else{
// Reset $scope.explicitChange
$scope.explicitChange = false;
}
}, true);

variable change not notified to `$scope.$watch`

I am handling a JQRangeSlider through an angular directive + controller.
Within my controller, I am assigning new values to scope variables but there change is not notified to the controller scope (I am using isolated scope '=' in my directive) even though it is correctly updated.
// JQRangeSlider event handler
var valuesChanged = function(e, data) {
if (data.values.min && data.values.max) {
console.log('values changed', data.values); // this is always printed
$scope.minselectedtime = data.values.min; // not triggering $scope.$watch
$scope.maxselectedtime = data.values.max;
$scope.$broadcast('minchanged', $scope.minselectedtime); // instantly triggering $scope.$on('minchanged',...)
console.log('scope', $scope);
}
};
$scope.$watch('minselectedtime', function(newValue, oldValue) {
if (newValue === oldValue) {
return;
}
console.log('minselectedtime -->', newValue);
}, true);
$scope.$on('minchanged', function(event, min) {
console.log('minchanged ==>', min);
});
$scope.$watch('minselectedtime', ...) is only triggered on the next $scope modification (where $scope.minselectedtime is not even modified):
$scope.toggleTimeScale = function(refinementString) {
console.log('scale set to', refinementString);
$scope.timescale = refinementString; // year, month, day...
}
Why isn't $scope.$watch immediatly notified of the variable change?
The isolated code has its $scope.minselectedtime value changed but the parent scope doesn't see its value changed until $scope.toggleTimeScale is triggered.
Your method of using $scope.$apply() is correct. The reason being that the update from JQRangeSlider happened 'outside' of angularjs knowledge. If you read the documentation on the $apply method (http://docs.angularjs.org/api/ng.$rootScope.Scope#$apply) you'll see that it's used in the case you need.

Categories

Resources