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

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.

Related

angular $watch old value and new value are the same

My intention is to watch a value within $rootScope, and call the listener method when value changed. However, I found that if value doesn't changed, the listener will also be called. Is there a way to watch if a value is changed in angular?
app = angular.module('myApp',[]);
app.controller('MyCtrl', function($rootScope, $timeout){
$rootScope.markers = 1;
$rootScope.$watch('markers', function(newValue, oldValue){
console.log('being watched oldValue:', oldValue, 'newValue:', newValue);
});
$timeout( function() {
$rootScope.markers = 1;
}, 500);
});
here is the code, http://plnkr.co/edit/MXIaJKE9UGTMjrreFjb0?p=preview
$watch runs when controller created, and then when value assigned, so you need to check equality inside $watch function:
if(oldValue != newValue) {...}

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

Watch $localStorage for changes

I am using the ngStorage modules which can be found here: https://github.com/gsklee/ngStorage
I am setting a lastSeenId in localStorage for a live updating social feed, as this is a hybrid app (Laravel/AngularJS) I can't store it in $scope.
My StatusesController gets a array or data (or nothing if none to view) ever X seconds and I get the last item of the array and set my $localStorage var like this:
$localStorage.lastStatusSeen = lastId;
My navigation controller is where I set some new counters for the user to see if they have missed anything, I am getting lastSeenId like this:
$scope.$storage.lastStatusSeen which gets sent over to my API to return a number of unseen status posts.
This all works, user is on status page, if new data, then set the local storage var to the last ID in the array of data.
The issue is that my NavigationController is not finding this change and always passes the lastSeenId that was passed when the user first visited the page.
Is there a way to watch that key in local storage for changes?
EDIT: As requested, updated code
app.controllers.navigationController = ['$scope', 'pollingService', '$localStorage', function($scope, pollingService, $localStorage) {
$scope.lastStatusSeen = $localStorage.lastStatusSeen;
$scope.$watch(function () {
return $localStorage.lastStatusSeen;
}, function (newVal, oldVal) {
if (!newVal) {
return;
}
if (newVal !== oldVal) {
// Assign to a local scoped var, or anything, here...
$scope.lastStatusSeen = newVal;
// I suggest call a method to do your logic outside this
//$scope.onIdChange(newVal, oldVal); // If you need oldVal
}
});
pollingService.startPolling('emailCount', 'api/email/new-count', 5000, function(response) {
$scope.emailCount = response.data;
});
pollingService.startPolling('statusCount', 'api/social/new-count?lastid=' + $scope.lastStatusSeen, 5000, function(response) {
$scope.socialCount = response.data;
});
}];
You can watch it using the $watch method of any scope entity of angular ($scope or $rootScope):
$scope.$watch(function () {
return $localStorage.lastStatusSeen;
}, function (newVal, oldVal) {
if (!newVal) {
return;
}
if (newVal !== oldVal) {
// Assign to a local scoped var, or anything, here...
// I suggest call a method to do your logic outside this
$scope.onIdChange(newVal, oldVal); // If you need oldVal
}
});
Be careful with performance because this runs almost every $digest cycle. Another note is that you can associate this watcher to a variable, so you can cancel it anytime you need:
var watchIdStored = $scope.$watch(function () {
/* ... */
When you want to remove it, call the var as a function:
watchIdStored(); // Removes the $watcher
Edit 1:
The problem there is on pollingService.startPolling('statusCount', 'api/social/new-count?lastid=' + $scope.lastStatusSeen. It builds up a string that isn't changed anymore. You have to inform this change as it happens, providing to the poll the update.
If you put a console inside the watcher, where you update the var, you'll see that it's updating.
Edit 2:
As you have a stopPolling() method, you can create a build/destroy function on your controller.
function _setupPolling (name, url, pollingTime, callback) {
pollingService.stopPolling(name);
pollingService.startPolling.apply(pollingService, arguments);
}
Now, you call it on every change that occurs on $localStorage.lastStatusSeen:
$scope.$watch(function () {
return $localStorage.lastStatusSeen;
}, function (newVal, oldVal) {
if (!newVal) {
return;
}
if (newVal !== oldVal) {
console.log(newVal);
// Assign to a local scoped var, or anything, here...
$scope.lastStatusSeen = newVal;
// I suggest call a method to do your logic outside this
_setupPolling('statusCount', 'api/social/new-count?lastid=' + $scope.lastStatusSeen, 5000, function(response) {
$scope.socialCount = response.data;
});
}
});

angularjs $watch cannot detect change of Property in an object

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

Having trouble with $watch in Angular controller

I'm having some trouble getting a $watch to work when testing a controller.
The idea here is that ctrl.value can be displayed in ARI format or AEP format, but the underlying $scope.model is always in ARI format. So whenever ctrl.value is changed, $scope.model is either just set to the same value, or converted to ARI format and set to that value. However, I can't get the $watch to fire when ctrl.value changes.
The relevant bits of the controller looks like this. (I'm using functions in the watch so I can spy on them in the test):
.controller('EventLikelihoodController', function($scope) {
var ctrl = this;
ctrl.value = $scope.model;
ctrl.renderingARI = true;
ctrl.getValue = function() {
return ctrl.value;
};
ctrl.updateModel = function(newValue) {
if (ctrl.renderingARI) {
$scope.model = newValue;
} else {
$scope.model = ctrl.convertAepToAri(newValue);
}
};
$scope.$watch(ctrl.getValue, ctrl.updateModel);
});
And my Jasmine test:
it('sets the model value correctly', function () {
spyOn(controller, 'updateModel').andCallThrough();
spyOn(controller, 'getValue').andCallThrough();
controller.value = 2;
scope.$digest();
expect(controller.getValue).toHaveBeenCalled();
expect(controller.updateModel).toHaveBeenCalledWith(2);
expect(scope.model).toBe(2);
controller.switchRendering();
scope.$digest();
expect(controller.updateModel).toHaveBeenCalledWith(4);
expect(scope.model).toBe(4);
});
The test fails saying it expected all the spy functions to have been called, but they weren't.
The switch rendering function changes the value from the ARI rendering to the AEP rendering. I have other tests that verify that it works correctly (ie ctrl.value is changing).
I'm not sure if the problem is the actual watch statement, or if it's just an issue running it in the test (I haven't written enough code to check if the controller works outside of a test yet...)
I think your watch statement should be as mentioned below as the this inside the function is not referring your controller's this. lets bind it manullay
$scope.$watch(angular.bind(this,ctrl.getValue), ctrl.updateModel);
Plus in the updateModel() we are updating $scope.model and we are listening for the changes in value . this.value has to be updated in the updateModel(). (this is applicable only if this the method which is supposed to change the value of ctrl.value

Categories

Resources