I have factory that's using indexedDB and method getRecipe that needs this indexed db to receive data.
Problem is that indexedDB returns it's instance in asynchronous call and getRecipe is another method that returns it's value in asynchronous call.
I try to solve it via promises, but I failed.
app.factory('RecipesStorage', ['$q', function($q) {
var getDb = function () {
var deferred = $q.defer();
var db;
var request = indexedDB.open('recipes', 1);
request.onupgradeneeded = function (e) {
console.log('Upgrading indexedDb');
var thisDb = e.target.result;
if (!thisDb.objectStoreNames.contains('recipe')) {
thisDb.createObjectStore('recipe');
}
}
request.onsuccess = function (e) {
db = e.target.result;
window.db = db;
deferred.resolve(db);
}
request.onerror = function (e) {
console.error('Error when opening indexedDB');
}
return deferred.promise;
};
var getRecipe = function (recipeId, callback) {
getDb().then(function(db) {
var transaction = db.transaction(['recipe'], 'readonly');
var objectStore = transaction.objectStore('recipe');
var data = objectStore.get(recipeId);
data.onsuccess = function (e) {
var result = e.target.result;
console.log('GetRecipe:', result);
callback(result);
}
data.onerror = function (e) {
console.error('Error retrieving data from indexedDB', e);
}
});
};
return {getRecipe:getRecipe};
}]);
But this doesent work. In getRecipe the function in then isn't invoked. I don't know where is problem.
We can use promise chain to make it work.
(I don't have database so i simulated async response and wrapped data with $q)
Demo Fiddle
fessmodule.controller('fessCntrl', function ($scope, RecipesStorage) {
$scope.alertSwap = function () {
RecipesStorage.getDb()
.then(function (result) {
$scope.data = result;
}, function (result) {
alert("Error: No data returned");
});
}
});
fessmodule.$inject = ['$scope', 'RecipesStorage'];
fessmodule.factory('RecipesStorage', ['$q', function ($q) {
var getDb = function () {
var data = [{
"PreAlertInventory": "5.000000",
"SharesInInventory": "3.000000"
} ];
var deferred = $q.defer();
deferred.resolve(data);
return getRecipe(data);
};
var getRecipe = function(db){
var data = [{
"TotalSharesBought": "0.000000",
"TotalShareCost": "0.000000",
"EstimatedLosses": "0.000000"
}];
db.push(data[0]);
var deferred = $q.defer();
deferred.resolve(db);
return deferred.promise;
}
var factory = {
getDb: getDb
};
return factory;
}]);
Reference
You can chain promises to create code flows
Error propagates, so you can catch it on the end of the chain
I can't see a problem in your code. Maybe this plunker helps you to figure out where the problem is. I created it based on your code and it apparently works.
The real problem was that in my version I've used angular in version 1.0.8, but when I switched it to version 1.2.0 it starts working as expected. Thanks :)
Related
I want to write the unit test for the factory which have lot chain of promises. Below is my code snippet:
angular.module('myServices',[])
.factory( "myService",
['$q','someOtherService1', 'someOtherService2', 'someOtherService3', 'someOtherService4',
function($q, someOtherService1, someOtherService2, someOtherService3, someOtherService4) {
method1{
method2().then(
function(){ someOtherService3.method3();},
function(error){/*log error;*/}
);
return true;
};
var method2 = function(){
var defer = $q.defer();
var chainPromise = null;
angular.forEach(myObject,function(value, key){
if(chainPromise){
chainPromise = chainPromise.then(
function(){return method4(key, value.data);},
function(error){/*log error*/});
}else{
chainPromise = method4(key, value.data);
}
});
chainPromise.then(
function(){defer.resolve();},
function(error){defer.reject(error);}
);
return defer.promise;
};
function method4(arg1, arg2){
var defer = $q.defer();
someOtherService4.method5(
function(data) {defer.resolve();},
function(error) {defer.reject(error);},
[arg1,arg2]
);
return defer.promise;
};
var method6 = function(){
method1();
};
return{
method6:method6,
method4:method4
};
}]);
To test it, I have created spy object for all the services, but mentioning the problematic one
beforeEach( function() {
someOtherService4Spy = jasmine.createSpyObj('someOtherService4', ['method4']);
someOtherService4Spy.method4.andCallFake(
function(successCallback, errorCallback, data) {
// var deferred = $q.defer();
var error = function (errorCallback) { return error;}
var success = function (successCallback) {
deferred.resolve();
return success;
}
return { success: success, error: error};
}
);
module(function($provide) {
$provide.value('someOtherService4', someOtherService4);
});
inject( function(_myService_, $injector, _$rootScope_,_$q_){
myService = _myService_;
$q = _$q_;
$rootScope = _$rootScope_;
deferred = _$q_.defer();
});
});
it("test method6", function() {
myService.method6();
var expected = expected;
$rootScope.$digest();
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
});
It is showing error on
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
After debugging I found that it is not waiting for any promise to resolve, so method 1 return true, without even executing method3. I even tried with
someOtherService4Spy.method4.andReturn(function(){return deferred.promise;});
But result remain same.
My question is do I need to resolve multiple times ie for each promise. How can I wait till all the promises are executed.
method1 does not return the promise so how would you know the asynchrounous functions it calls are finished. Instead you should return:
return method2().then(
method6 calls asynchronous functions but again does not return a promise (it returns undefined) so how do you know it is finished? You should return:
return method1();
In a test you should mock $q and have it resolve or reject to a value but I can't think of a reason why you would have a asynchronous function that doesn't return anything since you won't know if it failed and when it's done.
Method 2 could be written in a more stable way because it would currently crash if the magically appearing myObject is empty (either {} or []
var method2 = function(){
var defer = $q.defer();
var keys = Object.keys(myObject);
return keys.reduce(
function(acc,item,index){
return acc.then(
function(){return method4(keys[index],myObject[key].data);},
function(err){console.log("error calling method4:",err,key,myObject[key]);}
)
}
,$q.defer().resolve()
)
};
And try not to have magically appearing variables in your function, this could be a global variable but your code does not show where it comes from and I doubt there is a need for this to be scoped outside your function(s) instead of passed to the function(s).
You can learn more about promises here you should understand why a function returns a promise (functions not block) and how the handlers are put on the queue. This would save you a lot of trouble in the future.
I did below modification to get it working. I was missing the handling of request to method5 due to which it was in hang state. Once I handled all the request to method 5 and provided successCallback (alongwith call to digest()), it started working.
someOtherService4Spy.responseArray = {};
someOtherService4Spy.requests = [];
someOtherService4Spy.Method4.andCallFake( function(successCallback, errorCallback, data){
var request = {data:data, successCallback: successCallback, errorCallback: errorCallback};
someOtherService4Spy.requests.push(request);
var error = function(errorCallback) {
request.errorCallback = errorCallback;
}
var success = function(successCallback) {
request.successCallback = successCallback;
return {error: error};
}
return { success: success, error: error};
});
someOtherService4Spy.flush = function() {
while(someOtherService4Spy.requests.length > 0) {
var cachedRequests = someOtherService4Spy.requests;
someOtherService4Spy.requests = [];
cachedRequests.forEach(function (request) {
if (someOtherService4Spy.responseArray[request.data[1]]) {
request.successCallback(someOtherService4Spy.responseArray[request.data[1]]);
} else {
request.errorCallback(undefined);
}
$rootScope.$digest();
});
}
}
Then I modified my test as :
it("test method6", function() {
myService.method6();
var expected = expected;
var dataDict = {data1:"data1", data2:"data2"};
for (var data in dataDict) {
if (dataDict.hasOwnProperty(data)) {
someOtherService4Spy.responseArray[dataDict[data]] = dataDict[data];
}
}
someOtherService4Spy.flush();
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
});
This worked as per my expectation. I was thinking that issue due to chain of promises but when I handled the method5 callback method, it got resolved. I got the idea of flushing of requests as similar thing I was doing for http calls.
I have the following controller:
.controller('SponsorsCtrl', function ($scope, Sponsors, $http) {
$scope.$on('$ionicView.enter', function () {
Sponsors.all($http).then(function (data) {
$scope.sponsors = data;
var check = "check";
})
});
})
The reason for using "then" is because I received an asynchronous object. Now I can however also receive a synchronous object via the following service:
(function(){
angular
.module('sponsors.services', [])
.factory('Sponsors', Sponsors);
Sponsors.$inject = [];
function Sponsors() {
var service = {
all: all,
allServer: allServer,
allLocal: allLocal,
get: get,
getTimeStamp: getTimeStamp
};
return service;
function all($http) {
var timeDifference = (Date.now() - this.getTimeStamp());
if (timeDifference < 600000) {
return this.allLocal();
}
else {
return this.allServer($http);
}
}
function allServer($http) {
return $http.get("http://dream16backend.azurewebsites.net/api/dream16/sponsors")
.then(function (resp) {
//Set localstorage, create timestamp and return the data
window.localStorage.setItem('sponsors', resp.data);
window.localStorage.setItem('sponsorsTimeStamp', Date.now());
var bla = JSON.parse(window.localStorage.getItem('sponsors'));
return bla;
}, function(err) {
console.log('ERR', err);
});
}
function allLocal() {
return JSON.parse(window.localStorage.getItem('sponsors'));
}
function get(adressId) {
for (var i = 0; i < sponsors.length; i++) {
if (sponsors[i].id === parseInt(sponsorId)) {
return sponsors[i];
}
}
return null;
}
function getTimeStamp() {
return window.localStorage.getItem('sponsorsTimeStamp');
}
}
})();
This way only the async call (function allServer) works, but the sync fails becaus: Sponsors.all(...).then is not a function
then I thought the fix was to move the "then" functionality to the all function in the service. This makes the sync call (function allLocal) work, but now the async call fails. The else condition now looks like this:
else {
this.allServer($http).then(function (data) {
return data;
})
}
And the controller looks like:
.controller('SponsorsCtrl', function ($scope, Sponsors, $http) {
$scope.$on('$ionicView.enter', function () {
$scope.sponsors = Sponsors.all($http);
var check = "check";
});
})
I verified that the call itself is working (via test variable "bla"). I also see that the controller the controller runs var check = "check"; before running the async code. What am I doing wrong here?
OK...so you need to return a promise for both instances of Sponsors.all() since one instance is already returning $http promise.
Inject $q in service so that allLocal() will also return a promise.
function allLocal() {
return $q.resolve(JSON.parse(window.localStorage.getItem('sponsors')));
}
And in controller you need to use then()
$scope.$on('$ionicView.enter', function () {
Sponsors.all($http).then(function(data){
$scope.sponsors = data;
});
var check = "check";
});
As mentioned in comments above there is no need to inject $http in controller and pass it to service when it would be simpler to just inject $http in service where it is actually needed
I'd propose you the following solution. In both cases return "Promise" object. For allLocal function it will look like this:
function allLocal() {
var deferred = $q.defer();
deferred.resolve(JSON.parse(window.localStorage.getItem('sponsors')));
return deferred.promise;
}
So now you can use .then in both cases - sync and async
I'd recommend injecting the $http service into your service.. I.e.
.factory('MyService', function ($http, $timeout,$q) {
var service = {
all: all,
allServer: allServer,
allLocal: allLocal,
get: get,
getTimeStamp: getTimeStamp
};
return service;
function all() {
var timeDifference = (Date.now() - this.getTimeStamp());
if (timeDifference < 600000) {
return this.allLocal();
}
else {
return this.allServer($http);
}
}
function allServer() {
return $http.get("http://dream16backend.azurewebsites.net/api/dream16/sponsors")
.then(function (resp) {
//Set localstorage, create timestamp and return the data
window.localStorage.setItem('sponsors', resp.data);
window.localStorage.setItem('sponsorsTimeStamp', Date.now());
var bla = JSON.parse(window.localStorage.getItem('sponsors'));
return bla;
}, function(err) {
console.log('ERR', err);
});
}
function allLocal() {
var dfd = $q.defer(); //create a deferred object
$timeout(function(){
var localResponse = JSON.parse(window.localStorage.getItem('sponsors'));;
dfd.resolve(localResponse); //resolve the localObject
});
return dfd.promise; //return the promise object so controller gets the .then function
}
function get(adressId) {
for (var i = 0; i < sponsors.length; i++) {
if (sponsors[i].id === parseInt(sponsorId)) {
return sponsors[i];
}
}
return null;
}
function getTimeStamp() {
return window.localStorage.getItem('sponsorsTimeStamp');
}
})
If you are dealing with a service that may or may not return a promise, you can use $q.when(...) to wrap that API call and let $q handle the rest.
In your case all you have to do is wrap your service api like so $q.when(Sponsors.all($http)) and use it as any regular promise.
check out https://github.com/kriskowal/q/wiki/API-Reference#promise-methods.
I want to access the updated value out site the scope like the following scenario.
Var data = [];
var getData = function()
{
var onSuccess = function(result)
{
data = result;
}
var onError = function(err)
{
console.log(err);
}
ApiCall().then(onSuccess, onError);
}
getData();
console.log(data);
Here in the above case data is always coming blank array [] not the updated one. how can i access the updated data outside the scope?
You can't and you shouldn't. You need to synchronize your code if you want it to run asynchronously. You can do this by using the promise chain:
return ApiCall().then(onSuccess, onError);
...
getData().then(console.log);
Try the following - you basically need to return your promise.
Var data = [];
var getData = function()
{
var onSuccess = function(result)
{
data = result;
return data;
}
var onError = function(err)
{
console.log(err);
}
return ApiCall().then(onSuccess, onError);
}
getData().then(function (data) { console.log(data); });
Basically I have the function getUserInfo which has one function that returns available and assigned groups within the service. Then I have the other two functions below return those objects. However, I can't run the getAssignedGroups() and getAvailableGroups() before the first function is done. So I thought I'd use the then() to ensure those two ran once the first function was completed.
I have this function in a controller:
$scope.getUserInfo = function(selectedUser) {
$scope.userGroupInfo = groupService.getUserGroups(selectedUser.domain,$scope.groups).then(
$scope.assignedGroups = groupService.getAssignedGroups(),
$scope.availableGroups = groupService.getAvailableGroups()
);
};
This is my service:
spApp.factory('groupService', function () {
var assignedGroups, availableGroups, allGroups;
var getGroups = function () {
allGroups = [];
$().SPServices({
operation: "GetGroupCollectionFromSite",
completefunc: function(xData, Status) {
var response = $(xData.responseXML);
response.find("Group").each(function() {
allGroups.push({
id: $(this).attr('ID'),
name: $(this).attr('Name'),
Description: $(this).attr('Description'),
owner: $(this).attr('OwnerID'),
OwnerIsUser: $(this).attr('OwnerIsUser'),
});
});
}
});
return allGroups;
}
var getUserGroups = function (selectedUser, allGroups) {
assignedGroups = [];
$().SPServices({
operation: "GetGroupCollectionFromUser",
userLoginName: selectedUser,
completefunc: function(xData, Status) {
var response = $(xData.responseXML);
response.find("errorstring").each(function() {
alert("User not found");
booErr = "true";
return;
});
response.find("Group").each(function() {
assignedGroups.push({
id: $(this).attr('ID'),
name: $(this).attr('Name'),
Description: $(this).attr('Description'),
owner: $(this).attr('OwnerID'),
OwnerIsUser: $(this).attr('OwnerIsUser'),
});
});
}
});
//from here I start comparing All Groups with user groups to return available groups
var assignedGroupsIds = {};
var groupsIds = {};
var availableGroups = [];
assignedGroups.forEach(function (el, i) {
assignedGroupsIds[el.id] = assignedGroups[i];
});
allGroups.forEach(function (el, i) {
groupsIds[el.id] = allGroups[i];
});
for (var i in groupsIds) {
if (!assignedGroupsIds.hasOwnProperty(i)) {
availableGroups.push(groupsIds[i]);
}
}
/* return {
assignedGroups:assignedGroups,
availableGroups:availableGroups
}*/
}
var getAvailableGroups = function () {
return availableGroups;
}
var getAssignedGroups = function () {
return assignedGroups;
};
return {
getGroups:getGroups,
getUserGroups:getUserGroups,
getAvailableGroups: getAvailableGroups,
getAssignedGroups:getAssignedGroups
}
});
You can inject the Angular promise module into your factory:
spApp.factory('groupService', function ($q) {
Then inside the getUserGroups() function of your factory, declare a deferred object:
var deferred = $q.defer();
Then you need to use deferred.resolve(response) inside your async request and return deferred.promise from your function. Also, the then function takes a function with the returned response as the argument (so you can then access the response from the controller):
$scope.userGroupInfo = groupService.getUserGroups(selectedUser.domain,$scope.groups).then(function(resp) {
$scope.assignedGroups = groupService.getAssignedGroups(),
$scope.availableGroups = groupService.getAvailableGroups()
});
Look into the AngularJS docs on $q for more info.
Better approach will be use callback approach. You can take following approach.
$scope.getUserInfo = function(selectedUser, getUserInfoCallback) {
$scope.userGroupInfo = groupService.getUserGroups(selectedUser.domain,$scope.groups, function (response){
$scope.assignedGroups = groupService.getAssignedGroups( function (response){
$scope.availableGroups = groupService.getAvailableGroups( function(response){
//Here call parent callback
if(getUserInfoCallback)
{
getUserInfoCallback(response); //If you need response back then you can pass it.
}
})
})
})
);
};
Now you need to define each method as callback. Following is sample method for your reference.
$scope.getAssignedGroups = function (getAssignedGroupsCallback){
//Do all operation
//Now call callback
if(getAssignedGroupsCallback)
{
getAssignedGroups();//you can pass data here which you would like to get it return
}
};
Note: I have just added the code to clarify the approach. You need to change it to compile and implement your own logic.
Summary of approach: All services are invoked asynchronously therefore you need to pass callback method as chain (to keep your code clean) so that your code can wait for next step.
Hope it helps.
I am following this Angular tutorial on promises. I have a service that checks if an array is empty and if so, hits a REST service, returns a promise and updates the array. Here is the relative code:
requestingProviders: [],
getRequestingProviders: function() {
var that = this;
var deferred = $q.defer();
if(this.requestingProviders.length === 0) {
this.getResource().search({
role: 'REQUESTING_PROVIDER'
}, function(data) {
that.requestingProviders = data.providers;
deferred.resolve(data.providers);
});
return deferred.promise;
} else {
return that.requestingProviders;
}
}
The service is being called from a controller. Here is the code where it is being called:
$scope.providers = providerService.getRequestingProviders();
The REST call is made and returns fine, but the view is never updated. This is not working like the tutorial explained. Here is a plunker that shows what I am expecting. What am I doing wrong?
You need to resolve your promise:
var prom = providerService.getRequestingProviders();
prom.then(function (data) {
$scope.providers = data;
});
Also, change your code to always return the promise:
getRequestingProviders: function() {
var that = this;
var deferred = $q.defer();
if(this.requestingProviders.length === 0) {
this.getResource().search({
role: 'REQUESTING_PROVIDER'
}, function(data) {
that.requestingProviders = data.providers;
deferred.resolve(data.providers);
});
} else {
deferred.resolve(that.requestingProviders);
}
return deferred.promise;
}