Promise factory in Angular - javascript

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.

Related

How callbacks work with $scope in Angularjs

Imagine we have an external module has a function that needs a callback parameter for a async task. As our parameter is a function, and they call that callback function in their module, so how do we use $scope that is out of our digest cycle? How angularjs handles such things?
And also we use $scope.apply() or $scope.digest() in such situations, so how do they work?
Note: I don't need you to provide some codes necessarily, just want to know about concepts. Thanks.
externalModule.doSomething(param, function(result) {
$scope.result = result;
$scope.$apply();
})
That's it actually. However it is good style to wrap all external modules - as an example you can look at $http, $interval, $timeout wraping calls to javascript functions.
So you should put this code in some factory externalModuleWrap:
module.factory('externalModuleWrap', function($rootScope, $q) {
return {
doSomething: function(param) {
var defer = $q.defer();
externalModule.doSomething(param, function(result) { defer.resolve(result); }
$rootScope.$apply();
return defer.promise;
}
}
})
Now you can call externalModuleWrap from any point of your angular project, without problems.

Resolving a promise in Angular Service

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.

how to call a factory from a regular function in AngularJs

I have this factory which is called by some controllers.
app.factory('fileData', function($http) {
return {
get: function(filename){ return $http({ method: 'GET', url: filename});
}
};
});
Now I want to call it from a regular function and return the data from the factory. How can I do that? This one does not work because the fileData passed in is not recognized without $scope.
function getData (file, fileData) {
rels = [];
var handleSuccess = function(data, status) {
rels = data;
console.log(rels);
};
fileData.get(filename).success(handleSuccess);
return rels;
}
Any idea how to go around this?
Move return rels inside handleSuccess, you're returning before handleSuccess is getting called. So you're signaling that the function completed execution prematurely.
function getData (file, fileData) {
rels = [];
var handleSuccess = function(data, status) {
rels = data;
console.log(rels);
return rels; // Return rels when the Promise is finished
};
fileData.get(file).success(handleSuccess);
}
Since you're returning the promise from $http, you need to handle its execution in an asynchronous way which, is simply not returning until the promise is resolved.
It would also be a good idea to add a reject handler.
Additionally, .success() is deprecated and you should use .then() and .catch() respectively for handling resolve() and reject() of the Promise appropriately.
See below for the preferred approach to Promises with $http
fileData.get(file)
.then(handleSuccess)
.catch(handleFailure); // Handle any errors returned from $http
EDIT Update for DI example for controller
You need to inject the fileData factory into your Angular Controller. For more info on Dependency Inject see the Angular Docs for DI. Just to note, DI is a huge piece of Angular so it is pretty fundamental to understand this before proceeding.
angular
.module('yourApp')
.controller('yourController', ['$scope', 'fileData', function($scope, fileData) {
// Expose getData via $scope
$scope.getData = getData;
function getData(file, fileData) {
var rels = [];
var handleSuccess = function(data, status) {
rels = data;
console.log(rels);
return rels;
};
fileData.get(file)
.then(handleSuccess);
}
}]);
Your call to the method exposed in the factory (get) uses the $http service which returns a promise. So you cannot get the result of this call in a synchronous way.
In your sample the return rels happens way before the handleSuccess method is called and thus you are returning the old value (an empty array).
The reason it works inside of a controller function is that the controller instance is living for a longer time and when the server call returns successfully, the variable in you controller's $scope is assigned (which then is reflected in your UI via the angular data binding).
So the best way to solve this, is to avoid having a synchronous api in your "regular function". Could you probably make this also async by returning a promise?

AngularJS $q.resolve(), ES6 Promise.resolve() (and other animals)

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 ;) )

Angularjs $http.get in a factory doesn't return valid JSON but a different object

When I call $http.get from a factory it doesn't return the JSON object I need but a different one.
Here's my code:
var cart = angular.module('cart', []);
cart.factory('getItems',['$http', function($http){
return{
get:$http.get('index.php').
success(function(data){
return data;
})
}
}]);
cart.controller("TableCtrl", ['$scope', 'getItems', function($scope, getItems){
console.log(getItems.get);
}]);
now this is the JSON object that it should return:
[{"id":"1","name":"Cap"},{"id":"2","name":"Coat"},{"id":"3","name":"Shoes"}]
And this is what I get when I console.log
d {$$state: Object, success: function, error: function, then: function, catch: function…}
everything runs fine if I run the $http.get inside the controller, but if I place it in a factory it just doesn't.
It's returning a promise, which you need to resolve
In your controller..
getItems.get().then(function(response) {
console.log(response.data)
});
More information, see The Promise Api
Angular $q docs
$http.get returns a promise not the data itself. Your current use of the success function is not going to get data back.
In your factory:
return{ get: $http.get('myurl')};
Then consuming it should be:
myFactory.get().success(response){
var mydata = response.data;
}
Success is just an addition to the promise. I like to just use .then like standard, unadorned promises.
When I first heard about promises, I thought it meant you could use it without having to provide any sort of callback-like code on your consuming end. That is not true, but promises are still super awesome and very powerful. The coolest thing is that you can keep a promise around, and anybody can add a .then to the promise. Whoever does will get their code executed as soon as the asynchronous request finishes, or right away if it has already finished. So promises let you not have to worry about asynchronous behavior, but you still have to use what is basically its more powerful version of callbacks.

Categories

Resources