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.
Related
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
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?
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 ;) )
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.
I am trying to unit test a service which has asynchronous methods but am having no luck.
I have tried to implement with promises by using the $q support in angularjs.
Any help would be appreciated.
http://jsfiddle.net/9pBze/37/
angular.module('myapp', ['myservice']);
angular.module('myservice', []).factory('myservice', function($q) {
var ls = {};
ls.DoIt = function() {
var deferred = $q.defer();
setTimeout(function(){
deferred.resolve(5);
},3000);
return deferred.promise;
}
return ls;
});
describe('services', function () {
beforeEach(module('myservice'));
it("should equal 2", inject(function(myservice) {
myservice.DoIt().then(function(returned) {
expect(returned).toEqual(2);
});
}));
});
First of all, the setTimeout is particularly tricky to test since it hard to mock. Fortunately AngularJS has a wrapper around it ($timeout) that plays the same role but can be easily mocked:
ls.DoIt = function() {
var deferred = $q.defer();
$timeout(function(){
deferred.resolve(5);
},3000);
return deferred.promise;
}
The mock provided for $timeout allows us to easily simulate elapsed time (with $timeout.flush()) which means our tests can run fast, without really waiting for the async event to complete (please note that the production code is still using async API!).
The changed tests would look like:
it("should equal 5", inject(function(myservice, $timeout) {
var valueToVerify;
myservice.DoIt().then(function(returned) {
valueToVerify = returned;
});
$timeout.flush();
expect(valueToVerify).toEqual(5);
}));
And finally the working jsFiddle: http://jsfiddle.net/v9L9G/1/
It's not related to Angular itself, but to Jasmine async tests.
If you need a setTimeout use Angular $timeout. And if you wish to have a fine control over setTimeout/$timeout executions, use mocked Clock.