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
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
Angular is loosing my my JSON values, and i'm trying to find out why. I think I need to make a promise or a time out, or maybe apply. I'm not quite sure...
angular version: 1.2.1
galleryApp.controller('GalleryCtrl', function ($scope, $http, $routeParams, $filter){
$http.get('mygallery.json').success(function(data) {
$scope.gallery = data;
//DISPLAY: CONTENTS OF JSON IN OBJECT : WORKS
console.log($scope.gallery);
});
//DISPLAY: undefined : DOES NOT WORK AS EXPECTED
console.log($scope.gallery);
//DISPLAY: CONTENTS OF OBJECT: I can see scope.gallery exists!
//I just can't seem to access scope.gallery directly outside of http.get()
console.log($scope);
});
Note: scope.gallery or "gallery" works perfectly fine in my view! {{gallery.name}} etc.
It seems like there is some behind the scenes things angular is doing to the scope, or some concept that i'm not quite grasping.
Well, it's trivial as $http.get is an asynchronous operation. So while it is working the rest of the code will be finished. If you log the $scope.gallery it is undefined yet. If you log the $scope it's still undefined but will be updated when the success callback is invoked. Th reason of this effect for you is just feature of console.log which writes not the current snapshot but the object itself so if changed the output of the console will be updated respectively. But in general none of your code outside of the $http.get will work as you expected here. So you should either use the success callback or use $watch to track the changes.
Also refer to this documentation: $http, $q
The result of http.$get is a promise (specifically an HtmlPromise, see the $http docs). A promise is described as the following in the $q docs:
an interface for interacting with an object that represents the result of an action that is performed asynchronously, and may or may not be finished at any given point in time
The success method of an HtmlPromise takes a callback function to run once the promise is resolved (meaning the action is complete). The anonymous function thus runs asynchronously. When it is logged outside of the callback, the callback has not yet been run and therefore the scope variable has not been set.
I imagine the view is as expected because the http request completes and the view is updated so fast, it is imperceivable.
I have a resource with a custom update method :
angular.module('user.resources', ['ngResource']).
factory('User', function($resource) {
var User = $resource('/user/:id', {}, {
update: {
method: 'PUT'
}
});
User.prototype.update = function(cb) {
console.log('foo');
return User.update({
id: this._id
}, angular.extend({}, this, {
_id: undefined
}), cb);
};
I'm passing this resource to a custom directive via scope:
directive('avatarUpload', function($http) {
return {
restrict: 'E',
scope: {
model: '='
}, ...
and I'm calling the update method in the directive controller on a btn click:
$scope.model.update(function() {
console.log('bar');
});
The behavior which puzzle me atm is that clicking on the button the first time print 'foo' but not 'bar', clicking it a second time print 'bar', then 'foo'. Any more click always print 'bar' then 'foo'.
The PUT request is only fired from the second click and the ones after, never from the first.
Note: I've been using that resource update method fine in controllers, until trying to call it from a directive. I'm using angular 1.1.4
I do this resource passing because I want the directive to work on different type of resource.
It is hard to say for sure without seeing live code example but I presume that you are using AngularJS from the 1.1.x series (so called "unstable branch"). If so, the problem you are facing is linked to the new feature in AngularJS - HTTP request interceptors introduced in version 1.1.4 (this commit).
The newly introduced request interceptors are $q-based (promise-based) and in AngularJS world promises are only resolved as part of the $digest cycle. In other words you need to be in the "AngularJS world" ($digest cycle) for the promises to be resolved.
With the promise-based request interceptors there is a promise to be resolved before a $http call can be made. As as noted before this promise can only be resolved when you enter the $digest cycle. This won't happen if you are initiating the $http from outside of AngularJS (DOM event, setTimeout etc.).
In AngularJS $resource is based on $http so the above discussion apply to the $resource as well.
So, presuming that the above assumptions are correct and you are initiating the $resource call from outside of AngularJS (you are talking about a custom directive so I would bet on a DOM event) you should simply wrap the $resource call into scope.$apply.
Please note that wrapping the $resource call into $timeout (as suggested in another response), while will "fix" your issue (it will force a $digest loop and thus promises will get resolved) it is not the correct approach. The problem is that it will force a browser to leave the current JavaScript context and enter the repaint context for nothing. It will make your application slower and may result in UI flickering.
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
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)