AngularJS : How to test promise logic with a resolve in it? - angularjs

I have a service method that has some caching logic:
model.fetchMonkeyHamById = function(id)
{
var that = this;
var deferred = $q.defer();
if( that.data.monkeyHam )
{
deferred.resolve( that.data.monkeyHam );
return deferred.promise;
} else {
return this.service.getById( id).then( function(result){
that.data.monkeyHam = result.data;
});
}
};
I know how to use $httpBackend to force the mocked data to be returned. Now, how do I force it to resolve (and then test the result) when I've set the data explicitly?
I want to test the result in the controller then() function:
MonkeyHamModel.fetchMonkeyHamById(this.monkeyHamId).then( function() {
$scope.currentMonkeyHam = MonkeyHamModel.data.monkeyHam;
});
Then my test I want to explicitly set the data (so it loads from memory "cache" instead of httpBackend)
MonkeyHamModel.data.monkeyHam = {id:'12345'};
MonkeyHamModel.fetchMonkeyHamById( '12345');
// How to "flush" the defer right here like I would have flushed httpBackend?
expect( $scope.currentMonkeyHam.id ).toEqual('12345'); //fails because it is not defined yet since the $q never resolved
Where $scope is just the scope of my controller, but called $scope here for brevity.
UPDATE:
The suggested answer does not work. I need the function to return a promise, not a value that is the result of a promise:
model._monkeyHams = {} // our cache
model.fetchMonkeyHamById = function(id){
return model.monkeyHams[id] || // get from cache or
(model.monkeyHams[id] = this.service.getById(id).then(function(result){
return result.data;
}));
};
The following requires that you have touched the server already. I create a model on the front end (currentMonkeyHam) or whatever, and don't load it back after the first POST (an unnecessary GET request). I just use the current model. So this does not work, it requires going out to the server at least once. Therefore, you can see why I created my own deferred. I want to use current model data OR get it from the server if we don't have it. I need both avenues to return a promise.
var cache = null;
function cachedRequest(){
return cache || (cache = actualRequest())
}

Your code has the deferred anti pattern which makes it complicated - especially since you're implicitly suppressing errors with it. Moreover it is problematic for caching logic since you can end up making multiple requests if several requests are made before a response is received.
You're overthinkig it - just cache the promise:
model._monkeyHams = {} // our cache
model.fetchMonkeyHamById = function(id){
return model.monkeyHams[id] || // get from cache or
(model.monkeyHams[id] = this.service.getById(id).then(function(result){
return result.data;
}));
};
In your case, you were caching all IDs as the same thing, the general pattern for caching promises is something like:
var cache = null;
function cachedRequest(){
return cache || (cache = actualRequest())
}
Creating deferred is tedious and frankly - not very fun ;)

You can use setTimeout (or $timeout) for resolving the promise.
You can modify your code as -
model.fetchMonkeyHamById = function(id)
{
var that = this;
var deferred = $q.defer();
if( that.data.monkeyHam )
{
setTimeout(function() {
deferred.resolve(that.data.monkeyHam);
}, 100);
return deferred.promise;
} else {
return this.service.getById( id).then( function(result){
that.data.monkeyHam = result.data;
});
}
};
EDIT:
Modified as per Benjamin's suggestion -
Using $rootScope.digest() - code should be something like this
MonkeyHamModel.data.monkeyHam = {id:'12345'};
MonkeyHamModel.fetchMonkeyHamById( '12345');
$rootScope.digest();

We've done something similar in our code base, but instead of having an object with state that constantly changed we went with something that looks more like a traditional repository.
someInjectedRepoistory.getMonkeyHamModel().then(x => $scope.monkeyHam = x);
Repository{
getMonkeyHamModel() {
var deferred = $q.defer();
if( this.cache.monkeyHam )
{
deferred.resolve( this.cache.monkeyHam );
} else {
return this.service.getById( id).then( function(result){
this.cache.monkeyHam = result.data;
});
}
return deferred.promise
}
}
There are no problems with returning a completed deferred. That's part of the purpose of the deferreds, it shouldn't matter when or how they are resolved, they handle all of that for you.
As for your test we do something like this.
testGetFromService(){
someInjectedRepoistory.getMonkeyHamModel().then(x => verifyMonkeyHam(x));
verifyHttpBackEndGetsCalled()
}
testGetFromCache(){
someInjectedRepoistory.getMonkeyHamModel().then(x => verifyMonkeyHam(x));
verifyHttpBackEndDoesNotGetCalled()
}

Related

Need to use $scope.apply() otherwise view sometimes slow

So i'm building a web application in AngularJS that connects to an API (Qlik Sense Engine API) with QSocks.
Qsocks is a lightweight wrapper around the Qlik Sense Engine API wrapper that is written in NodeJS but can also be imported in a web environment. QSocks contains and uses the NPM package Promise package so it uses it's own non AngularJS promises.
My service looks like this:
var app_promise = (appFactory.activeConnection() ? appFactory.activeConnection() : appFactory.app());
this.getData = function(qMeasures, time) {
ratioChild.qHyperCubeDef.qMeasures[0].qDef.qDef = qMeasures;
ratioChild.qHyperCubeDef.qMeasures[0].qDef.qLabel = qMeasures;
ratioChild.qHyperCubeDef.qDimensions[4].qDef.qFieldDefs = [time];
ratioChild.qHyperCubeDef.qDimensions[4].qDef.qFieldLabels = [time];
var deferred = $q.defer();
app_promise.then(function (obj) {
obj.createChild(ratioChild).then(function (childObj) {
deferred.resolve(childObj);
});
});
return deferred.promise;
}
In simple words, when i call this service in e.g. a controller. I get an object where i can build other objects on it.
Side Note:
I do need to make a new AngularJS promise because app_promise.then and obj.createChild(ratioChild).then are the NPM promise package promises.
This is how my controller looks like (first part):
if (!$rootScope.balanceSheetFixedObj) {
var fixYearsqMeasure = "Sum({<Jaar=>}Saldo)";
balanceSheetService.getData(fixYearsqMeasure, self.time).then(function (childObj) {
$rootScope.balanceSheetFixedObj = childObj;
return childObj;
}).then(handleFixData)
} else {
handleFixData();
}
This is how my controller looks like (second part):
function handleFixData(childObj) {
childObj = (childObj) ? childObj : $rootScope.balanceSheetFixedObj;
childObj.getLayout().then(function(data) {
self.data = data;
if (data.qHyperCube.qPivotDataPages[0].qData.length > 0) {
var fixPivotData = data.qHyperCube.qPivotDataPages[0];
self.labels = fixPivotData.qLeft;
$scope.$apply(); // Here is my problem!
With $scope.$apply() my view is publishes/updated after a second.
If i leave out the $scope.$apply() it do publish/update the view but after 10-15 Seconds.. Way to late! Why is my view so slow? I would like to leave out the $scope.$apply()
I manage to solve my own problem. After looking back it was quite obvious what my problem was.
Thanks to #charlietfl i've taken a look to the childObj.getLayout(). What i saw what that the getLayout() function returns a QSocks promise and the code that updates my view was written inside of the .then() of the QSocks promise. As getLayout() is not an angular promise, this was the problem. My view was not updated properly.
My solution was to create a service function that creates an Angular Promise
this.getObjLayout = function(childObj) {
var deferred = $q.defer();
childObj.getLayout().then(function(data) {
deferred.resolve(data);
});
return deferred.promise;
}
And in the controller i invoke that function
function handleFixData(childObj) {
childObj = (childObj) ? childObj : $rootScope.balanceSheetFixedObj;
balanceSheetService.getObjLayout(childObj).then(function (data) {
self.data = data;
if (data.qHyperCube.qPivotDataPages[0].qData.length > 0) {
var fixPivotData = data.qHyperCube.qPivotDataPages[0];
self.labels = fixPivotData.qLeft;
}
})
}

Always return data from service

I have a series of services that either fetch data from an API server, or return data if it exists in local storage.
.factory('LogEntryResource', function(config, $resource) {
return $resource(config.apiUrl + 'logentries/:id/');
})
.factory('LogEntry', function(localStorageService, LogEntryResource) {
var localLogEntries = localStorageService.get("logEntries");
return {
all: function() {
if(localLogEntries){
return localStorageService.get("logEntries");
} else {
return LogEntry.query(function(data){
localStorageService.set("logEntries", data);
return data;
});
}
},
get: function(logEntryId){
...
},
delete: function(logEntryId){
...
},
update: function(logEntryId){
...
}
}
})
The problem is that in the app controllers sometimes a promise is returned, and sometimes the data is returned, so I need to handle the return value of LogEntry.all() to either wait for the promise to resolve or to use the data.
I'm not really sure how to go about it because I can either use a .then() which works for the promise, but is undefined if it has data, or vice-versa. I know I'm doing something wrong and looking for advice how to handle this situation of dealing with either data or a promise being returned.
.controller('LogEntryCtrl', function($scope, LogEntry) {
// var data = LogEntry.all();
// var promise = LogEntry.all();
$scope.logEntry = ???
}
I'm hoping there's a nice reusable solution instead of having to do a check to see what it is every time I use this code in my controllers/routes
// trying to avoid doing this
var logEntry = LogEntry.all();
if(logEntry.isPromise){
// do promise stuff here
} else if(logEntry.isData {
// do data stuff here
}
My suggestion would be always return a promise. You can use $q.resolve() to create a shortcut for a resolved promise
.factory('LogEntry', function(localStorageService, LogEntry, $q) {
var localLogEntries = localStorageService.get("logEntries");
return {
all: function() {
if(localLogEntries){
return $q.resolve(localLogEntries);
} else {
return LogEntry.query(function(data){
localStorageService.set("logEntries", data);
// update variable also
localLogEntries = data;
return localLogEntries ;
});
}
},
In controller you always use then() this way
LogEntry.all().then(function(data){
$scope.data = data;
});

Angular design pattern for self fulfilling promise

So been struggling a bit with getting promises to work properly, but after a lot of work, think I have gotten it. So now comes the question, can you create a self fulfilling promise, if you don't want to wait for the real thing...
Short pseudo version of what I want to do
var promise;
if (!factory.isDataLoaded()){
//The data is not loaded
var promise = factory.init();
} else {
//Data is all loaded
var promise = getSelfFullfilingPromise
}
//Some other code
promise.then(function({
//Do some stuff with the data from factory. which we know is loaded
})
Consider this option (taken straight from my app). I have a factory that loads up a couple of tables, especially one, it translates ids from one table to arrays of names, status and so forth from another table. Then the code does all kinds of wonderful things with it.
OK, it does some massaging and makes pretty diagrams. Problem is that if the initiation of the factory (i.e. do an API call, get some data, store it in a variable) haven't finished, half my page does not render, my boss gets angry, I get fired, and I'll have to dumpster dive behind McDonald (OK, not quite that bad).
So moved my init api call to a promise, sweet.
Then created a helper function that returns status (it checks if there is data in a variable and returns true or false). And I have the request it self (idGetSkill).
I then also have a directive which is called about 20 times, so I don't really want to call my init 20 times in order to ensure that there is data. I only whant to call it if the data is empty, or of skillLoaded returns false.
But if I use .then as a callback, I need a promise that resolves for it to run. So was thinking.
An example of where it could be used:
The Factory
.factory('skillFactory', function($http) {
var skillFactory = [];
var skills = [];
var searchId = [];
var mySkillId = [];
skillFactory.init= function() {
console.log("Got called")
return $http.get('/api/skillList')
.then(function(data){
skills=data['data']
console.log('Skill test ' + skills[0].alias );
})
}
skillFactory.skillLoaded=function(){
if(skills.length < 1) {
console.log("Warning, no data");
return false;
} else {
return true;
}
}
skillFactory.idGetSkill = function(data) {
if (skills.length < 1){
console.log("Warning, no data");
} else {
for (var id in skills) {
if (data == skills[id]._id) {
return skills[id];
}
}
}
}
}
And an app calling it
.controller("PromiseLoad", function ($scope, $http, $window, skillFactory ) {
var promise;
if( ! skillFactory.skillLoaded() ){
promise = skillFactory.init();
} else {
promise = skillFactory.init();
}
var skill = '55c8a069cca746f65c9836a1'
console.log("Will ask for skill " + skill)
promise.then( function() {
console.log("Im done waiting!")
$scope.answer = skillFactory.idGetSkill(skill);
console.log ("And got " +$scope.answer.alias);
})
});
(OK, the above example does not really need it, but its easier to provide this example rather than a directive as that needs a lot more things to work. Like data and stuff:) )
$q.when(data) returns a resolved promise.
$q.reject(data) returns a rejected promise.

How to asynchronously call functions

I have some list of functions to call in controller say
$scope.trader = {};
$scope.getUserLocation();
$scope.searchPaymentMethod($scope.trader.cityname);
$scope.getUserCurrency();
and each of these functions make an HTTP call to get some data. Say getUserLocation does this...
$scope.getUserLocation = function() {
var dataPromise = locationServiceCustomised.getCurrentLocation();
dataPromise.then(function(result) {
$scope.trader.cityname=result.countryName;
});
}
and these all functions set some value to $scope.trader whose value is being used in calling another function.
Now how to make one by one Asynchronous call to each of these functions so that these functions work one after another.
My full code is somewhat like this...
$scope.trader = {};
$scope.getOfferList = function() {
if($scope.trader.cityname == null || $scope.trader.cityname == '') {
$scope.getUserLocation();
}
$scope.searchPaymentMethod($scope.trader.cityname);
$scope.getUserCurrency();
}
$scope.getUserLocation = function() {
var dataPromise = locationServiceCustomised.getCurrentLocation();
dataPromise.then(function(result) {
$scope.trader.cityname=result.countryName;
});
}
$scope.searchPaymentMethod = function(country) {
locationService.paymentMethod({cityname: country}, function(data) {
------- Some Functionality-------
});
};
$scope.getOfferList();
If you have asynchronous functions that depend on values obtained from previous asynchronous functions, you need to chain promises with .then. If the functions can be called in parallel, then you need to use $q.all.
In your case, searchPaymentMethod is strictly dependent on value from getLocation, while getUserCurrency can be done in parallel.
$scope.getOfferList = function() {
var paymentMethodPromise =
$q.when($scope.trader.cityname)
.then(function(cachedLocation){
if (cachedLocation) return cachedLocation;
return getUserLocation();
})
.then(function(location){
// cache, if needed, but better do this in a service
$scope.trader.cityname = location;
return searchPaymentMethod(location);
});
var currencyPromise = getUserCurrency();
// when all complete, calculate offerList
return $q.all({
paymentMethod: paymentMethodPromise,
currency: currencyPromise })
.then(function(data){
var paymentMethod = data.paymentMethod,
currency = data.currency;
// based on that get offerList
return offersList;
})
};
Needless to say, that for this to work, the expectation is for all these functions (i.e. searchPaymentMethod, getUserLocation, etc...) return a promise of the right value. As an example, getUserLocation doesn't do that now, and should be changed to something like the following:
function getUserLocation(){
return locationServiceCustomised
.getCurrentLocation()
.then(function(location){
// manipulate the result if needed
return location.cityName;
})
}
A few other things to note:
You don't need to expose every function on the $scope - expose only the ones that need to be invoked from the View, e.g. <button ng-click="getOfferList()">
It's better to leave the cached-or-not decision to the service (that should implement these functions) and leave a clean API for the controller.
You basically need to chain promises. So, first of all, all your functions have to return a promise. So, for your first function you would have:
$scope.getUserLocation = function() {
var deferred = $q.defer();
locationServiceCustomised.getCurrentLocation.then(function(result) {
deferred.resolve(result);
});
return deferred;
}
Then just chain it:
$scope.getUserLocation()
.then($scope.searchPaymentMethod)
.then($scope.getUserCurrency);
where
$scope.searchPaymentMethod = function(input) {
var deferred = $q.defer();
$scope.trader.cityname = input.cityname;
// do some computationn
deferred.resolve(whateveryouwanttoreturn);
returnn deferred;
}

AngularJS promise and prevent redundant async calls

I have a wcf service method that gets some data and I call it using Microsoft Ajax library.
To share this data I create a dataService, and many controllers use this service.
I want every controller to get the same data after first call of getData is done, unless somebody need to refresh data and set forceRefresh to true.
My code fails because with the initialize of application 3 controler call dataService.getData and for all there start a new request. How can I make wait dataService.getData calls until the first one is finished and get same result for other subsequent ones..
angular.module('app', []).factory('dataService', function ($q) {
var data= [];
var getData= function (forceRefresh) {
console.log('getFolders called: ', reports.length);
var deferred = $q.defer();
if (forceRefresh || data.length < 1) {
WcfService.GetData(function(result) {
data= result;
deferred.resolve(data);
}, function(ex) { console.log(ex); });
} else {
deferred.resolve(reports);
}
return deferred.promise;
};
return {
getData: getData
};
});
One way would be to cache the promise, rather than the data, so it gets cached when it is created and not when the data arrives. After all, this sounds like your use case.
angular.module('app', []).factory('dataService', function ($q) {
var deferred = null;
var getData= function (forceRefresh) {
console.log('getFolders called: ', reports.length);
if(!forceRefresh && deferred) return deferred.promise;
deferred = $q.defer();
WcfService.GetData(
function(result) { deferred.resolve(data); }, // I'd promisify at a
function(ex){ deferred.reject(ex); } // higher level probably
);
return deferred.promise;
};
return {
getData: getData
};
});
how about setting a global flag in the $rootscope when a controller is querying for data, which can be checked before fetching the data by all the controllers, hence, avoiding redundant calls. The same flag can be put down when any of the controller has the promise fulfilled and data has been fetched, which can then be shared amongst all the controllers.
I found exactly what I search for
Promise queue for AngularJS services
http://inspectorit.com/tips-tricks/promise-queue-for-angularjs-services/

Resources