I get the basic idea of the infinite digest loop and how it happens, but I'm running into the problem. Here is a fiddle demonstrating my code and problem:
http://jsfiddle.net/eS5e5/1/
In the jsfiddle console you'll see the infinite digest loop.
Basically I have to make decisions on data that may not have loaded yet so I need to wait for the promise to resolve using then(). I have a promise called user. There are two different places in the code where I call then() on user.
Just after I define it. I need to set a scope variable based on it.
In another scope method, $scope.isAdmin()
For number 2, it might be asked why I just don't use $scope.user directly in the $scope.isAdmin() method. The problem is, it's possible for $scope.isAdmin() to be called before the async request for the user returns, in which case I need to 'block' before returning from $scope.isAdmin().
My question is, what about $scope.isAdmin() is making angular think that a 'watched' variable has changed and that the digest cycle needs to run again?
$scope.isAdmin() isn't actually changing anything.
Here is the stripped down code:
HTML:
<body ng-controller='myController'>
<div ng-if='isAdmin()'>Hi! <strong>{{ user.username }}</strong> is an Admin!!!</div>
<div ng-if='!isAdmin()'>Hi! <strong>{{ user.username }}</strong> is NOT an Admin!!!</div>
</body>
And the JS:
angular.module('myApp', [])
.factory('myService', function($q, $timeout) {
return {
getUser: function() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve({ username: 'me', isAdmin: true });
}, 2000);
return deferred.promise;
}
};
})
.controller('myController', function($scope, $q, myService) {
var getUserDeferred = $q.defer();
var user = getUserDeferred.promise;
user.then(function(user) {
$scope.user = user;
return user;
});
$scope.getUser = function() {
return myService.getUser().then(function(user) {
getUserDeferred.resolve(user);
});
};
$scope.isAdmin = function() {
return user.then(function(user) {
return user.isAdmin;
});
};
$scope.getUser();
});
So I finally figured out my own problem and thought I would answer it for others in case someone else may find this info useful.
The crux of the fix had to do with 2 concepts: angular promises and angular watches. By being aware of and applying the 2 concepts together the fix was actually pretty simple.
Everything you put on $scope is 'watched' including functions. Every time something watched changes $scope.$apply() runs again to apply the changes. If a scope function (eg: $scope.isAdmin()) changes its return value from one 'apply' to the next it will trigger another 'apply', until things stabilize and the return value isn't changing.
But in my code I was returning user.then(...) which just returns a new promise (which kept the apply cycle going on forever since the return value kept changing).
In my isAdmin() function I needed to defer its return value until the user actually loaded (any other return value would be meaningless). So I changed the code to check if the user async call had resolved by checking $scope.user and if so returning a valid isAdmin value. If $scope.user was still not defined I would just return the promise I already created.
I changed the $scope.isAdmin() to be:
$scope.isAdmin = function() {
if ($scope.user) {
return $scope.user.isAdmin;
}
return user;
};
This has the same effect as the original code without triggering an infinite apply cycle. Specifically, if the $scope.user has not resolved we still return a promise as before, by returning the user var. Note however that the user var is the same promise not a new one created by then() so the apply cycle stabilizes.
And just for completeness here is the updated jsfiddle:
http://jsfiddle.net/eS5e5/2/
Related
I have seen this issue a lot on SO, but none of the solutions I have tried are working for me. I tried using $apply() which gives me an error stating that the digest cycle is already running. I tried using the Dot "." notation, but nothing changes. I even tried using timeout and promises, but still it does not update in the view.
Here is the HTML:
<th colspan="4" ng-class="{'ssqtrue': deficiencies === false , 'ssqfalse': deficiencies === true}">
<span ng-model="definfo">{{definfo}}</span>
</th>
Here is my Controller code:
$scope.recalculateDashboard = function (goToDashboard) {
contractorService
.calculateScores()
.success(function () {
getscoringDetails();
getDefInfo();
if (goToDashboard) {
$scope.tabs[0].active = true;
}
}).error(function (reason) {
console && console.log(reason.statusText);
genericErrorAlertHandler();
});
};
function getDefInfo() {
contractorService.getDeficiencyInfo()
.success(function (data) {
$scope.$apply(function() {
$scope.definfo = data;
});
if ($scope.definfo == 'No Deficiencies Found') {
$scope.deficiencies = false;
} else {
$scope.deficiencies = true;
}
}).error(function (reason) {
console && console.log(reason.statusText);
genericErrorAlertHandler();
});
}
For the life of me, I can't figure out what is going on here. Any assistance is greatly appreciated!
Regards to <span ng-model="definfo">{{definfo}}</span>
You don't need ng-model directive here. Enough to use {{definfo}} or better way like folks pointed to use ng-bind like:
<span ng-bind="definfo"></span>
I tried using $apply() which gives me an error
Generally, developers use $scope.$apply when get callback from 3d party code like jQuery that stay out of digest cycle. So don't use it. BTW, safety way is to use $timeout for example a.e. wrap as:
$timeout(function () {
$scope.definfo = data;
});
I believe if you don't use Promise chain you can use success and error callbacks (what you actually did) instead to use then()
About $http.get('/SSQV4/SSQV5/Contractor/GetDeficiencyInfo');
$http.get returns original Promise but it also returns status 'Ok', config, headers ... and i'm sure you don't want parse full response in your controller.
So I would create new Promise in your service and fetch results only.
So instead: return $http.get(URL); I would write in Service:
this.getDeficiencyInfo = function () {
var deferred = $q.defer();
$http({method: 'GET', url: URL}).then(function(res){
deferred.resolve(res.data);
}, function (error) {
console.error(error);
deferred.resolve({error:error}); //actually you can reject
});
return deferred.promise;
};
DEMO in fiddle
I figured this out, although I don't know "why" it caused this particular issue. When the page first loads $scope.definfo is set by a variable from another http call. The variable var pagedata = [] holds a lot of information that comes back from the service when the page is first rendered. The scope variable is then set with this code: $scope.definfo = pagedata.DeficiencyInfo. Removing that line and calling the function getDefInfo directly, sets and updates the scope variable as desired. Perhaps one of you gurus could explain why that is to me and others so that it may help others. Thank you to everyone for your help.
I've been going crazy trying to get something to work in Angular 1.3 that may not even be possible... but it seems like it should be. Basically, I'd like to write a service that returns only a calculated value, not a promise.
I have a service that makes an $http.get request and returns a promise. I'd like to call this from another service, get that data, manipulate it and return a value. I don't want to have to deal with .then once I've called my service. My reasoning is that I'd like to have a sharable function that I can call and receive an actual value so that I can use it as a condition for something like ng-show. Like I say, I'd write this function in the controller and use .then to assign a variable with the data from the promise, but I don't want to write that in every single controller I need this in.
Example:
app.factory('searchService', ['$http', function($http) {
return {
performSearch: function(searchString) {
return $http.get('http://asdf.com/search/' + searchString);
},
getItemCount: function(itemName) {
var _this = this;
this.count = 0;
this.performSearch(itemName).then(
function successCallback(resp) {
_this.count = resp.data.items.length;
},
function errorCallback() {
// This section doesn't matter right now
};
return this.count;
}
};
}]);
So I'd like to just be able to call this from the controller and get back a "5" or something. Or even better, calling it from an ng-show like "ng-show="blahCtrl.getItemCount('item_search_string') == 0". Again, I have multiple controllers I may need this in, so I'd rather not duplicate functions all over the place just to extract that number I need.
This just doesn't work for me though... my return value is always whatever I initially set this.count to be. I've inserted a console.log a few places and have seen that (it seems) this.count is returned before the callback function can set its value. No matter what I do, I'm incapable of just returning a single value from this service and it's driving me crazy! What confuses me more is that if I were to do this code from inside the controller:
var _this = this;
searchService.performSearch('asdf').then(
function(data) { _this.searchResults.data; }
);
It works just fine and I get my promise data right inside of the searchResults variable no problem. It just doesn't work that way inside of the service for some reason.
Hopefully this makes sense, I may have rambled a bit or been unclear. Any help would be greatly appreciated... I've spent about as much time researching this and using trial and error on my own as I can.
Thanks!
One solution as i see.
Put this once in controller
app.controller('ctrl', function ($scope, searchService) {
$scope.searchService = searchService;
searchService.getItemCount();
});
and get in all view
{{ searchService.count }}
And i think, it's bad to make a request for each ng-if, ng-show etc.
You are going to need to use $q (https://docs.angularjs.org/api/ng/service/$q). Your function is returning before the promise is fulfilled. Your getItemCount should look like this:
app.factory('searchService', ['$http', '$q', function($http, $q) {
return {
performSearch: function(searchString) {
return $http.get('http://asdf.com/search/' + searchString);
},
getItemCount: function(itemName) {
var deferred = $q.defer();
this.performSearch(itemName).then(
function successCallback(resp) {
deferred.resolve(resp.data.items.length);
},
function errorCallback(err) {
deferred.reject(err);
};
return deferred.promise;
}
};
}]);
I have a scope variable which is set after a few request and after some logic execution like this
$scope.record = {};
$scope.record.field = "custom_value";
Now i have a different function which executes independently of this execution .
function test(){ if($scope.record.field == 'some_value') { // do something }}
The problem is that if condition should be checked only after $scope.record.field has a value .
I tried putting it in $q like this $q.all($scope.record.field).then(func) but it did not work .
I also tried using when $q.when($scope.record.field).then(func) but it still did not work.
Both the times i got undefined value .
I don't want to put any code in part where $scope.record.field is set which signifies the completion of assignment because i want to keep both parts separate and independent . Also both of them have access to the same scope .
How do i make sure some part of my code is executed only after $scope.record.field has a value ?
Snippet of possible implementation
//one of the ways you could do that
'use strict';
(function() {
function MyService($q, Util) {
var trackThis;
var deferred;
var methods = {
setValue(val){
trackThis = val;
if (deferred) deferred.resolve(val);
},
listenForValue(val){
deferred = $q.defer();
return deferred.promise;
}
}
return methods;
}
angular.module('myApp')
.factory('MyService', MyService);
})();
EDIT
Assume u have a controller as follows:
'use strict';
(function() {
class MyController {
constructor(MyService, AnotherService) {
MyService.listenForValue().then(function(value){
//do whatever you want with the value
});
// Now introduce have another guy setting the value
AnotherService.fetchValue(function(value){
MyService.setValue(value); //this guys will resolve the previous promise
});
}
}
angular.module('myApp')
.controller('MyController', MyController);
})();
Note: Add validation to prevent possible issues
Remark: Basically you create a service which allows u to share states among different active controllers, MyService allows u to create a promise, which can be resolved at any time by setting the value. Do note though that resolve can be invoked only once per promise
Disclaimer: I'm a beginner with AngularJS.
I'm trying to update $scope after a promise has returned, but it does not work. Here's the simplified code:
.controller('ChooseRewardCtrl', ['$scope', ...,
function($scope, ...) {
// scope data
$scope.points = CardModel.points;
// function called to refresh points
$scope.refresh_points = function() {
return CardModel.refresh_points() // This is a promise that changes CardModel.points value
.then(function(data){
$scope.$apply() // Throw error (cf. above)
})
}
}
I get a Error: [$rootScope:inprog] $digest already in progress
I saw lots of post about this error, but nothing that I can directly link to my problem.
Thanks!
Try this:
.then(function(data){
$scope.points = CardModel.points;
})
you need not use $apply().
log your function parameter ('data') and see what does it contain.
It should contain value returned by promise.
assign the appropriate value to your scope variable.
I want to update an Angular scope with data returned by some jQuery ajax call. The reason why I want to call the Ajax from outside Angular is that a) I want the call to return as fast as possible, so it should start even before document.ready b) there are multiple calls that initialize a complex model for a multiple-page web app; the calls have dependencies among themselves, and I don't want to duplicate any logic in multiple Angular controllers.
This is some code from the controller. Note that the code is somewhat simplified to fit here.
$scope.character = {};
$scope.attributeArray = [];
$scope.skillArray = [];
The reasoning for this is that a character's attributes and skills come as objects, but I display them using ng-repeat, so I need them as arrays.
$scope.$watch('character',function(){
$scope.attributeArray = getAttributeArray($scope.character);
$scope.skillArray = getSkillArray($scope.character);
});
In theory, when $scope.character changes, this piece of code updates the two arrays.
Now comes the hard part. I've tried updating $scope.character in two ways:
characterRequestNotifier.done(function() { // this is a jQuery deferred object
$scope.$apply(function(){ // otherwise it's happening outside the Angular world
$scope.character = CharacterRepository[characterId]; // initialized in the jquery ajax call's return function
});
});
This sometimes causes $digest is already in progress error. The second version uses a service I've written:
repository.getCharacterById($routeParams.characterId, function(character){
$scope.character = character;
});
, where
.factory('repository', function(){
return {
getCharacterById : function(characterId, successFunction){
characterRequestNotifier.done(function(){
successFunction( CharacterRepository[characterId] );
});
}
};
});
This doesn't always trigger the $watch.
So finally, the question is: how can I accomplish this task (without random errors that I can't identify the source of)? Is there something fundamentally wrong with my approaches?
Edit:
Try this jsfiddle here:
http://jsfiddle.net/cKPMy/3/
This is a simplified version of my code. Interestingly, it NEVER triggers the $watch when the deferred is resolved.
It is possible to check whether or not it is safe to call $apply by checking $scope.$$phase. If it returns something truthy--e.g. '$apply' or '$digest'--wrapping your code in the $apply call will result in that error message.
Personally I would go with your second approach, but use the $q service--AngularJS's promise implementation.
.factory('repository', function ($q) {
return {
getCharacterById : function (characterId) {
var deferred = $q.defer();
characterRequestNotifier.done(function () {
deferred.resolve(CharacterRepository[characterId]);
});
return deferred.promise;
}
};
});
Since AngularJS has native support for this promise implementation it means you can change your code to:
$scope.character = repository.getCharacterById(characterId);
When the AJAX call is done, the promise is resolved and AngularJS will automatically take care of the bindings, trigger the $watch etc.
Edit after fiddle was added
Since the jQuery promise is used inside the service, Angular has no way of knowing when that promise is resolved. To fix it you need to wrap the resolve in an $apply call. Updated fiddle. This solves the fiddle, I hope it solves your real problem too.