I am trying to get a scope to update when the return value of a service function changes. From reading, it sounds like I'm supposed to use $watch, but following the angular docs creates an infinite $digest loop in my application, here the controller:
.controller('navCtrl', function(CookieHandler, $scope){
// this works, but doesn't update when CookieHandler.get() changes
// $scope.user = CookieHandler.get();
$scope.$watch(
function() {return CookieHandler.get()},
function(newValue, oldValue) {
if( newValue !== oldValue) {
$scope.user = newValue
}
}
)
//etc
From the error message there's something about using $watch on functions that return an array, but CookieHandler.get() returns an object?
I looked around some more and read that I should use $apply instead of $watch here because it's easier to test, so I tried this:
.controller('navCtrl', function(CookieHandler, $scope){
// this works, but doesn't update when CookieHandler.get() changes
// $scope.user = CookieHandler.get();
$scope.$apply(function(scope){
scope.user = CookieHandler.get();
})
})
But this throws an error about the digest cycle already running. Is there a way to fix my $watch to stop the infinite loop? CookieHandler.get just checks the value with $cookieStore
$watch performs an equal comparison between the current value and the value from the previous digest cycle. If they are different then the function is executed.
If the object returned by CookieHandler.get() is always the same reference then the watch function will never get called. Even if you update properties of that object.
Arrays are a bad idea because they are often always a new object and JavaScript will see them as never equal.
You could serialize the object/array to JSON and use that value, but this won't work if there are changes in key/value orders.
It looks like you are trying to monitor when the current user is changed. Assuming you have a unique identifier for a user, then you might want to try something like this.
$scope.$watch(
function() {
var user = CookieHandler.get();
return (typeof user === 'undefined') ? 0 : user.id;
},
function(newValue, oldValue) {
if( newValue !== oldValue) {
$scope.user = CookieHandler.get();
}
}
);
Note that I call .get() twice. I don't know how your service works. Maybe that's not possible, but the point is to watch a value that is easily comparable.
Related
I tried to follow along with this example but my code never enters the callback with the newValue when the input changes for some reason. The only difference in my example and the example bin below is that I'm using a service to hold the value instead of a controller variable.
I've been trying to make it work but I can't wrap my head around it. What am I missing?
http://jsbin.com/yinadoce/1/edit?html,js,output
Note that I've excluded the input and the steps prior to the value being updated in the service as this works perfect. The issue is only that my watch doesn't understand when the value has changed in the service.
EDIT: Forgot to inject $scope in the controller when I pasted over the code, now it's complete.
Controller:
coForms.controller('CoFormsCtrl', ['$scope', 'coFormsInfo', function($scope, coFormsInfo) {
$scope.$watch(angular.bind(coFormsInfo.getInfo(), function() {
return coFormsInfo.getInfo();
}), function(newVal) {
console.log(newVal);
});
}]);
Service:
coForms.service('coFormsInfo', [function() {
var info = {
filteredList: []
}
this.setFilteredList = function(list) {
info.filteredList = list;
};
this.getInfo = function() {
return info;
};
}]);
The watcher is there to detect any changes in the variable you're watching. How can he watch something that is not... Strictly present like a return value?
I'm not sure about what I'm saying because I'm new to angular, but the logic seems false there. You need to watch something declared to detect some changes.
You should call your service to get your infos when you need them and watch for an info variable.
EDIT
My bad there is something like that but you should declare it in a function maybe like the example on the documentation
var food;
scope.foodCounter = 0;
expect(scope.foodCounter).toEqual(0);
scope.$watch(
// This function returns the value being watched. It is called for each turn of the $digest loop
function() { return food; },
// This is the change listener, called when the value returned from the above function changes
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
// Only increment the counter if the value changed
scope.foodCounter = scope.foodCounter + 1;
}
}
);
I have a web page that serves as the editor for a single entity, which sits as a deep graph in the $scope.fieldcontainer property. After I get a response from my REST API (via $resource), I add a watch to 'fieldcontainer'. I am using this watch to detect if the page/entity is "dirty". Right now I'm making the save button bounce but really I want to make the save button invisible until the user dirties the model.
What I am getting is a single trigger of the watch, which I think is happening because the .fieldcontainer = ... assignment takes place immediately after I create my watch. I was thinking of just using a "dirtyCount" property to absorb the initial false alarm but that feels very hacky ... and I figured there has to be an "Angular idiomatic" way to deal with this - I'm not the only one using a watch to detect a dirty model.
Here's the code where I set my watch:
$scope.fieldcontainer = Message.get({id: $scope.entityId },
function(message,headers) {
$scope.$watch('fieldcontainer',
function() {
console.log("model is dirty.");
if ($scope.visibility.saveButton) {
$('#saveMessageButtonRow').effect("bounce", { times:5, direction: 'right' }, 300);
}
}, true);
});
I just keep thinking there's got to be a cleaner way to do this than guarding my "UI dirtying" code with an "if (dirtyCount >0)"...
The first time the listener is called, the old value and the new value will be identical. So just do this:
$scope.$watch('fieldcontainer', function(newValue, oldValue) {
if (newValue !== oldValue) {
// do whatever you were going to do
}
});
This is actually the way the Angular docs recommend handling it:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization
set a flag just before the initial load,
var initializing = true
and then when the first $watch fires, do
$scope.$watch('fieldcontainer', function() {
if (initializing) {
$timeout(function() { initializing = false; });
} else {
// do whatever you were going to do
}
});
The flag will be tear down just at the end of the current digest cycle, so next change won't be blocked.
I realize this question has been answered, however I have a suggestion:
$scope.$watch('fieldcontainer', function (new_fieldcontainer, old_fieldcontainer) {
if (typeof old_fieldcontainer === 'undefined') return;
// Other code for handling changed object here.
});
Using flags works but has a bit of a code smell to it don't you think?
During initial loading of current values old value field is undefined. So the example below helps you for excluding initial loadings.
$scope.$watch('fieldcontainer',
function(newValue, oldValue) {
if (newValue && oldValue && newValue != oldValue) {
// here what to do
}
}), true;
Just valid the state of the new val:
$scope.$watch('fieldcontainer',function(newVal) {
if(angular.isDefined(newVal)){
//Do something
}
});
I am trying to do something similar to this plunker. Notice that I use an ng-click event to set the value of the number back to the same number. However, the change does not seem to fire.
$scope.$watch('value', function(newValue, oldValue){
console.log("Value changed from "+oldValue+" to "+newValue);
});
$scope.call = function(){
console.log("Call is called");
$scope.value=1;
}
So I change this to the following...
$scope.call = function(){
console.log("Call is called");
$scope.value=0;
$scope.value=1;
}
Still no watch fire, however, this will seem to solve the issue (albeit very in a messy way)
$scope.name = 'World';
$scope.value = {};
$scope.value.value=1;
$scope.$watch('value', function(newValue, oldValue){
console.log("Value changed from "+oldValue.value+" to "+newValue.value);
});
$scope.call = function(){
console.log("Call is called");
$scope.value = {};
$scope.value.value=1;
}
So the question is, what is going on here and why can't I watch a number that is set to the same value? Is there a cleaner way to handle this?
This behaviour is documented(scroll down to $watch section)
The listener is called only when the value from the current
watchExpression and the previous call to watchExpression are not equal
(with the exception of the initial run).
Inequality is
determined according to reference inequality, strict comparison via
the !== Javascript operator, unless objectEquality == true ...
I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>
I have a web page that serves as the editor for a single entity, which sits as a deep graph in the $scope.fieldcontainer property. After I get a response from my REST API (via $resource), I add a watch to 'fieldcontainer'. I am using this watch to detect if the page/entity is "dirty". Right now I'm making the save button bounce but really I want to make the save button invisible until the user dirties the model.
What I am getting is a single trigger of the watch, which I think is happening because the .fieldcontainer = ... assignment takes place immediately after I create my watch. I was thinking of just using a "dirtyCount" property to absorb the initial false alarm but that feels very hacky ... and I figured there has to be an "Angular idiomatic" way to deal with this - I'm not the only one using a watch to detect a dirty model.
Here's the code where I set my watch:
$scope.fieldcontainer = Message.get({id: $scope.entityId },
function(message,headers) {
$scope.$watch('fieldcontainer',
function() {
console.log("model is dirty.");
if ($scope.visibility.saveButton) {
$('#saveMessageButtonRow').effect("bounce", { times:5, direction: 'right' }, 300);
}
}, true);
});
I just keep thinking there's got to be a cleaner way to do this than guarding my "UI dirtying" code with an "if (dirtyCount >0)"...
The first time the listener is called, the old value and the new value will be identical. So just do this:
$scope.$watch('fieldcontainer', function(newValue, oldValue) {
if (newValue !== oldValue) {
// do whatever you were going to do
}
});
This is actually the way the Angular docs recommend handling it:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization
set a flag just before the initial load,
var initializing = true
and then when the first $watch fires, do
$scope.$watch('fieldcontainer', function() {
if (initializing) {
$timeout(function() { initializing = false; });
} else {
// do whatever you were going to do
}
});
The flag will be tear down just at the end of the current digest cycle, so next change won't be blocked.
I realize this question has been answered, however I have a suggestion:
$scope.$watch('fieldcontainer', function (new_fieldcontainer, old_fieldcontainer) {
if (typeof old_fieldcontainer === 'undefined') return;
// Other code for handling changed object here.
});
Using flags works but has a bit of a code smell to it don't you think?
During initial loading of current values old value field is undefined. So the example below helps you for excluding initial loadings.
$scope.$watch('fieldcontainer',
function(newValue, oldValue) {
if (newValue && oldValue && newValue != oldValue) {
// here what to do
}
}), true;
Just valid the state of the new val:
$scope.$watch('fieldcontainer',function(newVal) {
if(angular.isDefined(newVal)){
//Do something
}
});