This question already has answers here:
How can I access a variable outside a promise `.then` method?
(2 answers)
Closed 5 years ago.
I have a service in AngularJs which will return a value from Database.
userData.getUserData(function(response) {
$scope.uid = response
});
when I inject this service in my controller it will return a Promise,
But i need this Promise value Outside of my Function, how can i do that ?
Plunkr link of code
From your plunker code you have a service which looks like this:
angular.module('plunker');
.service('myService', function($firebaseRef, $firebaseObject){
this.getUserData = function(el) {
$firebaseObject($firebaseRef.users.child(this.localStorage().uid)).$loaded(function(data) {
el(data);
})
}
});
and a controller like this:
app.controller('MainCtrl', function($scope, myService) {
myService.getUserData(function(response) {
$scope.uid = response;
})
console.log($scope.uid);
$scope.postRequest = function(val) {
$firebaseArray($firebaseRef.requests.child($scope.uid)).$add(val);
console.log(val)
console.log($scope.request);
}
});
The problem is that the line console.log($scope.uid); prints undefined.
You are thinking in the terms of a standard blocking programming, but in this case the call to getUserData is non-blocking which means that you don't wait for the response, instead you only send the request to the server (Firebase) and continue to the next statement which is console.log.
The callback function(response) { $scope.uid = response; } will be invoked when the client reads success response (HTTP 2xx) returned by the server. This takes at least the time request travels to the server and response to travel back + the time it takes for the server to actually get the data. For example 150ms.
So, basically at the time the console.log statement is executed, the response callback was still not invoked, ie. the $scope.uid is not set which means that the console.log will print undefined.
To resolve this you need to execute your code, which depends on the response from the server, in the callback itself. For example something like this:
app.controller('MainCtrl', function($scope, myService) {
myService.getUserData(function(response) {
$scope.uid = response;
console.log($scope.uid);
// and any other code which depends on the $scope.uid
});
// ...
});
The cool factor would be to use AngularJS promises via $q service. For example, you could redefine your service like this:
angular.module('plunker');
.service('myService', function($q, $firebaseRef, $firebaseObject){
var deferred = $q.defer();
this.getUserData = function(el) {
$firebaseObject($firebaseRef.users.child(this.localStorage().uid)).$loaded(function(data) {
deferred.resolve(data);
});
};
return deferred.promise;
});
then in your controller you can use your service method like this:
app.controller('MainCtrl', function($scope, myService) {
myService.getUserData()
.then(function(data) {
$scope.uid = data;
console.log($scope.uid);
// and any other code
// you can also return promises here and then chain
// them, read about AngularJS promises
});
// ...
});
This is basically same as the example before, but with added benefit of better readability which is accomplished by avoiding callback hell.
I noticed that you have postRequest function which uses $scope.uid. I guess that you do not want to execute this function if you do not have $scope.uid. I also guess that this function is called by some event, like click on a button. My recommendation is that you disable the button or whatever else invokes this function until the $scope.uid is loaded.
For example like this:
<button type="button" ng-click="postRequest(something)" ng-disabled="uid === undefined">Post</button>
Hope this helps.
You issue that has been discussed has to do with the fact that you are trying to use $scope.uid before your promise has returned anything.
You can get around things like this by taking a few steps, mainly, you can init the scope var before you use. For instance if the response is an object you could just do something like this:
$scope.uid = {};
userData.getUserData(function(response) {
$scope.uid = response;
});
Then your var wont be undefined. But you should also consider when and how you are using this variable, that will effect if you want to init like this or not.
If you log like this, it will work
userData.getUserData(function(response) {
$scope.uid = response;
console.log($scope.uid);
});
and if you log like this it will not work because this log is not going to wait for you promise to return before logging;
userData.getUserData(function(response) {
$scope.uid = response;
});
console.log($scope.uid);
You'd need to provide more information to determine how best to deal with using this returned information and local variable. But the general idea of the problem is that you are attempting to log the variable before the promise is back.
TL:DR You have access to $scope.uid outside of the function, you need to wait for the reponse to give it the data before it will be inside, you can init it if you do not want it to start out as undefined
UPDATE : you need to use a callback to fire the second call After you have the first call back
userData.getUserData(function(response) {
$scope.postRequest(response);
});
$scope.postRequest = function(val) {
$firebaseArray($firebaseRef.requests.child($scope.uid)).$add(val);
console.log(val) console.log($scope.request);
}
Your plunk fixed : https://plnkr.co/edit/KbVoni3jfnHm54M80kYl?p=preview
You have to wait until the process of getting the response from userData.getUserData is done.
There are 3 ways as far as I know to solve this:
Using Callback
function getUserData(callback){
userData.getUserData(function(response) {
callback(response);
});
}
then you call that function
getUserData(function(response){
$scope.uid = response;
// then you can proceed and use the $scope.uid here
});
Wrap it in function
getUserData(function(response){
callAnotherFunction(response);
});
function callAnotherFunction(response){
console.log(response);
// You can use the value inside this function
}
Or use timeout
You can use $timeout to give time to the request and assign it to $scope.uid
Related
Constants.getContants is a promise which will get all the constants that are used in my application. I would like to save this to a $scope variable so that I can access it anywhere in the controller or application. Now, whenever I need to access it, I need to repeat the call and do the operation there itself.
Even If I try to save it in the $scope it will not be available outside the corresponding handler. How to solve this issue.
Following is the code that I'm using:
Constants.getConstants().then(function (AppContants) {
$scope.responseCount = AppContants.data.serverUrl+AppContants.data.appId
console.log($scope.responseCount);
//$scope.$apply();
});
console.log($scope.responseCount);
Here AJAX call is going out of sync also. I know that actions need to performed inside the handler function so that we can be sure that the intended action is executed only after a successful AJAX call. I need to use these variables outside the function. I tried $scope.$apply() operation as well. It didn't help. Is there a way to solve this? Thanks in advance.
Constants.getConstants().then(function(response)
{
$scope.responseCount = response.data;
}, function(error)
{
console.log(error);
});
And in your service you should have something like
this.getConstants= function($username){
var endpoint = "url";
return $http({
method: 'get',
url: endpoint
});
};
In your case the 2nd Console.Log executes just after placing the AJAX call. It does not wait for the AJAX to respond as it is a asynchronous call.
You can only be able to use '$scope.responseCount' property after the AJAX cal is resolved.
As a workaround you can:
Place this call to fetch constants at the time of application startup and save the constants in some service (shared service).
Do your operation in the 'then' block of this AJAX call.
Here's the thing. When you are Constants.getConstants() it return response as promise. since javascript asynchronous it does not wait until response return. It just keep on executing. That's why the console outside the then function display undefined.
workaround is you can add a function inside promise and put you operations inside that function
Constants.getConstants().then(function(AppContants) {
$scope.responseCount = AppContants.data.serverUrl + AppContants.data.appId
console.log($scope.responseCount);
sampleFunc()
});
function sampleFunc() {
// do your oprations here
console.log($scope.responseCount);
}
You can cache the promise in a service:
app.service("ConstantCache", function(Constants) {
var promiseCache;
this.getPromise = function() {
if ( promiseCache } {
return promiseCache;
} else {
promiseCache = Constants.getConstants();
return promiseCache;
};
};
this.trashCache = function() {
promiseCache = null;
};
});
Then the cached promise can be used in any controller as many times as desired:
ConstantCache.getPromise().then(function(AppContants) {
$scope.responseCount = AppContants.data.serverUrl + AppContants.data.appId
console.log($scope.responseCount);
sampleFunc()
});
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.
I'm building a ticket admin table, and I have some issues trying to export a variable outside the ajax function.
My code:
app.controller('bodyController',function($scope,$http,$sce){
$scope.ticketList = [];
$http.get("tickets.php")
.then(function(response) {
$scope.ticketModify = response.data;
console.log($scope.ticketModify); //this one return the correct data.
return $scope.ticketModify;
});
console.log($scope.ticketModify); //this return undefine
Same result with factory if I try to return the response.data into any variable
Just because code comes physically above other code doesn't mean that it gets executed from top to bottom. Think about your program like this:
app.controller('bodyController',function($scope,$http,$sce){
$scope.ticketList = [];
$http.get("tickets.php")
.then(handleResponse);
console.log($scope.ticketModify); //this return undefine
function handleResponse(response) {
$scope.ticketModify = response.data;
console.log($scope.ticketModify); //this one return the correct data.
return $scope.ticketModify;
}
Do you see now why $scope.ticketModify is still undefined? The position of the code does not matter, what matters is the time at which it is executed. You should chain another then on to the then you have there if you want to do more with the newly modified $scope.ticketModify. Or, call another function and pass $scope.ticketModify from your current then. You can do anything!
What executes in the .then is executed when the server responded.
But just after the ´$http.get´Promise, the next code will be executed.
You should look into async programming and Promises. https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Promise
app.controller('bodyController', function($scope, $http, $sce) {
$scope.ticketList = [];
$scope.ticketModify = "";
$http.get("tickets.php")
.then(function(response) {
$scope.ticketModify = response.data;
console.log($scope.ticketModify);
return $scope.ticketModify;
});
console.log($scope.ticketModify);
// This should print a empty String
}
the place where it is working alright for you, is after the call to tickets.php is returned.
The place where it is not working is due to the fact that the ticketModify is not defined by the time you are accessing. You called the tickets.php as an async call and immediately trying to access the variable which is being populated after the promise is resolved.
So, all you could do is to declare ticketModify on scope before the http call to tickets.php and update the value on success / then callback on tickets.php
The value for myIdentity is created only once and is persistant saved in the local storage after first usage of the following Angular.js controller.
The function $scope.createIdentity() is a complex function in a more global controller and saves the result to $localstorage.myIdentity.
This perfectly works!!! the createIdentity() function works with Q promise inside for itself.
But in the AppCtrl I have an issue with race conditions because $localstorage.myIdentity is not yet resolved before
my $http XHR Request is fired. So it doesn’t contain any value for myId.
This occurs only the first time the controller is used.
But I need to start the socket.io connection at the first call of the AppCtrl and this is here an bigger problem for me.
At a second usage of AppCtrl the correct value for myIdentity is yet available in $localStorage.
TimeacleControllers.controller('StatusCtrl', ['$scope', '$http', 'Socket', '$localStorage',
function ($scope, $http, Socket, $localStorage) {
if ($localStorage.myIdentity === undefined) {
$scope.createIdentity();
}
var myParams = {
myId: $localStorage.myIdentity
};
$http
.post('http://example.org', myParams)
.success(function (data) {
console.log('received data: ' + data);
Socket.connect();
Socket.on('connected', function () {
console.log("Connection!");
});
Socket.on('message', function (msg) {
console.log("Message: " + msg);
});
})
.error(function (err) {
// Handle login errors here
console.log("Error - " + err);
});
}]);
So what can you do here to make the Ajax Request wait until the $localstorage.myIdentity can be resolved? Please help.
You mentioned about Q promise but having problem with async sequence...?
If createIdentity is returning a promise, just put the http call inside .then
$scope.createIdentity().then(function() {
$http.... // or you can wrap it inside a function
})
Edit: If you have no access to the code that populate the variable (normally happens inside directive), you can setup a one time watch to monitor the value change.
var unwatch = $scope.$watch(function(){
return $localStorage.myIdentity;
}, function(newValue){
if (newValue === undefined) return;
$http...
unwatch();
});
You said $scope.createIdentity() uses a promise (and presumably is an async operation). Make it return a promise for your code to observe. When it resolves, make it resolve the identity.
Promises are chainable. When you return a promise from then, the next then listens for that promise instead of the original promise. In this case, we make the next then listen to the AJAX.
$scope.createIdentity().then(function(identity){
// When createIdentity resolves, use the data for the AJAX.
// Return the AJAX promise for the next chained then.
return $http.get(...);
}).then(function(data){
// When the AJAX completes, use the data for the socket call
Socket.connect();
...
});
//inside a service PService
this.getPTypes = function(){
var types = PTypesFactory.get({});
return types.$promise.then(function(result)
{
console.log(result.groups);
return result.groups;
});
}
//inside a controller
$scope.groups = PService.getPTypes();
console log shows correct fetched REST data, but when I do
console.log($scope.groups);
I get
Object {then: function, catch: function, finally: function}
which is promise API instead of the correct resolved data.
The problem is that you trying to use a asynchronous function like it was a synchronous one.
then is a method which returns a promise.
when invoking it with a callback that callback would not be invoked immediately, only when the response get back from the server.
You can write it like so:
Service
this.getPTypes = function(callback){
PTypesFactory.get({}).then(callback);
}
Controller
PService.getPTypes(function(res){
$scope.groups = res.data;
});
Promises are used to handle asynchronous operations. The function you pass to the then method is called at some indeterminable point in time. You can't return a value from within it to some other point in execution.
Instead of calling then in your service, just return the promise:
this.getPTypes = function(){
return PTypesFactory.get({}).$promise;
}
and handle its resolution in the controller:
$scope.groups = PService.getPTypes().then(function(result) {
console.log(result.groups);
});