I was watching this AngularJS tutorial describing how to hook into Twitter with an Angular resource. (Video tutorial) Here is the resource that is set up in the example controller:
$scope.twitter = $resource('http://twitter.com/:action',
{action: 'search.json', q: 'angularjs', callback: 'JSON_CALLBACK'},
{get: {method: 'JSONP'}});
The tutorial shows that there are a couple ways to get data back from the resource using the get call. The first method is by passing a callback to the get function. The callback will be called with the result after the ajax request returns:
$scope.twitter.get(function(result) {
console.log('This was the result:', result);
});
I understand this method. It makes perfect sense to me. The resource represents a place on the web where you can get data, and get simply makes an ajax call to a url, gets json back, and calls the callback function with the json. The result param is that json.
It makes sense to me because it seems obvious that this is an asynchronous call. That is, under the hood, the ajax call fires, and the code following the call isn't blocked, it continues to be executed. Then at some indeterminate point later on, when the xhr is successful, the callback function is called.
Then the tutorial shows a different method that looks a lot simpler, but I don't understand how it works:
$scope.twitterResult = $scope.twitter.get();
I assume that the xhr underneath get must be asynchronous, yet in this line we are assigning the return value of the get call to a variable, as if it returned synchronously.
Am I wrong for not understanding this? How is that possible? I think it's really neat that it works, I just don't get it.
I understand that get can return something while the xhr underneath it goes off and processes asynchronously, but if you follow the code example yourself, you will see that $scope.twitterResult gets the actual twitter content before any subsequent lines are executed. For example, if you write console.log($scope.twitterResult) immediately after that line, you will see the results from twitter logged in the console, not a temporary value that is replaced later on.
More importantly, because this is possible, how can I write an Angular service that takes advantage of this same functionality? Besides ajax requests, there are other types of data stores requiring asynchronous calls that can be used in JavaScript which I would love to be able to write code for synchronously in this style. For example, IndexedDB. If I could wrap my head around how Angular's built-in resources are doing it, I would give it a shot.
$resource is not synchronous although this syntax might suggest that it is:
$scope.twitterResult = $scope.twitter.get();
What is going on here is that call to the AngularJS will, after call to twitter.get(), return immediately with the result being an empty array. Then, when the async call is finished and real data arrives from the server, the array will get updated with data. AngularJS will simply keep a reference to an array returned and will fill it in when data are available.
Here is the fragment of $resource implementation where the "magic" happens: https://github.com/angular/angular.js/blob/master/src/ngResource/resource.js#L372
This is described in the $resource documentation as well:
It is important to realize that invoking a $resource object method immediately returns an empty reference (object or array depending on isArray). Once the data is returned from the server the existing reference is populated with the actual data. This is a useful trick since usually the resource is assigned to a model which is then rendered by the view. Having an empty object results in no rendering, once the data arrives from the server then the object is populated with the data and the view automatically re-renders itself showing the new data. This means that in most case one never has to write a callback function for the action methods.
$q can do this trick too. You can convert a normal object to a 'delayed value' using something like this:
var delayedValue = function($scope, deferred, value) {
setTimeout(function() {
$scope.$apply(function () {
deferred.resolve(value);
});
}, 1000);
return deferred.promise;
};
and then use it in a controller, to get a similar effect to what $scope.twitter.get() does in the OP's example
angular.module('someApp', [])
.controller('someController', ['$scope', '$q', function($scope, $q) {
var deferred = $q.defer();
$scope.numbers = delayedValue($scope, deferred, ['some', 'numbers']);
}]);
Related
Thanks in advance for the help...
I have a controller I'm using to call an API to send a password reset link. I have abstracted the actual $http call off into a service to keep the controller thin. Originally I was doing something like this:
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
$http(request).then(function() {
return {}; //return some object using response
}]);
I felt like this would be the best approach as it would keep the controller as thin as possible and kept all service related actions separate. My problem with this was that returning from within the promise never actually returned me anything. I had to actually return the $http service call to get the promise. The only way I was able to make all of this work was if I called .then from within the controller.
//controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.forgotPassword(emailAddress).then(function() {
//do stuff
}
}]);
//service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http){
this.forgotPassword = function(emailAddress) {
return $http(request);
};
}]);
This just feels a little wrong to me because the controller now depends on receiving a promise back from the service. I may just be overthinking this but I would like a second opinion.
Is this considered acceptable/good practice? Is there an alternative to this which would allow me to achieve the encapsulation I'm looking for?
Thanks again.
I've interfaced with the $http from the controller in two slightly different ways.
Like you it felt wrong returning the $http from the service and interfacing with it.
So first I created services and passed in a success method and an error method (callbacks).
// Service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
function sendForgotPasswordEmail(emailAddress, success, error){
$http.post('/api/v1/resetpassword', {emailAddress:emailAddress})
.then(success, error);
}
return {
sendForgotPasswordEmail: sendForgotPasswordEmail
}
}]);
// Controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.sendForgotPasswordEmail(emailAddress,
function(response){ //success
// notify user of success
},
function(response){ // error
// notify user of error
});
}]);
This worked great. I created an large application this way, but as I started on my second large angular project I wondered why I was hiding the $http's promise?
By passing back the promise, I can use other libraries that support promises. With my first approach I can't leverage other libraries promise support.
Passing back the $http promise
// Service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
function sendForgotPasswordEmail(emailAddress){
return $http.post('/api/v1/resetpassword', {emailAddress:emailAddress});
}
return {
sendForgotPasswordEmail: sendForgotPasswordEmail
}
}]);
// Controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.sendForgotPasswordEmail(emailAddress)
.then(
function(response){ //success
// notify user of success
},
function(response){ // error
// notify user of error
});
}]);
I deleted my original answer, and I feel like a dork for stating that you could do it the other way. When I went back and checked my original code back when I first started angular, I found that I was calling then() twice in my application - once in my service where I returned the data, and once in my controller because calling $http(request).then() returns a promise.
The fact is, you're dealing with an asynchronous call. Suppose in your controller, you wanted to do this:
$scope.foo = myService.getFoo(); // No then()
The XmlHttpRequest inside the $http in getFoo() is an asynchronous call, meaning that it calls it and moves on. The synchronous option is deprecated. It's bad practice to make a blocking synchronous HTTP call because it will seize up your UI. This means you should use a callback when the data is ready, and the promise API is made just for that.
If you absolutely do not want to use the then() in your controller, I suppose you could probably pass your scope binding parameters to your service and let your service update them in your then call. I haven't tried this, and because it's asynchronous, I'm not sure if angular will know to call a digest() so you may need to call a $scope.$apply() if the values don't update. I don't like this, because I think the control of the values in the scope should be handled by the controller and not the service, but again - it's your personal preference.
Sorry for leading you astray with my initial answer - I ran into the same question you had, but when I looked back - I saw I used a silly solution.
-- Relevant statements in original answer --
Consider the following:
Where do you want your error handling for the call and who needs to know about it?
Do you need to handle specific failures in a particular controller or can they all be grouped together to one error handler? For some apps, I like to display the errors in a particular place rather than in a general modal dialog, but it's acceptable to create a service to handle all errors and pop them up for the user.
Where do you want to handle your progress/busy indicator?
If you have an interceptor wired up for all http calls and broadcasting an event to show/hide the busy indicator, then you don't need to worry about handling the promise in the controller. However some directives will use the promise to show a busy indicator which requires you to bind it to the scope in the controller.
To me, the decision is determined by the requirements and by personal choice.
Try using a callback like so:
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
var recovery = function(request, callback) {
$http(request).then(function(response) {
callback(response);
})
}
return { recovery: recovery }
}]);
Then you would call it like this:
forgotPasswordService.recovery('http://...', function(response){
console.log(response);
})
I'm trying to chain several $http calls, and then display the response of the last $http call. The calls all work correctly and the proper response is displayed, but only for the very first time I run the chain of calls. On subsequent runs, it makes it through the $http calls, but does not update the view with the latest response.
I can see in both Firefox and Chrome console network logs, that all the calls within the chain are getting executed (even on subsequent runs of the chain). In debugging the issue, I've replaced the $http calls with simple data assignment and everything works - including updates to the view - so at this point I'm mostly confident it's related to the $http/promise. Can anyone shed some light on what I'm doing wrong, or pointers on where to look as I start to read up on Angular.
Thanks.
I'm on Angular version 1.2.16. and here's my code:
//controller
function($scope, $http) {
$http.get('http://localhost:8080/call/one')
.then(function(responseOne) { return responseOne.data })
.then(function(dataFromCallOne) {
return $http.jsonp('http://localhost:8080/call/two')
.then(function(responseTwo) {
$scope.results = responseTwo.data
});
});
}
In my view, I have
<div ng-controller="MyCtrl">
{{results}}
<button ng-click="makeCall()">Call</button>
</div>
I've tried, the following but still no updates to my view, both methods work for initial display of results, but not on subsequent updates:
if(!$scope.$$phase) {
$scope.$apply()
}
and assigning chain results to $scope.result
$scope.results = $http.get('http://localhost:8080/call/one')
.then(function(responseOne) { return responseOne.data })
.then(function(dataFromCallOne) {
return $http.jsonp('http://localhost:8080/call/two')
.then(function(responseTwo) { return responseTwo.data });
});
$scope.results.then(function(data) {
$scope.results = data
});
I found my error. In my chain of $http calls, one of them is an $http.jsonp() call. When using JSONP, Angular requires a callback param - specifically called JSON_CALLBACK. This JSON_CALLBACK, then gets replace by an internally generated, unique callback name, right before the request is made.
?callback=JSON_CALLBACK --> ?callback=angular.callbacks._0
?callback=JSON_CALLBACK --> ?callback=angular.callbacks._1
?callback=JSON_CALLBACK --> ?callback=angular.callbacks._2
This was problematic for me since my jsonp request is to an OAuth provider that required signing the complete url (including the callback param). So initially I would sign the url with callback=JSON_CALLBACK, but Angular would turn it to something like callback=angular.callbacks._#. I would get an OAuth error since the signature and actual url didn't jive. Anyways, at some point I hard coded callback=angular.callbacks._0, just so I could get my OAuth client code working, but I forgot all about it.
Now subsequent calls to $http.jsonp() with non-unique callback params were not being processed after the response was returned. That's why my $scope is not updated after $http promise.
My hack at the moment is to continue using the hard coded callback=angular.callbacks._#, but with incrementing numbers.
Are $resource promises supposed to be compatible with $q? Perhaps I am using it wrong. I have two $resource objects that I might load like:
$rootScope.foos = Res1.query(); // [ foo1, foo2...]
$rootScope.bars = Res2.query(); // [ bar1, bar2...]
I need to broadcast an event when both (and only both) queries have arrived. So I'm using $q.all like this:
$q.all([$rootScope.foos.$promise, $rootScope.bars.$promise])
.then(function(){
// sometimes $rootScope.bars == []
$rootScope.$broadcast('foos_and_bars!');
});
Event listeners find that $rootScope.bars is empty / [] (when a second later it has data)
Update in response to #Daiwei and #ExpertSystem
Ok here's the JSFiddle for this: Reproducing JSFiddle
The console shows $q.all() being returned before the promises are resolved. I suppose either it's being used wrong or it is an angular bug having to do with any of [$resource, $scope, $http, etc.]...
UPDATE:
It turned out to be a version-conflict issue.
The solution provided below is working with later versions (tested with 1.2.8), but is totally redundant.
See Purrell's answer for more info.
Quoting the docs on $resource:
It is important to realize that invoking a $resource object method immediately returns an empty reference (object or array depending on isArray). Once the data is returned from the server the existing reference is populated with the actual data. This is a useful trick since usually the resource is assigned to a model which is then rendered by the view. Having an empty object results in no rendering, once the data arrives from the server then the object is populated with the data and the view automatically re-renders itself showing the new data. This means that in most cases one never has to write a callback function for the action methods.
[...]
The Resource instances and collection have these additional properties:
$promise: ...
$resolved: ...
Your Res1 and Res2 are $resource objects (not instances) and (as per the docs) invoking the query() method "immediately returns an empty reference" (in your case an array). (UPD: The returned arrays still have the additional properties though.)
As the docs suggest, most of the time you just assign those empty references to the Scope and forget about them (once the data is fetched the model (and subsequently the view) will get automagically updated).
Yet, if you do have reasons for wanting to get explicitly informed as soon as the data is there, you could use two "real" promises:
// Create two deferred objects
var deferred1 = $q.defer();
var deferred2 = $q.defer();
// As soon as the data arrives, resolve the deferred objects
$rootScope.foos = Res1.query(function () { deferred1.resolve(); });
$rootScope.bars = Res2.query(function () { deferred2.resolve(); });
// Combine their promises using `$q.all()`
$q.all([deferred1.promise, deferred2.promise]).then(...);
See, also, this working example.
UPDATE
Since the returned arrays also have the $promise property, the more concise version is:
$rootScope.foos = Res1.query();
$rootScope.bars = Res2.query();
$q.all([$rootScope.foos.$promise, $rootScope.bars.$promise]).then(...);
Working demo
This turned out to be an accidental version conflict between ngResource and angular. Angular was on 1.2.6 but ngResource was on 1.2.3.
The originally posted fiddle was not working for a similar but different ngResource version problem, namely that it was using an old ngResource version which didn't use to expose the $promise objects (although they were still used under the hood). In later versions of ngResource the promises are exposed so they can be used for things like this.
#ExpertSystem's suggestion is good but would not have fixed the issue. I did try with $q.defer() promises prior and it has the same issue when the version conflict is present. It does work in the absence of the version conflict, but then it is unnecessary, no need to create your own promises when they are already given to you for convenience.
Here's the working JSFiddle (at angular 1.2.1 but that is good enough for ngResource)
After your $q.all()...you can use .spread(function(prom1, prom2){} instead to run once all promises are returned. With the way you are currently doing it the .then will fire every time one item in the array returns, hence why sometimes one of the items from your array are empty.
I want to execute some code once I load resources from my back-end.
I was able to do this by using a callback on ONE resource request like this:
$scope.resrc = Resrc.query({idResource: 1}, function(){
//CODE AFTER RESOURCE IS LOADED
});
But trying to use $q to wait for MULTIPLE resources to load an then execute code is NOT working for me! (As they suggest here https://stackoverflow.com/a/14545803/215945)
$q.all([
$scope.services = Services.query({idResource: 1}),
$scope.brands = Brands.query({idResource: 1})
]).then(function() {
//CODE AFTER RESOURCES ARE LOADED
});
What am I doing wrong?
From Angular documentation
It is important to realize that invoking a $resource object method immediately returns an empty reference (object or array depending on isArray). Once the data is returned from the server the existing reference is populated with the actual data. This is a useful trick since usually the resource is assigned to a model which is then rendered by the view. Having an empty object results in no rendering, once the data arrives from the server then the object is populated with the data and the view automatically re-renders itself showing the new data. This means that in most case one never has to write a callback function for the action methods.
The key point here is that, the object you are passing is NOT a promise.
A promise is a way to say you are waiting for something to finish first. It is the backbone of ajax, and I would recommend reading up on it if you are unfamiliar.
To make $q work you will need to give the promise instead object or reference instead
From the same documentation
The Resource instances and collection have these additional
properties:
$promise: the promise of the original server interaction that created
this instance or collection.
On success, the promise is resolved with the same resource instance or
collection object, updated with data from server. This makes it easy
to use in resolve section of $routeProvider.when() to defer view
rendering until the resource(s) are loaded.
On failure, the promise is resolved with the http response object,
without the resource property.
$resolved: true after first server interaction is completed (either
with success or rejection), false before that. Knowing if the Resource
has been resolved is useful in data-binding.
So you will need to do something like
$scope.services = Services.query({idResource: 1});
$scope.brands = Brands.query({idResource: 1});
$q.all([
$scope.services.$promise,
$scope.brands.$promise
]).then(function() {
//CODE AFTER RESOURCES ARE LOADED
});
I think this is because the objects that are returned from the calls to $resource are not promises. I have never used this feature but the documentation suggests
$scope.services = Services.query({idResource: 1});
$scope.brands = Brands.query({idResource: 1});
$q.all([
$scope.services.$promise,
$scope.brands.$promise
]).then(function() {
//CODE AFTER RESOURCES ARE LOADED
});
I'm trying to figure out an elegant way to write AngularJS services without being so repetitive with the $q syntax.
Currently, I'm writing services like follows:
(function() {
function ServiceFactory($q, $timeout, $http) {
return {
getFoo: function() {
var deferred = $q.defer();
$timeout(function() {
$http.get('/the/foo/thing').then(function(response) {
if (response.isError) {
deferred.reject();
} else {
deferred.resolve(response.data.foo);
}
}, function() {
deferred.reject();
});
});
return deferred.promise;
}
};
}
angular.module('myapp').service('MyService', ['$q', '$timeout', '$http', ServiceFactory]);
}.call(this));
It works very well, buy I'm always writing down a bunch of code just to delay $http.get and expose a Promise. Sometimes I will have some extra stuff into success callback, like handling data, creating a different response object... But most of the time, is just like the code above: call $q.defer + $http.get().then... + return promise
So, I'm thinking about a way to clean up/reduce the code without affecting the clarity of what I'm doing, e.g if another developer open the file, it should not be a mystery.
As a side note, the $http service here is actually a decorator, handling server responses to give me a more structured object with things like response.isError and response.data.
Also, I have seen a similar solution in another question {1}, but this is not as same. Just returning the response of $http.get().then() will expose the entire response to controllers on response, and it is not the desired effect. Instead, when I call MyService.getFoo().then(...), I'm expecting a foo object as a response from service, fed by server via response.data.foo, or a call to errorCallback.
I've forgot to mention: my server is not RESTful, so $resource is not an option right now. My URLs are more like /thing/get/:id, /thing/do-stuff/:id.
{1} Similar question
After thinking for a while, I figured out a better way to write the services. I've done some changes to $http response, and now my implementation is returning a Promise whenever a Controller calls http.get() or whatever http methods. With this implementation, I've reduced the methods' code to two or three lines per request in most of the cases. Now I should try to use the AngularJS decorator setup.
A working example is here:
http://embed.plnkr.co/p4EHQAbE40XWXjBWqjIM/preview