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.
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've created a service that fetches through $http.get().
My service is used in a directive that renders to several ui elements. The service is async. It's used in the directive code like: myService.getStuff(), and it returns a customized promise (from within the callbacks of the $http.get promise).
But I want my service to only perform one call to $http.get during the lifecycle of the app. And cache the http.get response.
So say I use the directive 2 times in my html. Then this is what I want: The first time it's called it (returns immediately and) fetches stuff via $http.get. The second time it's called it detects an outstanding call to $http.get. thus does not do another call to $http.get, but somehow awaits the return of the first call.
You might want to use the internal cache of $http
return $http.get(/*...*/, { cache: true }).then(/*...*/);
or just cache the promise in your service.
function MyService($http) {
var promise;
this.get = function() {
promise = promise || $http.get(/*...*/).then(/*...*/);
return promise;
}
}
If you're using uirouter, another way is to resolve the data in a parent state, and inject the resolved data to all your components.
Set cache to true like this:
$http.get(yourUrl, {cache: true})
.then(...)
Any subsequent call to this will use the cached value.
I am reading
http://angular-tips.com/blog/2013/08/watch-how-the-apply-runs-a-digest/
directive link function:
element.bind('click', function() {
scope.foo++;
scope.bar++;
scope.$apply();
});
a better way for using $apply:
element.bind('click', function() {
scope.$apply(function(){
scope.foo++;
scope.bar++;
});
});
What’s the difference? The difference is that in the first version, we are updating the values outside the angular context so if that throws an error, Angular will never know. Obviously in this tiny toy example it won’t make much difference, but imagine that we have an alert box to show errors to our users and we have a 3rd party library that does a network call and it fails. If we don’t wrap it inside an $apply, Angular will never know about the failure and the alert box won’t be there.
Confusion:
Why angular need to know error, i just need to show it for users. for example, there is an ajax request in link fn of directive, I just need to tell what happened if fails.
TAngular $scope has a function called $apply() which takes a function as an argument. AngularJS says that it will know about model mutation only if that mutation is done inside $apply(). So you simply need to put the code that changes models inside a function and call $scope.apply(), passing that function as an argument. After the $apply() function call ends, AngularJS knows that some model changes might have occurred. It then starts a digest cycle by calling another function —- $rootScope.$digest() — which propagates to all child scopes. In the digest cycle watchers are called to check if the model value has changed. if a value has changed, the corresponding listener function then gets called. Now it’s upto the listener how it handles the model changes.
The Ajax call through Angular buildin $http the model mutation code is implicitly wrapped withing $apply() call, so you don’t need any additional steps.
I'm trying to come up with some code which allows me to run a function in the controller but only once the whole dom is setup and ready (including the directives link function run etc.).
I'm currently communicating between ctrl/service and the directive via $rootScope broadcasts. The first broadcast at the time of the controller loading is not being picked up by the directive. The reason is of course that the controller loads before the directive link function runs. I've read a few similar questions on SO where people recommended on using $timeout for these calls. This unfortunately doesn't always work and I don't want to clutter my ctrl/services with lots of $timeout calls. Therefore I'm looking for another solution to my problem.
Communication pattern is as follows:
1.) Controller tells Service to prepare some data (via function call in service)
2.) Service tells directive to display data (via broadcast)
3.) Directive displays data ...or not in my case :(
EDIT:
As timing is essential in my app, I'm basically looking for a way to initiate a function in the controller as soon as all angular components have finished loading. That function in the controller will display content by assigning a value to a scope variable. At the same time it will start taking the time. I can of course only start doing that once the directives are loaded, otherwise the tmining is wrong or the directive is not yet ready to display content etc.
I've read through a blog post by Ben Nadel, which basically shows how directives are loaded. I was hoping to setup an outer directive which loads last so I can trigger the finished loading from there. Unfortunately that doesn't work as soon as any of the inner directives use a templateUrl.
http://www.bennadel.com/blog/2603-directive-controller-and-link-timing-in-angularjs.htm
Using $timeout would be terrible. Don't do that. You can't define how long a server call is going to take.
I would recommend using this pattern:
Have the controller use a service to load some data, and have the
promise in the controller assign the return data to a scope variable.
Pass that scope variable into your directive.
Setup a watch in the directive link function, when it loads it will go from undefined to desired value. Done!
// in your controller
YourService.all().then(function(data) {
$scope.data = data;
});
// in your view
<some-directive your-data="data"></some-directive>
// in your directive
angular.module('blah.directives').directive('someDirective', function() {
return {
scope: {
yourData: '='
},
link: function(scope, element, attrs) {
var watcher = scope.$watch('yourData', function() {
if(scope.yourData === undefined) return;
// at this point your data will be loaded, do work
// optionally kill watcher at this point if it's not going to update again
watcher();
});
}
}
});
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