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
});
Related
I need to write some modules that load data one time and then provide an interface to that data. I'd like to load the data asynchronously. My application already uses promises. Is providing a promise as the result of requiring a module a valid pattern/idiom?
Example Module:
var DB = require('promise-based-db-module');
module.exports =
DB.fetch('foo')
.then(function(foo){
return {
getId: function(){return foo.id;},
getName: function(){return foo.name;}
};
});
Example Usage:
require('./myPromiseModule')
.then(function(dataInterface){
// Use the data
});
UPDATE:
I've used this for a while now and it works great. One thing I've learned, and it's hinted at in the accepted answer, is that it is good to cache the promise itself, and whenever you want to access the data use then. The first time the data is accessed, the code will wait until the promise is resolved. Subsequent usage of then will return the data immediately. e.g.
var cachedPromise = require('./myPromiseModule');
cachedPromise.then(function(dataInterface){
// Use the data
});
...
cachedPromise.then(function(dataInterface){
// Use the data again somewhere else.
});
This seems like a perfectly good interface for a module who's job is to do a one-time fetch of some data.
The data is obtained async so a promise makes sense for that. The goal is to fetch the data just once and then let all places this module gets used just have access to that original data. A promise works great for that too because it's a one-shot device that remembers its state.
Personally, I'm not sure why you need the getId() and getName() methods when you could just offer direct access to the properties, but either can work.
A downside to this interface is that there is no means of requesting a fresh copy of the data (newly loaded from the DB).
I have a service with a method that gets me a list of project types using a $resource. It's working well for me, except that if I make multiple nearly simultaneous calls (from say, two directives) each will create another request instead of using the same response/$promise/data.
I found this which led me to this and TL;DR, apparently it's creating a redundant $q.defer() and is actually considered to be a deferred anti-pattern.
The code below works well if the calls to get project types are significantly staggered (like more than milliseconds apart). The consecutive calls are resolved with the shared.projectTypes. It also works in the sense that if the request to get project types fails, the dfr.reject() will be triggered and be caught by .catch in the calling controller.
angular.module('projects')
.factory('projectService', function(notificationService){
// an object to share data gathered by this service
var shared = {};
// $resource for projects API
var projectResource = $resource(baseApiPath + 'projects', {}, {
...,
getProjectTypes: {
method: 'GET',
url: baseApiPath + 'projects/types'
},
...
});
// loads a list of project types
var loadProjectTypes = function(){
var dfr = $q.defer();
// if we've already done this, just return what we have.
if(shared.projectTypes){
dfr.resolve(shared.projectTypes);
}
else {
// begin anti-pattern (?)
projectResource.getProjectTypes(null,
function(response){
shared.projectTypes = response.result.projectTypes;
dfr.resolve(response);
},
function(errResponse){
console.error(errResponse);
notificationService.setNotification('error', errResponse.data.messages[0]);
dfr.reject(errResponse);
});
}
return dfr.promise;
};
return {
shared: shared,
project: projectResource,
loadProjectTypes: loadProjectTypes
};
});
So, I read that having this extra var dfr = $q.defer() is not necessary as the $resource would provide all that for me. With a bit of refactoring, I ended up with this:
...
// $resource for projects API
var projectResource = $resource(baseApiPath + 'projects', {}, {
...,
getProjectTypes: {
method: 'GET',
url: baseApiPath + 'projects/types',
isArray: true,
transformResponse: function(response){
return JSON.parse(response).result.projectTypes;
}
},
...
});
// loads a list of project types
var loadProjectTypes = function(){
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};
...
To clarify, I have added isArray and transformResponse to the resource because my API returns a lot of extra meta information and all I wanted was an array of types. In my loadProjectTypes method, I'm including the same caching we originally had, but I'm caching the result of projectResource.getProjectTypes() instead of the actual response data (even though that might be exactly what I'm caching because of the transformResponse).
This works on the happy path (reduced calls to API, returns the same thing to everyone, etc) but my main problem is with the chaining and catching of errors.
In my original anti-pattern example, if there is an error with GET /project/types, I'm using dfr.reject() which is then passed back to my controller where I have a .catch().
This is code from the controller which actually makes the original request to get project types:
$q.all([
projectService.loadProjects(),
userService.loadUserRole('project_manager'),
userService.loadUserRole('sales_representative'),
projectService.loadProjectTypes(),
clientService.loadClients()
])
.then(function(response){
// doing stuff with response
})
.catch(function(errResponse){
// expecting errors from service to bubble through here
console.error(errResponse);
});
With the anti-pattern example, the dfr.reject is causing the error to show up here in the catch, but in my supposed non-anti-pattern example, it's not happening. I'm not sure how to reject or resolve the $resource results in the same way I was before. If one of the points of promise chaining is to have one spot to handle errors from any chain link, I was doing it right.
I tried to use $q.resolve()/reject(), since I don't have dfr anymore, but this seems dumb and doesn't work anyway.
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes(null,
function(response){
return $q.resolve(response);
},
function(errResponse){
return $q.reject(errResponse);
}));
How do I get the chain to work so that .catch() in the controller is where the errors get handled?
Did I actually implement the anti-pattern in my original code, or was that one of the accepted ways to use $q.defer() and it wasn't an anti-pattern at all?
In the second link I posted, there is an answer that says:
"What's wrong with it? But the pattern works! Lucky you.
Unfortunately, it probably doesn't, as you likely forgot some edge
case. In more than half of the occurrences I've seen, the author has
forgotten to take care of the error handler."
However, my original code was addressing the errors. It was working, except that each caller was getting it's own promise. I feel that's where I missed something.
I might be confused, but I'm thinking that the loadProjectTypes method should return the same promise/data to anyone who calls it, no matter when it's called. It should be the one true source of anything projectTypes and only make the call once, the very first time.
Any time I look for any of this (lots of purple/visited google links on these subjects), everyone is either showing chaining with contrived examples, or only using $http, or something else. I haven't found anyone doing error catching in a promise chain that uses $resource.
UPDATE: Adding my requirements for the solution. I posted them in my answer, but wanted to include them in the original post too.
Requirement 1: Allows multiple calls to the method, but only makes one API request which updates all callers with the same data.
Requirement 2: Must be able to use result of method as actual data, just as the promise spec intends. var myStuff = service.loadStuff() should actually set myStuff to be "stuff".
Requirement 3: Must allow promise chaining so that all errors in any part of the chain can be caught by a single catch at the end of the chain. As I've found in my solution, there can be more than one chain, and more than one catch, but the point is that each chain has a catch, and any "links" in the chain that break should all report their errors to their respective catch.
Isn't that always the way, as soon as you speak your problems, you come across your solution.
Requirement 1: Only make one request per method call. This is solved with the original fix to the anti-pattern. This will either always return the $resource result by either returning the cached $resource or returning and caching at the same time.
var loadProjectTypes = function(){
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};
Requirement 2: Be able to use the service method as a promise where I can set the value of a $scope variable directly to the result of loadProjectTypes(). Using the revised method above, I can simply state $scope.theTypes = projectService.loadProjectTypes() and it'll automatically be filled with the list of types when they come in, just as the promise spec intends.
Requirement 3: Be able to chain together multiple $resource calls and have their errors be caught by a single .catch(). By using the $promise of the result of loadProjectTypes within $q.all(), I can catch any errors in any catch I want.
$q.all([
...,
projectService.loadProjectTypes().$promise,
...
])
.then(function(response){
// my project types comes in as response[n]
})
.catch(function(errResponse){
// but any errors will be caught here
});
Technically, I can put catches in different places and they'll all work the same. Anytime I have loadProjectTypes(), I can use a .catch() and my errors will be handled there. Each loader of types can handle the API being down in it's own way. This could be really good actually. A controller might get the UI to display a message and a small directive might just display something else, or nothing at all. They each can handle the bad in their own way.
My service, directive and controller look like this now:
angular.module('projects')
.factory('projectService', function(notificationService){
// an object to share data gathered by this service
var shared = {};
// $resource for projects API
var projectResource = $resource(baseApiPath + 'projects', {}, {
...,
getProjectTypes: {
method: 'GET',
url: baseApiPath + 'projects/types',
isArray: true,
transformResponse: function(response){
return JSON.parse(response).result.projectTypes;
}
},
...
});
// loads a list of project types
var loadProjectTypes = function(){
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};
return {
shared: shared,
project: projectResource,
loadProjectTypes: loadProjectTypes
};
});
angular.module('projects')
.directive('projectPageHeader', ['projectService', function(projectService){
return {
restrict: 'E',
scope: {
active: '#',
},
templateUrl: 'src/js/apps/projects/partials/dir_projectPageHeader.html',
replace: true,
controller: function($scope){
$scope.projectService = projectService;
// sets the types to the array of types
// as given by the transformResponse
$scope.types = projectService.getProjectTypes();
// could also do a .$promise.catch here if I wanted.
// all catches will fire if get projectTypes fails.
}
};
}]);
angular.module('projects')
.controller('projectListPageController', [
'$scope','projectService',
function($scope, projectService){
// load it all up
$q.all([
projectService.loadProjectDetails($routeParams.projectId).$promise,
userService.loadUserRole('project_manager').$promise,
userService.loadUserRole('sales_representative').$promise,
projectService.loadProjectStatuses().$promise,
projectService.loadProjectTypes().$promise,
clientService.loadClients().$promise
])
.then(function(response){
// do work with any/all the responses
})
.catch(function(errResponse){
// catches any errors from any of the $promises above.
})
}]);
Since the loadProjectTypes (or any other load_____ method) saves the types within the service it comes from, I don't really need to do any storing on the controller. projectService.shared.projectTypes is universal across the entire app. The .then() method in my controller could potentially be noop if all the services were storing the results of their loads internally (which is how I like it) unless there was some view specific thing I needed to do with them. I typically only use controllers for entire pages, or $modals. Everything else is broken up into directives and most information and logic is in services.
I'm leaving the question open in case someone has a better solution. I like the one that Jack A. posted, but I feel it makes my load___ methods more verbose than they already are. Since there are a few of them with slight differences, it leads to a lot of redundant code, or complex 'smart' methods in my actual code. It definitely solves Requirement 1 and possibly 2 and 3 though.
UPDATE (GOTCHA):
So, I've been using this pattern for a few days now and it's working really exactly as I intend. It's really streamlined our process; however, I recently came upon a gotcha when using a method like loadProjectTypes in a singular context (i.e.: outside of $q.all()).
If you just use the load method like so:
// This code is just placed in your controllers init section
loadProjectTypes()
.$promise
.then(function(response){
// ... do something with response (or noop)
})
.catch(function(errResponse){
// ... do something with error
});
You will run into a situation when that controller 'refreshes'. For example, you have the code above in controllerA, you change "pages" which uses controllerB, then you go back to the first "page" and controllerA refreshes and tries to run this again. The error you get is that "there is no .then of undefined."
Inspecting this in the console, the first time loadProjectTypes() runs, it returns the response from the $resource (which includes $promise AND all the projectType data). The second time - coming back from controllerB - it will only hold the projectType data. There is no more $promise because you are not returning the result of a $resource, you returned the cached shared.projectTypes that you set after the first time. That's why we did all this, remember? I'm not sure why this goes away since that's what you saved to shared.projectTypes, but it does, and it doesn't actually matter.
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
For me, the easiest fix was to just have loadProjectTypes().$promise as the only member of a $q.all() set:
// again, this code is just placed somewhere near the top of your controller
$q.all([
loadProjectTypes().$promise
])
.then(...)
.catch(...);
In most cases, my controllers will be getting more than one thing so this would've happened eventually, but there will always be a situation where you only need to load one thing. Using a single item set in $q.all() is the only way to have no issues when using this solution. It's not that bad really, could've been worse.
I wrote something very similar to this a while ago, with a couple key differences:
I only create the promise when the data is already in the cache and return the native promise when an actual request is initiated.
I added a third state for when a request for the resource is already pending.
A simplified version of the code looks like this:
module.factory("templateService", function ($templateCache, $q, $http) {
var requests = {};
return {
getTemplate: function getTemplate(key, url) {
var data = $templateCache.get(key);
// if data already in cache, create a promise to deliver the data
if (data) {
var deferred = $q.defer();
var promise = deferred.promise;
deferred.resolve({ data: data });
return promise;
}
// else if there is an open request for the resource, return the existing promise
else if (requests[url]) {
return requests[url];
}
// else initiate a new request
else {
var req = $http.get(url);
requests[url] = req;
req.success(function (data) {
delete requests[url];
$templateCache.put(key, data);
});
return req;
}
},
};
});
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'm writing an AngularJS service for a SignalR hub. Here's my factory for the service:
.factory('gameManager', [function () {
$.connection.hub.start();
var manager = $.connection.gameManager;
return manager;
}])
That code would be perfect, except that that .start() call is asynchronous, and the hub has not completed starting by the time the manager is returned. Basically, I want to block until the start is complete before returning the manager. The .start() method returns a Jquery deferred object, which I'm guessing is part of the answer, but I don't know how to use it without a callback function?
Something like the following should do the trick.
app.factory('gameManager', [function () {
return $.connection.hub.start().then(function() {
return $.connection.gameManager;
});
}])
Now your callback function will return a deferred/promise too, so the service consumer will need to be expecting that. Your consuming code might look something like this:
gameManager.then(function(gameManager) {
// do whatever with game manager
gameManager.doSomething();
});
The docs for jquery Deferred are here. In particular, check out Deferred.then().
Note that:
the deferred.then() method returns a new promise that can filter the status and values of a deferred through a function ... These filter functions can return a new value to be passed along to the promise's .done() or .fail() callbacks, or they can return another observable object (Deferred, Promise, etc) which will pass its resolved / rejected status and values to the promise's callbacks...
update:
An alternate approach (and probably the better approach - since it won't require that your consumer handle the promise) is to let the hub initialize completely, before setting up your factory, and kicking off your app controller. Something like this...
$.connection.hub.start().then(function() {
app.factory('gameManager', function() {
return $.connection.gameManager;
});
// ...
// kick off the rest of the app..
});
You will not find what you are looking for, you will have to go with Lee's answer. Javascript is mostly single-threaded and does not allow blocking (with specific exceptions, such as alert window or synchronous ajax call).
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']);
}]);