Disclaimer: there actually two questions being asked here but I feel like they are closely related.
I'm trying to pass a promise object to a directive and I want to run some initialization code in the directive as soon as the promise resolves.
In a controller I have:
$scope.item = $http.get(...)
.success(function (result) {
$scope.item = result.item;
});
$scope.item is passed to a directive (via an attribute in an isolated scope called item).
The directive link function finally do something like:
Promise.resolve(scope.item)
.then(function () {
initialize();
});
This works fine in Firefox but when I run it on IE I get an error because Promise is not defined. This problem made me realize I need to probably use the AngularJS $q service in order to provide consistency among browsers and while I was looking at the documentation I discovered another problem, which seemed small at first but it is actually giving me headaches: the success() function is deprecated and I should use then(successCallback) instead. Easy peasy, I thought, BUT as soon as I change the success call in the controller the code stop working in Firefox too! I cannot figure out why. So this is the first question.
The second question is that (even if I leave the success call in the controller) if I modify the code in the directive link function to use $q with what I thought was the equivalent:
$q.resolve(scope.item, function() { initialize(); });
this still doesn't work at all. Any suggestion?
You need to use Angular's $q not only because it works across browsers - but also because it is deeply linked to Angular's digest cycles. Other promise libraries can accomplish this feat but native promises cannot easily do so.
What $q.when does (the $q version of Promise.resolve) is convert a value or a promise to a $q promise. You don't need to do it since you're already using Angular's own $http API which returns promises already.
I warmly recommend that you put your web calls in services and don't affect the scope directly - and then call those services to update the scope.
The pattern is basically:
$http.get(...) // no assign
.success(function (result) { // old API, deprecated
$scope.item = result.item; // this is fine
});
Or with the better then promise API that has the benefits of promises over callbacks like chaining and error handling:
$http.get(...).then(function (result) {
$scope.item = result.data;
});
You are correct about the .success method being deprecated. The .then method returns data differently than the .success method.
$scope.httpPromise = $http.get(...)
.then(function (result) {
$scope.item = result.data.item;
return result.data;
});
You need to return the data to chain from it.
$scope.httpPromise.then ( function (data) {
//Do something with data
initialize();
});
For more information on the deprecation of the .success method see AngularJS $http Service API Reference -- deprecation notice.
Since scope.item is a promise all you have to do is:
scope.item.resolve.then(function() { initialize(); });
Make sure $q is injected in your directive.
improved answer
As #benjamin-gruenbaum mentioned correctly, I used an anti-pattern in my answer. So the solution is basically to pass the promise to your directive and use it there (as already mentioned in Benjamins answer).
Working jsFiddle: https://jsfiddle.net/eovp82qw/1/
old answer, uses anti-pattern
Sorry if I give you a solution that perhaps differs too much from your code. But maybe you can adopt it to your solution.
My approach would be to create a second promise I handover to your directive. This is imho a cleaner way for resolving the waiting state of the directive and don't reuse the same scope variable for two different tasks.
HTML
<body ng-app="myApp">
<div ng-controller="MyCtrl">
<my-directive promise="promiseFromController"></my-directive>
</div>
</body>
JS
function MyCtrl($scope, $q, $http) {
function init() {
var deferredCall = $q.defer();
// simulated ajax call to your server
// the callback will be executed async
$http.get('/echo/json/').then(function(data) {
console.log('received', data);
deferredCall.resolve(data); //<-- this will resolve the promise you'll handover to your directive
});
// we're return our promise immediately
$scope.promiseFromController = deferredCall.promise;
}
init();
}
angular.module('myApp',[])
.controller('MyCtrl', MyCtrl)
.directive('myDirective', function() {
return {
scope: {
promise: '='
},
controller: function($scope) {
console.log($scope);
$scope.promise.then(function(data) {
console.log('received data in directive', data);
});
}
}
})
Working jsFiddle: https://jsfiddle.net/1ua4r6m0/
(There is no output, check your browser console ;) )
Related
I wrote a simple graphql query that fetches an array of objects. The array is showing up when I do console.log(). However, the array does NOT update the html when the data is fetched. I need to click on the screen for it to update.
I am using the Angular JS stack along with graphql. It seems though that the issue is to do with angular js only and not the API call.
The following is the api call in the JS:
graphql("...").then(
result => {
$scope.data = result.data;
});
HTML:
<div>{{data.length}}</div>
A cleaner approach is to convert the third-party promise to an AngularJS promise with $q.when:
$q.when(graphql("...")).then(
result => {
$scope.data = result.data;
});
AngularJS modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and AngularJS execution context. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc...1 Since the promise comes from outside the AngularJS framework, the framework is unaware of changes to the model and does not update the DOM.
$q.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
Try to add scope.$apply();. Like this:
graphql("...").then(
result => {
$scope.data = result.data;
$scope.$apply();
});
A better approach over $apply is $timeout.
The $timeout does not generate error like "$digest already in
progress" because $timeout tells Angular that after the current cycle,
there is a timeout waiting and this way it ensures that there will not
any collisions between digest cycles and thus output of $timeout will
execute on a new $digest cycle.
graphql("...").then(
result => {
$timeout(function() {
$scope.data = result.data;
});
});
We have a service, lets call it AccountService which exposes a method called getAccounts(customerId) among others.
In its implementation all it does is to fire up a $http GET request and return a promise to the calling controller which will put the returned array of accounts in the controller scope once resolved.
On a simplified view all looks like below:
// The service
.factory('AccountService', ['$http', function($http) {
var _getAccounts = function(customerId) {
var request = {
'method': 'GET',
'url': 'http://localhost:8081/accounts/' + customerId
};
return $(request);
};
return {
getAccounts: _getAccounts
};
}]);
// Inside the conntroller
AccountService.getAccounts($scope.customerId)
.then(function(response) {
$scope.accounts = response.data;
});
So once the promise will resolve the controller scope will get populated with the list of accounts.
Note that I kept the above code as simple as I could to get you the idea of what my problem is but in reality it will be code to deal with exceptions, watcher to refresh, etc. Everything works fine.
My problem is that this AccountService is used from lots of controllers and putting the promise resolve in all of these looks to me not only repeating all this boiler plate resolver code but also complicating the unit testing as I am obliged to r/test both successful and exception scenarios in every single controller test.
So my question is:
Is there a nice way to resolve the promise in the service and return the response to the controller, not the promise?
Please note I am a very beginner with Angular and JS so please be gentle if my question looks naive. I have heaps of java experience and my mind seems to go java like everywhere which may not be the case.
Thank you in advance for your inputs
To answer your original question:
Is there a nice way to resolve the promise in the service and return the response to the controller, not the promise?
In my opinion, no, there isn't. It boils down to the way asynchronous calls work - you either pass a callback (and the method returns nothing), or you don't pass a callback and the method returns an object which will be notified (a promise). There may be some workarounds, but I don't think it gets nicer than that.
One way to partially reduce the boilerplate is to use a catch in the service, and return the promise returned by it instead.
Consider the following extremely simplified example:
angular.module('myApp')
.factory('NetworkRequests', [
function() {
var _getData = function() {
var promise = new Promise((resolve, reject) => {
var a = true,
data = ['a', 'b', 'c'];
if (a) {
resolve(data);
} else {
reject('Rejection reason: ...');
}
});
return promise.catch((error) => {
// Notify some error handling service etc.
console.log(error);
return [];
});
};
return {
getData: _getData
};
}
]);
The promise variable would be the result from your http request. You should return some data in the catch function that makes sense in the controller context (e.g. empty array). Then you don't have to bother with error handling in the controller:
angular.module('myApp')
.controller('DataController', ['NetworkRequests',
function(NetworkRequests) {
NetworkRequests.getData().then((data) => {
this.data = data;
});
}
]);
Again, this doesn't solve the complete issue, but at least the error handling part can be encapsulated in the service.
You can design in such a way that once your $http is done with fetching the data, store it your factory variable (somewhat a cache), and for subsequent factory calls, you check if the cache has such data. If yes, return the cache data, else call the $http calls.
Here is the code:
.factory('AccountService', ['$http', '$q', function($http, $q) {
var cachedData = null;
var defered = $q.defer(); //create our own defered object
var _getAccounts = function(customerId) {
if (cachedData !== null) {
console.log('get from cachedData')
defered.resolve(cachedData); // resolve it so that the data is passed outside
return defered.promise; //return your own promise if cached data is found
} else {
var request = {
'method': 'GET',
'url': 'mockdata.json'
};
return $http(request).then((response) => { //return a normal $http promise if it is not.
console.log('get from $http');
cachedData = response.data;
return cachedData;
});
}
};
return {
getAccounts: _getAccounts
};
}]);
Here is the working plnkr. You can open up the console, and click the GetData button. You will see that first time it logs get from $http, where as subsequent calls it logs get from cachedData.
One way is to reuse an object and fill it with data. It is used by ngResource.
It is something like
var data = [];
function getAccounts(customerId) {
var promise = $http(...).then((response) => {
Object.assign(promise.data, response.data)
});
promise.data = [];
return promise;
};
Data is available for binding as $scope.accounts = AccountService.getAccounts(...).data. The obvious drawback is that there is a splash of unloaded content.
Another way is the one you've mentioned. It is being used most frequently. If there is a problem with WET code in controllers, it should be treated by eliminating WET code with class inheritance, not by changing the way it works.
Yet another way is the recommended one. Using a router and route/state resolvers eliminates the need for asynchronously loaded data. The data resolved in resolver is injected into route template as an array.
In my controller, I use a method from a factory to update some data. For example, I'm trying to fetch an updated array of users. Should I be returning the promise itself from the factory? Or should I be returning the data from the promise (not sure if I phrased that correctly)?
I ask because I've seen it both ways, and some people say that the controller shouldn't have to handle whether the request was successful or if it failed. I'm specifically talking about the promise returned from $http in this case.
Or maybe in other words, should I be using the then() method inside the factory, or should I be using it in the controller after returning from the factory?
I've tried to handle the success and error callbacks (using the this() method) from within the service, but when I return the data to the controller, the users array is not properly updated. I'm assuming that's because of the request being async. So in the controller, it would look something like this:
vm.users = userFactory.getUsers();
If I handle the promise from within the controller, and set the users array within the then() method, it works fine. But this goes back to where I should be using then():
userFactory.getUsers().then(
function(data){
vm.users = data;
}, ...
Hopefully someone would be able to shed some light on this or provide some input. Thanks!
There's no way you can return the data from the factory (since it's an async call) without using either a callback approach (discouraged):
userFactory.prototype.getUsers = function(callback){
$http.get('users').then(function (response) {
callback(response.data);
});
};
Or the promise approach.
If you're worried about handling the errors on the controller, then worry not! You can handle errors on the service:
userFactory.prototype.getUsers = function(){
return $http.get('users').then(function(response) {
return response.data;
}, function(error) {
// Handle your error here
return [];
});
};
You can return the results of then and it will be chained. So things from service will execute and then, later on, Controller ones.
I have no problem with controller deciding what to do basing on response failing/succeding. In fact it lets you easily handle different cases and doesn't add a lot of overhead to the controller (controller should be as small as possible and focused on current task, but for me going different path whether request failed is the part of its task).
Anyway, in Angular HTTP requests are wrapped in promises internally (I remember that in the previous versions there was a way to automatically unwrap them), so returning from service/factory always returns a promise, which has to be resolved.
I prefer returning a promise from a service/factory because I tend to let other classes decide what to do with the response.
New to Angular, may be using promises wrong. I have a factory returning a promise:
.factory('myData', ['$http', '$q', function($http, $q) {
var deferred = $q.defer();
$http.get('/path/to/endpoint')
.success(function(data) {
deferred.resolve(data);
})
.error(function(err) {
deferred.reject(err.what())
});
return deferred.promise;
}])
Now if I inject my factory somewhere I can use the promise:
.controller('myController', ['$scope', 'myData', function($scope, myData) {
myData.then(function(result) {
$scope.data = result;
});
}]);
This is fine, but I'm starting to use myData in several places and I don't want to be writing a new .then in every directive and controller I use the data in. After the promise is resolved I don't care about it anymore, is there any way to make myData return a promise if it's unresolved but return just the result after it's finished resolving?
To word it another way, can myData simple be the .then result after resolution, or do I have to write a new .then every time?
On working with promises
First of all your myData service can just return the call:
.factory('myData', ['$http', function($http) {
return $http.get('/path/to/endpoint').then(function(req){
return req.data;
});
}]);
Unwrapping values
So you know how to work with promises in Angular...
But, you want something better, you want the promise to automatically unwrap with Angular's digests. This is tricky but it can be done. Note that it can be confusing in code and I don't really recommend it but here's the general idea:
Automatic unwrapping
.factory('myData', ['$http', function($http) {
var result = []; // initial result to return
var p = $http.get('/path/to/endpoint').then(function(res){
result.push.apply(result, res.data); // add all the items
});
result.then = p.then; // allow hooking on it
return result; // return the array, initially empty
}]);
This would let you do something like:
.controller('myController', ['$scope', 'myData', function($scope, myData) {
$scope.data = myData;
}]);
Which will put an empty array there and will replace it (also causing a digest) whenever the real data arrives, you can still do myData.then in order to check if it's done loading yourself so you can use the old syntax if you need to be sure.
Is it a good idea?
Note that Angular used to do this until version 1.2 (removed completely in 1.3) but stopped doing so because automatic unwrapping was considered too magical. You can formulate your own decisions but take note that the Angular core team decided this was not a good idea. Note that things like ngResource still do this.
Yes, resolve the promise in your service/factory and reference the resolved promise value in your controllers instead of referencing and handling the promise in your controllers. Hope that makes sense.
I can’t get my head around some of the promises. They work fine in some instances of my app, but in some they just never work.
In my controller, I have the following command:
myFactory.redrawCategories()
.then(myFactory.redrawTasks());
The redrawTasks function is called instantly, without waiting for the redrawCategories to finish.
The functions inside my factory look like this
redrawTasks: function(){
var defer = $q.defer();
db.getAllDocs("task_").then(function(res) {
angular.forEach(res, function(value){
value = value.doc;
tasks[value.category].push(value);
});
angular.forEach(tasks, function(taskArray, cat){
// some code
});
defer.resolve(1);
});
return defer.promise;
},
The other one is like
redrawCategories: function(){
var deferred = $q.defer();
db.getAllDocs("category_").then(function(res) {
var categoryArray = [];
angular.forEach(res, function(value){
categoryArray.push(value.doc);
});
deferred.resolve("done");
});
return deferred.promise;
},
Some of the unimortant code has been removed for better overview.
No idea really how to do it. I've tried putting the resolve() function just in front of the return but that doesn't work either.
I've read that sometimes you have to wrap things in a $scope.$apply, well in this case most likely a $rootScope.$apply as it's in the factory outside of the controller scope, but that doesn't really change it either, besides I haven't really grasped when something is "outside of Angular" as they describe that.
I've read a lot of examples and tutorials, but I just don't see the forest for the trees anymore.
Any help would be appreciated a lot. I'm so stuck with this :/ Thanks
.then expects a function reference.
myFactory.redrawCategories()
.then(myFactory.redrawTasks());
should be
myFactory.redrawCategories()
.then(myFactory.redrawTasks);
When you have the () the function is executed immediately and .then is passed whatever it returned.
As noted in the comments, if you're relying on this in redrawTasks you'd do
myFactory.redrawCategories()
.then(function() {
return myFactory.redrawTasks();
});
Try:
myFactory.redrawCategories()
.then(myFactory.redrawTasks);
You were calling the function. Instead you just want to pass it to then function and have angular call it.