How can I make an Angular service code "look synchronous"?
My questions arose when I cleaned my controller and put the business logic code into a service instead. So far so good. Now I would like to "wait" in the service function until all asynchronous calls have returned and then return. How can I do that?
To illustrate my problem, suppose you have a controller code which just:
requests some data from the backend
does some processing with the data and
hands the data over to the scope
Like that:
DataController before refactoring:
$scope.submitForm = function() {
RestBackend.query('something').then(function(data) {
// do some additional things ...
...
$scope.data = data;
});
};
Pretty straightforward. Fetch data and fill scope.
After refactoring into controller + service, I ended up with:
DataController refactored:
$scope.submitForm = function() {
DataService.getData().then(function(data) {
$scope.data = data;
});
};
DataService refactored:
this.query = function() {
var dataDefer = $q.defer();
RestBackend.query('something').then(function(data) {
// do some additional things ...
...
dataDefer.resolve(data);
});
return dataDefer.promise;
};
I dislike the fact that I have to work with a promise in the controller also. I like promises but I want to keep the controller agnostic of this "implementation detail" of the service. This is what I would like the controller code to look like:
DataController (as it should be):
$scope.submitForm = function() {
$scope.data = DataService.getData();
};
You get the point? In the controller I don't want to care about promise or not. Just wait for the data to be fetched and then use it. Thus, I am looking for a possibility to implement the service like this:
query the data (asynchronously)
do not return until the data has been fetched
return the fetched data
Now item 2. is not clear to me: How can I "wait until data has been fetched" and only proceed afterwards? The goal is that the service function looks synchronous.
I too think your solution is fine.
Returning a promise is not an implementation detail of the service. It is part of the service's API (the "contract" between the service and the service-consumer).
The controller expects a promise that resolves with the data and handles that as it sees fit.
How that promise is constructed, how the data is fetched etc, these are the implementation details.
You can swap the service at any time with one that does totally different things as long as it fulfills the contract (i.e. returns a promise that resolves with the data onve ready).
That said, if you only use the data in the view (i.e. do not directly manipulate it in the controller right after it is fetched), which seems to be the case, you can use ngResources approach:
Return an empty array and populate it with the data once it is fetched:
$scope.data = DataService.getData();
// DataService refactored:
this.getData = function () {
var data = [];
RestBackend.query('something').then(function(responseData) {
// do some additional things ...
...
angular.forEach(responseData, function (item) {
data.push(item);
});
});
return data;
};
BTW, in your current (fine) setup, you need $q.defer(). You can just use promise-chaining:
this.query = function() {
return RestBackend.query('something').then(function(data) {
// do some additional things ...
...
return data;
});
};
I think what you have is a very good solution. You should not have to wait for promise to be resolved, it defeats the purpose of async javascript. Just ask yourself why do you need to make it run sync?
If you rely in html on promise to be resolve you can do something like this
<div class="alert alert-warning text-center" data-ng-hide="!data.$resolved">
Got data from service.
</div>
As you use ngRoute, I would recommend you to resolve you data in your route config, and the view will be loaded once all your data will be resolved.
$routeProvider
.when('/your-url', {
templateUrl: 'path/to/your/template.html',
controller: 'YourCtrl',
// that's the point !
resolve: {
superAwesomeData: function (DataService) {
return DataService.getData();
}
}
});
Now, superAwesomeData can be injected in your controller and it will contains the data, resolved.
angular.module('youModule')
.controller('YourCtrl', function (superAwesomeData) {
// superAwesomeData === [...];
});
Related
I have been working with wrapping my head around the "angularjs" way of thinking (Angular 1) and I have a relatively ok grasp as I work my way through a small personal project. I am at a bit of a roadblock, not because I cannot get it to work, but I would like to know what the proper way to set up the data in my application.
The basic situation is this:
I have 3 json files:
categories.json
products.json
vendors.json
These hold the data (which I will fetch from a database later but am simplifying for now).
I basically need to load the data from all three of these files so that I can form a variable holding all "Products" (which is a JS class I declared separately).
I started off by storing the data inside one controller (relevant code below):
myApp.controller('productListController', ['$scope', '$http', '$q', function ($scope, $http, $q) {
var promises = [];
promises.push(getCategories($http));
promises.push(getVendors($http));
promises.push(getProducts($http));
$q.all(promises).then(function (response) {
//categories = response[0];
//vendors = response[1];
//products = response[2];
$scope.products = createProductList(response);
$scope.vendors = response[1].data;
$scope.vendorChecked = getCheckedVendors($scope.vendors);
})
This worked fine but I realized that I need this data in other views, which led me to try to move this code into a service.
The problem I had when doing this is that I do not know of a way for the controller to know that the service is done fetching the data so that I can then save it in the ProductListController $scope.
I would need to have a way to for example:
myApp.service('ProductService', ['$http', '$q', function ($http, $q) {
self = this;
var promises = [];
promises.push(getCategories($http));
promises.push(getVendors($http));
promises.push(getProducts($http));
$q.all(promises).then(function (response) {
//These three I would like to update in ProductListController
//when it is done.
self.products = createProductList(response);
self.vendors = response[1].data;
self.vendorChecked = getCheckedVendors(self.vendors);
})
Is this the correct approach to take? If so, how can I let the controller know that the service is done fetching the data and save for example:
$scope.products = ProductService.products;
$scope.vendors = ProductService.vendors;
$scope.categories = ProductService.categories;
Is this even the correct approach? Another approach I thought of was to use a factory instead of a service. Then I had another problem because I had for example:
return {
getProducts: function() {
//http get request code in here
return promise
},
getVendors: function() {
//http get request code in here
return promise
},
getCategories: function() {
//http get request code in here
return promise
},
getAllData: function () {
//in here I want to use the three promises in the 3 functions above
//but I am not able to call them from here. If I was able to do that
//then I could call this method from ProductListController and get the
//data that way.
}
I am sorry if this is long but I wanted to describe the different things I tried. I know I can make it work but I want to learn the right way, or a couple of right ways.
It is better to always return promise:
var promises = [];
promises.push(getCategories($http));
promises.push(getVendors($http));
promises.push(getProducts($http));
return $q.all(promises)
If you also not satisfied that in each controller you should call createProductList,getCheckedVendors - consider putting this tranforms to $http transformResponce https://docs.angularjs.org/api/ng/service/$http.
Or you can create your own promise. (Using $q.defer https://docs.angularjs.org/api/ng/service/$q).
Using servie or factory actually doesnt matter. This is factory:
var factory = {};
factory.getProducts: function() {
return promise
}
factory.getCategories: function() {
return promise
}
factory.getVendors: function() {
return promise
}
factory.getAllData: function () {
var promises = [];
promises.push(factory.getProducts());
promises.push(factory.getCategories());
promises.push(factory.getVendors());
return $q.all(promises)
}
return factory;
And in controler you just have:
MyFactory.getAllData().then(...)
I've been facing a trouble while working with Factory/Service. I've created an AjaxRequests factory for all of my AJAX calls. My factory code is
.factory('AjaxRequests', ['$http', function ($http) {
return {
getCampaignsData: function () {
var campaigns
return $http.get(url).then(function (response) {
campaigns = response.data;
return campaigns;
});
}
}
}])
I've created another service in which I am injecting this factory. My service code
.service('CampaignsService', ['$rootScope', 'AjaxRequests', function ($rootScope, AjaxRequests) {
this.init = function () {
this.camps;
AjaxRequests.getCampaignsData().then(function (response) {
this.camps = response.campaigns;
console.log(this.camps); // It is showing data
})
console.log(this.camps); // But it is not working :(
};
this.init();
}])
And in my controller
.controller('AdvanceSettingsController', ['$scope', 'CampaignsService', function ($scope, CampaignsService) {
$scope.CampaignsService = CampaignsService;
}
])
I've read this article to learn promises but it is not working here. I can directly achieve it in controller and it's been working fine. But it consider as a bad coding standard to make controller thick. But when I use service and factory I stuck. My question is why I am not getting ajax data to use in my whole service ? I need to use CampaignsService.camps in my view template as well as in my whole rest script but every time I get undefined. What is happening here? I've asked the same question before but couldn't get any success. Some one please help me to understand about promises and why I am getting this type of error if I'm working same ? This type of question has already been asked before but it was working in controller. May be I am stuck because I'm using it in a service.
A big thanks in advance.
This is not a bug or some tricky functionality. Just like in any other AJAX implementation, you can only access the response data in AngularJS's $http success method. That's because of the asynchronous nature of Asynchronous JavaScript And XML.
And what you have is working.
.controller('AdvanceSettingsController', ['$scope', 'AjaxRequests', function ($scope, AjaxRequests) {
$scope.camps = [];
AjaxRequests.getCampaignsData().then(function(data) {
$scope.camps = data;
});
}
])
And then bind camps:
<div ng-repeat="camp in camps>{{camp.name}}</div>
What's bad in your implementation is that instead of grouping related stuff in services you are writing a big AjaxRequests service for everything. You should have a CampaignsService that has a getData method and inject that in your controller.
Why is this working? Because $http does a $scope.$apply for you, which triggers a digest cycle after the data is loaded (then) and updates the HTML. So before the then callback that ng-repeat is run with [] and after it it's again run but with data from the response because you are setting $scope.camps = data;.
The reason <div ng-repeat="camp in CampaignsService.camps>{{camp.name}}</div> does not work is because of function variable scoping.
The this reference inside of your then callback is not the same as the this reference outside of it.
This will work and uses the common var self = this trick:
var self = this;
this.camps = [];
this.init = function () {
AjaxRequests.getCampaignsData().then(function (response) {
// here "this" is not the one from outside.
// "self" on the other hand is!
self.camps = response.campaigns;
});
};
I am using some data which is from a RESTful service in multiple pages.
So I am using angular factories for that. So, I required to get the data once from the server, and everytime I am getting the data with that defined service. Just like a global variables. Here is the sample:
var myApp = angular.module('myservices', []);
myApp.factory('myService', function($http) {
$http({method:"GET", url:"/my/url"}).success(function(result){
return result;
});
});
In my controller I am using this service as:
function myFunction($scope, myService) {
$scope.data = myService;
console.log("data.name"+$scope.data.name);
}
Its working fine for me as per my requirements.
But the problem here is, when I reloaded in my webpage the service will gets called again and requests for server. If in between some other function executes which is dependent on the "defined service", It's giving the error like "something" is undefined. So I want to wait in my script till the service is loaded. How can I do that? Is there anyway do that in angularjs?
You should use promises for async operations where you don't know when it will be completed. A promise "represents an operation that hasn't completed yet, but is expected in the future." (https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise)
An example implementation would be like:
myApp.factory('myService', function($http) {
var getData = function() {
// Angular $http() and then() both return promises themselves
return $http({method:"GET", url:"/my/url"}).then(function(result){
// What we return here is the data that will be accessible
// to us after the promise resolves
return result.data;
});
};
return { getData: getData };
});
function myFunction($scope, myService) {
var myDataPromise = myService.getData();
myDataPromise.then(function(result) {
// this is only run after getData() resolves
$scope.data = result;
console.log("data.name"+$scope.data.name);
});
}
Edit: Regarding Sujoys comment that
What do I need to do so that myFuction() call won't return till .then() function finishes execution.
function myFunction($scope, myService) {
var myDataPromise = myService.getData();
myDataPromise.then(function(result) {
$scope.data = result;
console.log("data.name"+$scope.data.name);
});
console.log("This will get printed before data.name inside then. And I don't want that.");
}
Well, let's suppose the call to getData() took 10 seconds to complete. If the function didn't return anything in that time, it would effectively become normal synchronous code and would hang the browser until it completed.
With the promise returning instantly though, the browser is free to continue on with other code in the meantime. Once the promise resolves/fails, the then() call is triggered. So it makes much more sense this way, even if it might make the flow of your code a bit more complex (complexity is a common problem of async/parallel programming in general after all!)
for people new to this you can also use a callback for example:
In your service:
.factory('DataHandler',function ($http){
var GetRandomArtists = function(data, callback){
$http.post(URL, data).success(function (response) {
callback(response);
});
}
})
In your controller:
DataHandler.GetRandomArtists(3, function(response){
$scope.data.random_artists = response;
});
I was having the same problem and none if these worked for me. Here is what did work though...
app.factory('myService', function($http) {
var data = function (value) {
return $http.get(value);
}
return { data: data }
});
and then the function that uses it is...
vm.search = function(value) {
var recieved_data = myService.data(value);
recieved_data.then(
function(fulfillment){
vm.tags = fulfillment.data;
}, function(){
console.log("Server did not send tag data.");
});
};
The service isn't that necessary but I think its a good practise for extensibility. Most of what you will need for one will for any other, especially when using APIs. Anyway I hope this was helpful.
FYI, this is using Angularfire so it may vary a bit for a different service or other use but should solve the same isse $http has. I had this same issue only solution that fit for me the best was to combine all services/factories into a single promise on the scope. On each route/view that needed these services/etc to be loaded I put any functions that require loaded data inside the controller function i.e. myfunct() and the main app.js on run after auth i put
myservice.$loaded().then(function() {$rootScope.myservice = myservice;});
and in the view I just did
ng-if="myservice" ng-init="somevar=myfunct()"
in the first/parent view element/wrapper so the controller can run everything inside
myfunct()
without worrying about async promises/order/queue issues. I hope that helps someone with the same issues I had.
I have a controller like this:
function MyCtrl($scope) {
$scope.doSomething = function(){
alert("Do something!");
}
}
And I have multiple views which depend on this (ie multiple of the below):
<div ng-controller="MyCtrl">
...
</div>
The problem is, the data the controller depends on needs to be loaded in the background (the controller does not load that data), and a callback (dataIsReady()) will be called after the data is ready.
function dataIsReady(){
// TODO: call the doSomething() function
}
Now, I want to basically call the doSomething() function, which is inside MyCtrl, from the dataIsReady() function. How can I do that?
I think what you need is a data service, which you can then inject into your controller. You can call a function on your data service which will handle the retrieval of the data and return a "promise" which can then be used to trigger your callback function when the data has loaded.
Have a look at the following code which is a slightly modified version from egghead.io:
Plunker Demo (w/ local storage): http://plnkr.co/edit/9w2jTg?p=preview
var myApp = angular.module('myApp', []);
myApp.factory('AvengersService', function ($http) {
var AvengersService = {
getAsyncCast: function () {
// $http returns a promise, which has a then function, which also returns a promise
var promise = $http.get("avengers.json") // or some JSON service
.then(function (response) {
// The 'then' function here is an opportunity to modify the response
// The return value gets picked up by the 'then' in the controller.
return response.data;
});
// Return the promise to the controller
return promise;
}
};
return AvengersService;
});
myApp.controller('AvengersCtrl', function($scope, AvengersService) {
// Call the async method and then do something when the data is retrieved
AvengersService.getAsyncCast()
.then(function (asyncData) {
// Callback logic that depends on the data goes in here
console.info("Cast pulled async.");
$scope.avengers.cast = asyncData;
});
});
Hope that helps.
Notice: This approach in this answer is terribly wrong, one should not access to the scope of a controller outside of angular, or outside of controller at all. This would also be terribly slow if you try to call it several times. Other than that, it is fine. I am giving this answer because it is also the simplest way. I would never use that kind of code in production, though. The appropriate way is to write a service to communicate with the controller.
Given that you have defined $scope.doSomething in MyCtrl:
var scp = angular.element('[ng-controller="MyCtrl"]').scope();
scp.doSomething();
Will call doSomething method defined in the controller.
I have looked, and assume this is simple, but just couldn't figure out the API documentation for this.
Assume I have a controller that pulls data when first called (I'm leaving out a ton, of course):
myCtrl = function ($scope, Data) {
$scope.data = [];
data_promise = Data.getData(); //a $http service
data_promise.success(function (data) {
$scope.data = data;
});
}
That works great, and when the page loads I get $scope.data populated exactly as I need it. However, of course, the user may wish to update the data. Assume a simple service "Data.save()" called when a server clicks a "save" button on a form:
myApp.factory('Data', function ($http) {
save: function (data) {
$http({
method: 'POST',
url: 'someURL',
data: data,
}).success(function(){
//something here that might trigger the controller to refresh
});
};
});
What would I put in the success callback that might re-instantiate the controller so that it has the most up-to-date data from the server? Currently I am having to refresh the page to get the updated data. I am not worried right now about minimizing server calls by cashing results and changes. I just need to get this to work first.
Thanks!
You do not need to refresh. Simply change the updated data in the ControllerScope.
This should work.
myApp.factory('Data', function ($http) {
save: function (data, $scope) {
$http({
method: 'POST',
url: 'someURL',
data: data,
}).success(function(){
$scope.data = newData;
//something here that might trigger the controller to refresh
});
};
});
// in your controller
Data.save(data, $scope);
But: You shouldn't do this way. This looks messy. Use services or events which you watch to wait for the changes comming back from the service.
OK, although I am sure there is a better answer I have one for me. Essentially I am taking the important parts of the controller and placing them in the success callback. In order to keep it from looking messy, I have wrapped all the parts of the controller that need be updated in a named function.
myCtrl = function ($scope, Data, $q) {
// Binding the Data
var updateAll;
updateAll = function () {
$scope.data1 = [];
$scope.data2 = [];
$scope.data3 = [];
$scope.data4 = [];
service_promise1 = Data.getData1(); //a $http service
service_promise2 = Data.getData2();
service_promise3 = Data.getData3();
service_promise4 = Data.getData4();
$q.all([service_promise1,service_promise2,service_promise3,service_promise4])
.then(function([service1,service2,service3,service]){
$scope.data1 = service1 + service2 //I know this isn't valid Javascript
// just for illustration purposes
$scope.data2 = service2 - service1 + service 3
//etc...
});
};
updateAll();
//Form Section
$("#myForm').dialog({
buttons: {
Save: function () {
Data.save(data).success(function(){
updateAll();
});
}
}
});
}
Breaking this down, I have wrapped all the assignments to scope objects that rely on services into the updateAll function and invoke it on instantiation of the myCtrl. In the form that updates the data I call the updateAll() function upon success of the Data.save() function.
Not exactly brain surgery, I'll admit, but I had gotten confused with $scope.$apply() and thinking about just calling myCtrl(). That somehow seemed like the "Angular" way, but neither worked. I guess the controller function gets run only once on page refresh and there is no Angular way to call it again.