AngularJS only fetch data if undefined - javascript

I'm fetching some data with $http in a controller to show the user some data. But after this is done I don't want to do anymore fetches because the data is already fetched. However when going from one tab (controller) to another tab (controller) data is always fetched. My knowledge is limited to web and AngularJS. I thought below code would work but data is always fetched.
// We already have data don't bother to get it again
if (angular.isDefined($scope.data)) {
console.log("Already got data, no fetch");
return;
} else {
console.log("Fetch data first time");
}
$http.post('/api/data'......

Most likely (although, there isn't enough code in your example to be certain) your tab controllers have different scopes, so $scope.data is actually not defined for the second tab (controller).
You could certainly put the data on $rootScope but I would recommend against that, as I would against a global variable.
Also, you'd have a race condition here because you might switch tabs before the data arrived and that would trigger a second request.
A good way to solve this is with a service. The service can cache the promise and return that to the next caller.
.factory("fooSvc", function($http){
var promise;
return {
getData: function(){
if (promise) return promise;
promise = $http.get("/some/url").then(function(response){
// optionally post-process the response
return response.data;
});
return promise;
}
}
})
Then, in the controller you could just get the data and not worry about duplicate calls:
.controller("TabCtrl1", function($scope, fooSvc){
fooSvc.getData().then(function(data){
$scope.data = data;
})
}

Related

Return promise from Angular service

I have a service that (when it's all said and done) updates a value on the database.
I would like to update the view scope based on the result (success/fail) but of course the http request used by the service is asynchronous so the return value is not immediately available and comes up undefined in the controller.
If I were making the http request inside the controller, I would update the scope inside the callback function, but because Im using a service, the scope's umm... scope(?) is not available to it.
Im thinking a Promise is what I need to be returning but perhaps there is something more simple.
SERVICE
.service('doStuff',function($http){
this.update = function(data) {
$http.post('http://api.internet', data).then(function(res){
return(res.data.result);
});
}
})
CONTROLLLER
/* service is injected into controller etc. */
var result = doStuff.update(data);
p(result); // undefined (as expected)
I figured since Im returning from the http callback, it would wait for the result to be available before returning but I guess Im missing something.
Since $http is always async, you cannot return anything in the call back function. It is as good as not returning anything.
What you need to do is you need to return the $http promise, and then handle the callback functions in your controller.
Service:
.service('doStuff', function($http) {
this.update = function(data) {
return $http.post('http://api.internet', data);
}
})
Controller:
doStuff.update(data).then(function(result){
p(result);
});
Foremost, you need to return the query itself. Looks like
this.update = function(data) {
return $http.post('http://api.internet', data).then(function(res){
return(res.data.result);
});
}
Next step, you need get out of the promise function.
doStuff.update(data)
.then(function(res) {
//someone if request is success
})
.catch(function(rej) {
//someone if request is reject
});

Calling $http promise inside controller

Thanks in advance for the help...
I have a controller I'm using to call an API to send a password reset link. I have abstracted the actual $http call off into a service to keep the controller thin. Originally I was doing something like this:
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
$http(request).then(function() {
return {}; //return some object using response
}]);
I felt like this would be the best approach as it would keep the controller as thin as possible and kept all service related actions separate. My problem with this was that returning from within the promise never actually returned me anything. I had to actually return the $http service call to get the promise. The only way I was able to make all of this work was if I called .then from within the controller.
//controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.forgotPassword(emailAddress).then(function() {
//do stuff
}
}]);
//service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http){
this.forgotPassword = function(emailAddress) {
return $http(request);
};
}]);
This just feels a little wrong to me because the controller now depends on receiving a promise back from the service. I may just be overthinking this but I would like a second opinion.
Is this considered acceptable/good practice? Is there an alternative to this which would allow me to achieve the encapsulation I'm looking for?
Thanks again.
I've interfaced with the $http from the controller in two slightly different ways.
Like you it felt wrong returning the $http from the service and interfacing with it.
So first I created services and passed in a success method and an error method (callbacks).
// Service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
function sendForgotPasswordEmail(emailAddress, success, error){
$http.post('/api/v1/resetpassword', {emailAddress:emailAddress})
.then(success, error);
}
return {
sendForgotPasswordEmail: sendForgotPasswordEmail
}
}]);
// Controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.sendForgotPasswordEmail(emailAddress,
function(response){ //success
// notify user of success
},
function(response){ // error
// notify user of error
});
}]);
This worked great. I created an large application this way, but as I started on my second large angular project I wondered why I was hiding the $http's promise?
By passing back the promise, I can use other libraries that support promises. With my first approach I can't leverage other libraries promise support.
Passing back the $http promise
// Service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
function sendForgotPasswordEmail(emailAddress){
return $http.post('/api/v1/resetpassword', {emailAddress:emailAddress});
}
return {
sendForgotPasswordEmail: sendForgotPasswordEmail
}
}]);
// Controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.sendForgotPasswordEmail(emailAddress)
.then(
function(response){ //success
// notify user of success
},
function(response){ // error
// notify user of error
});
}]);
I deleted my original answer, and I feel like a dork for stating that you could do it the other way. When I went back and checked my original code back when I first started angular, I found that I was calling then() twice in my application - once in my service where I returned the data, and once in my controller because calling $http(request).then() returns a promise.
The fact is, you're dealing with an asynchronous call. Suppose in your controller, you wanted to do this:
$scope.foo = myService.getFoo(); // No then()
The XmlHttpRequest inside the $http in getFoo() is an asynchronous call, meaning that it calls it and moves on. The synchronous option is deprecated. It's bad practice to make a blocking synchronous HTTP call because it will seize up your UI. This means you should use a callback when the data is ready, and the promise API is made just for that.
If you absolutely do not want to use the then() in your controller, I suppose you could probably pass your scope binding parameters to your service and let your service update them in your then call. I haven't tried this, and because it's asynchronous, I'm not sure if angular will know to call a digest() so you may need to call a $scope.$apply() if the values don't update. I don't like this, because I think the control of the values in the scope should be handled by the controller and not the service, but again - it's your personal preference.
Sorry for leading you astray with my initial answer - I ran into the same question you had, but when I looked back - I saw I used a silly solution.
-- Relevant statements in original answer --
Consider the following:
Where do you want your error handling for the call and who needs to know about it?
Do you need to handle specific failures in a particular controller or can they all be grouped together to one error handler? For some apps, I like to display the errors in a particular place rather than in a general modal dialog, but it's acceptable to create a service to handle all errors and pop them up for the user.
Where do you want to handle your progress/busy indicator?
If you have an interceptor wired up for all http calls and broadcasting an event to show/hide the busy indicator, then you don't need to worry about handling the promise in the controller. However some directives will use the promise to show a busy indicator which requires you to bind it to the scope in the controller.
To me, the decision is determined by the requirements and by personal choice.
Try using a callback like so:
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
var recovery = function(request, callback) {
$http(request).then(function(response) {
callback(response);
})
}
return { recovery: recovery }
}]);
Then you would call it like this:
forgotPasswordService.recovery('http://...', function(response){
console.log(response);
})

Most efficient way to receive json async responses?

(my case applies to C#, MVC, returning JSON, got jquery, angular), but I expect it applies to more than that.
I have a website where my angular/html/js calls ~7 services through Angular controllers and async-gets/displays data (weather, road conditions, etc). Some of these take longer than others (from ms to ~10s). I'd like to have a single call to my service which returns all of this data - but doesn't wait until the last call to return anything (10s).
Is there a way to make a single call, and return results as I have them and they get displayed accordingly? Do I need to have a repeating call which has a boolean like "IsMore=T" and calls the service again? (doesn't sound efficient).
Ideally, I'd like to keep a response channel open and keeping pumping results until it's done. Possible?
I'm not sure I understand completely, but I think you could just chain the response promises together, something like:
$scope.getWeatherData = function () {
return myWeatherService.get().then(function (resp) {
$scope.weatherData = resp;
});
}
$scope.getTrafficData = function () {
return myTrafficService.get().then(function (resp) {
$scope.trafficData = resp;
});
}
$scope.getWeatherData.then(getTrafficData).then(...chain others...);
This assumes that the service calls return a $http promise.
Anyway, whenever the promise comes in, the data will be on $scope, and hence the display will be updating as the promises arrive. It sounds like you might be using something like $q.all(), which would wait until all promises are resolved.
Buildilng on #reptilicus' answer:
$scope.getWeatherData = function () {
return myWeatherService.get().then(function (resp) {
$scope.weatherData = resp;
});
};
$scope.getTrafficData = function () {
return myTrafficService.get().then(function (resp) {
$scope.trafficData = resp;
});
};
$q.all([
$scope.getWeatherData(),
$scope.getTrafficData()
]).then(function () {
// do whatever's next, or nothing
});
... would request/receive both responses in parallel (if that's what you want). The relevant $scope property for each request will be populated when the related response is received, and the "do whatever's next" code will run once they are all complete.
Note, you need to inject $q to your controller constructor for this to work. :)
Edit: I just noticed that #reptilicus did mention $q.all. The difference between chaining the .thens and $q.all is that under chaining, one request wouldn't start until the previous was received....

One time async data load in angular factory - convention ?

I'm writing a simple app for displaying (read-only) employee information. I would like to load the info from JSON once only. Not sure what the convention is around this in the angular factory.
I know that one solution is to but the JSON file in a javascript file and load it as a js file (but I would want to keep the file as JSON).
I guess I could also wrap the http call in a promise, and change the return accordingly.
Is there a way of doing this without changing the return? Block on the employee load ?
.factory('Employees', ['$http', function($http) {
var employees = $http.get('res/employees.json').then(function(response){
return response.data; // This is async so won't return right away
});
// This way works (since not async)
// var employees = [
// {
// "id": 232,
// "name": "Bob"
// }];
return {
all: function() {
return employees; // This will return empty before employees is loaded
}
}
}]);
This is a wrong implementation of the promise pattern. Your 'employee' service should return a promise also that gets initialized and then returns the same resolved promise upon subsequent requests. Something like this:
.factory('Employees', ['$q', '$http', function($q, $http) {
var _deferred = $q.defer();
$http.get('res/employees.json')
.success(function(data, status, headers, config) {
// this callback will be called asynchronously
// when the response is available
_deferred.resolve(data);
})
.error(function(data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
_deferred.reject("Error");
});
};
return {
getEmployees: function(){
return _deferred.promise;
}
}
}]);
.controller('MyController', ['$scope', 'Employees', function($scope, Employees) {
$scope.employees = [];
$scope.employees = Employees.getEmployees();
}]);
$scope.employees will initially be an empty array until the promise is resolved. Also, this code does not have error recovery.
One possible solution that might work for you is to fetch the data and manually bootstrap your application with an appended value or service of the fetched data. There are already built solutions for this kind of problem, one is called the angular-deferred-bootstrap and another is a solution I made just a month ago. Both are making use of the AngularJS lifecycle in manually bootstrapping the application, using angular.bootstrap(). Note that when you are manually bootstrapping your application you need to remove the ng-app directive.

don't want to expose $http in the controller

There is related question about processing $http in a service, but I wanted to elaborate slightly. I would like my controller to be able to execute service calls using API similar to the Angular $http API:
$scope.login = function(user) {
securityService.login(user).success(function(data) {
$scope.data = data;
}).error(function(data) {
$scope.error = data;
});
};
That's a nice readable API. On the surface, all I would need to do in the service API is:
return {
name : 'User Service',
login : function(user) {
return $http.post("/api/login", user);
}
};
Great, it returns the promise and the success and error messages come with. But... What if I want to deal with the success and failure cases in the service? I want to maintain the nice, readable service API. In this case, maybe I'd want to preserve the user so that I could expose methods like securityService.currentUser() or `securityService.isLoggedIn()'.
I tried the $http.post().then(...) promise API, but those return the entire HTTP response. Again, I want to isolate HTTP to the services and maintain a similar concrete callback API.
You could make your own promise in login using angular's $q:
var deferred = $q.defer(),
promise = deferred.promise;
$http.post("/api/login", user).success(...).error(...)
return promise;
Within the success/failure of the $http promise in your service, resolve/reject your deferred object.
Edit:
Expanding upon the $http.post:
$http.post("/api/login", user).success(function(data) {
if (data == "foo") { // success case?
deferred.resolve('you logged in!');
} else {
deferred.reject('something really bad happened!');
}
})
I just ran a test case and it seems that if I return the promise from the service API like this:
return $http.post("/api/login", user).success(...).error(...)
I can also do the same in the controller:
service.login(user).success(...).error(...)
and both get called, the service first and the controller second. That's perfect!

Categories