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;
});
Related
I'm using Todd Mottos and John Papas Styleguide and am quite familiar with both. Now I'm trying to create a data service with a function which needs to do two nested REST-calls, the second depends on the first. what would be the best and cleanest way to achieve this?
I did write three functions:
[...]
return {
get: get
};
var benutzerkennung = {};
// API function
function get(referenzID, versorgungsfallIdent) {
return getBenutzerkennung()
.then(function() {
return getMasterData(referenzID, versorgungsfallIdent);
})
.catch(requestFailed);
}
function getBenutzerkennung() {
return commonQueryService
.requestBenutzerkennung()
.then(function(response) {
benutzerkennung = response.data.reference;
});
}
function getMasterData(referenzID, versorgungsfallIdent) {
// "data" is just a simple js-object filled with the three params, two coming
// from the controller calling .save() and one coming from the first request
var data = getFilledDataObject(benutzerkennung, referenzID, versorgungsfallIdent);
return $http
.post('./services/anStammdatenService/get/getANStammdaten', data)
.then(function (response) {
return response.data;
});
}
so you see that the Service-Method to be called from the controller is
function save()
and it does the first request wrapped in the function "getBenutzerkennung()" which is required for the next request, wrapped in "getMasterData()". Is that a good codestyle?
Would be much appreciated!
Avoid mutating variables outside a .then method when chaining promises.
BAD
var outside;
function getA (params){
return serviceA(params).then(function(response) {
outside = response.data;
});
}
function getB (params){
return getA(params).then(function() {
return serviceB(params,outside)
});
}
The code risks closure problems. If the getB function is called multiple times before the previous chained XHRs complete, the outside variable may not be set properly.
Chain data properly
function getA (params){
return serviceA(params).then(function(response) {
return response.data;
});
}
function getB (params){
return getA(params).then(function(dataA) {
return serviceB(params,dataA);
});
}
I'm not familiar with the style guides you mentioned but in general I would tend to do something like this:
const get = () => commonQueryService
.requestBenutzerkennung()
.then(function(response) {
return response.data.reference;
})
.then(function(benutzerkennung){
var data = getFilledDataObject(benutzerkennung, referenzID, versorgungsfallIdent);
return $http.post('./services/anStammdatenService/get/getANStammdaten', data)
})
.then(function (response) {
return response.data;
})
If you like you could extract the functions you pass to the .then()s so you'll get a nice clean promise pipeline at the end. Something like this
const getData = function(response) {
return response.data.reference;
}
const postData = function(benutzerkennung){
var data = getFilledDataObject(benutzerkennung, referenzID, versorgungsfallIdent);
return $http.post('./services/anStammdatenService/get/getANStammdaten', data)
}
const sendResponse = function (response) {
return response.data;
}
const get = () => commonQueryService
.requestBenutzerkennung()
.then(getData)
.then(postData)
.then(sendResponse)
You may offcourse use more declarative names for your functions.
I've read many answers to this question but I just don't get it. Where does the promise go? I made a simple factory with an async call to a cloud database:
app.factory('asyncFactory', function() {
let toController = function() {
firebase.database().ref('en').once('value') // get the array from the cloud database
.then(function(snapshot) { // take a snapshot
console.log(snapshot.val()); // read the values from the snapshot
return snapshot.val(); // this returns later
});
return 47 // this returns immeadiately
};
return {
toController: toController // why is this necessary?
}
});
I call it from my controller:
$scope.words = asyncFactory.toController();
console.log($scope.words);
Here's the response:
As you can see, 47 returns to the controller immediately. If I comment out return 47 then the factory returns undefined. Later the async data logs but doesn't return to the controller. I use promises every day but I can't figure out where the promise goes.
Second question: do I need the line toController: toController ? Can I get rid of it?
Thanks!
To use the results from the firebase call in the controller, the factory method needs to return a promise:
app.factory('asyncFactory', function($q) {
return {
toController: toController
};
function toController() {
var es6promise = firebase.database().ref('en').once('value');
var qPromise = $q.when(es6promise)
.then(function(snapshot) { // take a snapshot
console.log(snapshot.val()); // read the values from the snapshot
return snapshot.val(); // this returns later
});
return qPromise;
};
});
Because the firebase .once method returns an ES6 promise, that promise needs to be brought into the AngularJS framework by converting it to a $q Service promise with $q.when. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc.
In the controller, use the .then method to extract the data after it returns from the server:
var qPromise = asyncFactory.toController();
qPromise.then(function(data) {
console.log(data)
$scope.words = data;
});
The factory function immediately returns a promise. When the data arrives from the server, the data will be placed on $scope.
Well toController is eating the promise for itself. ( whenever you call .then(), it means you are waiting for promise),
Try this
app.factory('asyncFactory', function() {
let toController = function() {
var deferred = $q.defer();
firebase.database().ref('en').once('value') // get the array from the cloud database
.then(function(snapshot) { // take a snapshot
console.log(snapshot.val()); // read the values from the snapshot
return deferred.resolve(snapshot.val()); // this returns later
});
//return deferred.resolve(47) // this returns immeadiately
};
return {
toController: toController // why is this necessary?
}
});
If you don't want this line
return {
toController: toController // why is this necessary? }
app.factory('asyncFactory', function() {
return {
var deferred = $q.defer();
firebase.database().ref('en').once('value') // get the array from the cloud database
.then(function(snapshot) { // take a snapshot
console.log(snapshot.val()); // read the values from the snapshot
return deferred.resolve(snapshot.val()); // this returns later
});
//return deferred.resolve(47) // this returns immeadiately
};
})
I have a problem with outputting the values of a promise function.
$scope.getTeacher = function(teacherAbr) {
var promise = dataService.getTeacher(teacherAbr);
var testje = promise.then(function(payload) {
return payload;
});
return testje; // outputs an d object, not the direct payload values
};
The output of this controller function is this:
However, when I'm doing testje.$$state it returns undefined. How can I output the value object? I can't output the payload variable in a new $scope, because this data is dynamic.
Here is a simplified version of this on Plunker.
You should change the way you think about things when you work with asynchronous code. You no longer return values, instead you use Promise or callback patterns to invoke code when data becomes available.
In your case your factory can be:
.factory('dataService', function($http, $log, $q) {
return {
getTeacher: function(teacher) {
// Originally this was a jsonp request ('/teacher/' + teacher)
return $http.get('http://echo.jsontest.com/key/value/one/two').then(function(response) {
return response.data;
}, function() {
$log.error(msg, code);
})
}
};
});
And usage in controller:
dataService.getTeacher('Lanc').then(function(data) {
$scope.teacher = data;
});
Also $http.get already returns promise, so you don't have to create one more with $q.defer().
Demo: http://plnkr.co/edit/FNYmZg9NJR7GpjgKkWd6?p=preview
UPD
Here is another effort for combining lessons with teachers.
Demo: http://plnkr.co/edit/SXT5QaiZuiQGZe2Z6pb4?p=preview
//in your services file
return {
getTeacher: function (teacher) {
// Originally this was a jsonp request ('/teacher/' + teacher)
return $http.get('http://echo.jsontest.com/key/value/one/two')
})
//change the controller to this
dataService.getTeacher(teacherAbr).then(function(msg){
$scope.getdata=msg.data;
console.log("From server"+msg.data);
});
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()
}
I'd like my objects to cache the result of some network requests and answer the cached value instead of doing a new request. This answer here done using angular promises looks a lot like what I'm going for, but I'm not sure how to express it using the Parse.com promise library. Here's what I'm trying...
module.factory('CustomObject', function() {
var CustomObject = Parse.Object.extend("CustomObject", {
cachedValue: null,
getValue: function() {
if (this.cachedValue) return Parse.Promise.as(this.cachedValue);
return this.functionReturningPromise().then(function (theValue) {
this.cachedValue = theValue;
return this.cachedValue;
});
},
My idea is to return a promise whether or not the value is cached. In the case where the value is cached, that promise is resolved right away. The problem is, as I follow this in the debugger, I don't seem to get the cached result on the second call.
Your value is almost correct. Your design is correct the only issue you have here is dynamic this.
In the context of the .then handler, this is set to undefined (or the window object), however - since you're using Parse promises and I'm not sure those are Promises/A+ compliant it can be arbitrary things - the HTTP request, or whatever. In strict code and a good promise library - that would have been an exception.
Instead, you can do CustomObject.cachedValue explicitly instead of using this:
var CustomObject = Parse.Object.extend("CustomObject", {
cachedValue: null,
getValue: function() {
if (CustomObject.cachedValue) return Parse.Promise.as(this.cachedValue);
return this.functionReturningPromise().then(function (theValue) {
CustomObject.cachedValue = theValue;
return this.cachedValue;
});
},
If $q promises are also possible instead of Parse promises, I'd use those instead:
var cachedValue = null;
getValue: function() {
return $q.when(cachedValue || this.functionReturningPromise()).then(function(theValue){
return cachedValue = theValue;
});
}
You can just cache the promise and return that
module.factory('CustomObject', function() {
var CustomObject = Parse.Object.extend("CustomObject", {
cachedPromise: null,
getValue: function() {
if (!this.cachedPromise) {
this.cachedPromise = this.functionReturningPromise();
}
return this.cachedPromise;
},
...
}
...
}
I am not familiar with the Parse.com promise library, but it could be a plain JS error:
The this inside the function is not referring to the Promise object, but to the global object.
Change the code like that:
...
getValue: function() {
if (this.cachedValue) return Parse.Promise.as(this.cachedValue);
var that = this;
return this.functionReturningPromise().then(function (theValue) {
that.cachedValue = theValue;
return that.cachedValue;
});
},