In my controller I'm getting a promise from another service. I add a 'then' clause to it, but the 'then' is never called.
See this plunker: http://plnkr.co/edit/dX0Oz1?p=preview (javascript version)
'fakeLongRunningPromise' creates a promise that resolves itself after 2 seconds.
In the controller itself I send a note to the console once the promise is resolved.
I can tell that the promise is being resolved because "Resolving promise" it outputted to the console. Why doesn't it output "promise resolved"?
Thinking maybe the promise is going 'out of scope' because the controller returns?
The AngularJS the result of promises resolution is propagated asynchronously, inside a $digest cycle. So, the callbacks registered with then will be only called upon entering the $digest cycle. The setTimeout executes "outside of the AngularJS world", and as such will not trigger callbacks.
The solution is to use Scope.$apply or the $timeout service. Here is the version with $apply:
window.setTimeout(function() {
console.log("Resolving promise");
$scope.$apply(function(){
deffered.resolve("worked");
});
}, 2000);
Here is a fixed plunk (JavaScript): http://plnkr.co/edit/g5AnUK6oq2OBz7q2MEh7?p=preview
I've used $timeout instead of setTimeout and it works:
# Resolve the promise after 2 seconds
$timeout( ()->
console.log "Resolving promise"
deffered.resolve ("worked")
, 2000)
Related
This is a very common question, but I have never found the answer that works properly. I have come across three answers, but none always works.
$apply: This will force an update, but will randomly through an error if it gets called while a digest is in progress.
"safe apply" where there is a check for a digest in progress before calling $apply. This doesn't always update the view for some reason I haven't been able to determine. In addition, there is a small chance that a digest will start between the check and $apply.
$timeout: according to the documentation, this should work reliably but doesn't seem to always update the view.
from $timeout documentation, the parameter invokeApply:
If set to false skips model dirty checking, otherwise will invoke fn within the $apply block. (default: true)
It never throughs an error, but sometimes doesn't update the view during a page load.
Here is a code sample where the problem occurs during page initialization:
EditService.getEvents(Gparams.curPersonID)
.then(function successCallback(response) {
if (response.status=='200') {
do some stuff
} else {
handle an error
}
var timer = $timeout(function() { })
.then(function successCallback(response) {
do something
});
$scope.$on("$destroy", function(event {
$timeout.cancel(timer)});
}); });
What is the correct answer? Please don't just say what but also discuss why.
Here is a code sample where the problem occurs during page initialization
A common cause of .then methods not updating the DOM is that the promise is not an AngularJS $q service promise. The solution is to convert the suspect promise to a $q service promise with the $q.when method.
//EditService.getEvents(Gparams.curPersonID)
//CONVERT to $q service promise
$q.when(EditService.getEvents(Gparams.curPersonID))
.then(function successCallback(response) {
if (response.status=='200') {
do some stuff
} else {
handle an error
}
The .then method of a $q service promise is integrated with the AngularJS framework and its digest cycle. Changes to the scope model will automatically update the DOM.
when
Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. This is useful when you are dealing with an object that might or might not be a promise, or if the promise comes from a source that can't be trusted.
--AngularJS $q Service API Reference - $q.when
I'm testing a $resource. If I have code like this:
$resource.do(stuff).$promise.then(function(data){
console.log(1);
});
console.log(2);
I get the print out:
2
1
But if I have code like this:
var callback = function(data){
console.log(1);
}
$resource.do(stuff,callback);
console.log(2);
I get the print out:
1
2
Why? I have found that with the second method, I do not need to call $rootScope.$apply() to get the correct output from my unit test (instead of console.log(2), I return a variable). Using the first method, I have to call $rootScope.$apply() in my unit test in order to get output. I thought that passing callbacks is the same as calling .then on a promise?
The callback is called as soon as the $resource finishes what it is doing. This doesn't have to be in the Angular digest cycle. The promise is resolved in the angular digest cycle. When you call $rootScope.$apply()
For more see: Resolving promises without a digest cycle
I have the following controller:
app.controller('SearchVideosController',
function SearchVideosController($scope, videoRepository) {
$scope.DoSearch(id, text) {
// Do some work...
videoRepository.getVideosForUserBasedOnSearchText(id,text)
.then(function(data){
// Do something with the data.
});
};
};
My videoRepository.getVideosForUserBasedOnSearchText() method uses $q and I want to create stub to ensure that the call is made.
I tried :
it("should have 3 searched videos", function(){
...
mockVideoRepository.getVideosForUserBasedOnSearchText.returns([]);
but get .then() is undefined.
Not sure how to handle the then() call.
You would need to get hold of $q service instance and use $q.when to create a promise wrapped value:-
mockVideoRepository.getVideosForUserBasedOnSearchText.returns($q.when([]));
Also remember you would need to manually perform a digest cycle in your test before the expecation to evaluate the result of the getVideosForUserBasedOnSearchText call. Only when a digest cycle is invoked promise will be resolved in your test. You can do it by getting hold of scope, and perform $digest or $apply. Example:- rootScope.$apply()
I created a directive that watches for a day attribute and retrieves a remote resource whenever the attribute value changes. CoffeeScript code :
angular.module('app.directives').directive 'myDirective', ['$timeout', ($timeout)->
DirectiveController=($scope, activityResource)->
# Load activities
load= ()->
activityResource.get {
day: $scope.day
environment_id: $scope.environment.id
},
(data)->
$scope.activities = data.activities
# Watch environment and reload activities
$scope.$watch 'environment', (value) ->
load()
# Watch selected day to reload activities
$scope.$watch 'day', (value) ->
load()
...
]
The load method is called each time the day changes but the result callback never triggers. Network inspection shows that the request is not sent to the remote end. The only workaround I found is to defer the execution of the load method using the $timeout service :
$scope.$watch 'day', (value) ->
$timeout (()-> load()), 10
I suspect an issue related to the scope lifecycle but I could not figure it out why I had to defer the call to the resource.
Update :
The resource source code :
resources = angular.module('app.resources', ['ngResource'])
...
resources.factory 'app.activityResource', ['$resource', 'app.endpoint', ($resource, endpoint)->
$resource "#{endpoint()}/user/environments/:environment_id/activities/:id/:verb",
{environment_id: "#environment_id", id: "#id"},
latest:
method : 'GET'
params:
verb: 'latest'
annual:
method : 'GET'
params:
verb: 'annual'
]
I also added to the code excerpt the second watched attribute (environment) used as a parameter to query the resource.
Update 2
I don't know if it is related but we're using CORS to access the remote end (which seems to work well).
The reason that you aren't seeing a request fired off is that as part of the $http call, the config is passed around to each interceptor as a promise.
https://github.com/angular/angular.js/blob/b99d064b6ddbcc9f59ea45004279833e9ea82928/src/ng/http.js#L665-L670
In angular, $q promises are only resolved at the beginning of a $digest cycle. As a result, your HTTP request won't even be fired off until a $digest cycle happens, which is why you're seeing that wrapping a $timeout around it helps ( a timeout causes a $digest ).
https://github.com/angular/angular.js/blob/2a5c3555829da51f55abd810a828c73b420316d3/src/ng/q.js#L160
https://github.com/angular/angular.js/blob/3967f5f7d6c8aa7b41a5352b12f457e2fbaa251a/src/ng/rootScope.js#L503-L516
You can also verify this by re-creating the error condition you're seeing and watching network requests. After you run this, you should see your request fire off:
angular.element( "[ng-app]" ).scope().$digest()
That code fires off a $digest cycle on the $rootScope, causing your unresolved promises to become resolved.
See this issue for more discussion, because I agree that this is surprising behavior.
https://github.com/angular/angular.js/issues/2881
I'm trying to write a small service in my angular app which will enable me to pick config parameters specified in global Javascript objects. I don't want to attempt accessing the global config object unless the document is ready (because I cannot guarantee the order in which script elements will be inserted in the HTML).
However, I can't understand why I need the call to $apply for the resolution to actually propagate to the then callback.
myModule.service('GlobalConfigService', ['$q', '$rootScope', function($q, $rootScope) {
var def = $q.defer();
$(document).ready(function() {
def.resolve(MyConfig.val);
$rootScope.$apply();
});
def.promise.then(function () {
console.log('I am called only when $apply() is called. Why?');
});
return def.promise;
}]);
In AngularJS the results of resolve() are propagated asynchronously, inside a $digest cycle, not immediately. This means that callbacks registered with then() will only be called (later) when a digest cycle occurs.
In your code, nothing is causing Angular to enter a digest cycle, so the then() callback is never called. Calling $apply() is one way to cause a digest cycle to run. Another way: add a button with ng-click that does nothing, then click that, it will cause a digest cycle and you should see the results:
<button ng-click="">Force digest by clicking me</button>
See also https://stackoverflow.com/a/14657974/215945