I have a factory where I'm getting data from a server using the $http methods:
.factory('$factory', function ($q, $http, $timeout, $state, $ionicHistory, $localstorage) {
var obj = [];
var functions = {
getData: function () {
var dfd = $q.defer();
if(!obj){
$http({
url: remoteUrl+'/getdata',
method: 'POST',
data: {}
}).then(function(response) {
$timeout(function(){
obj = response.data;
dfd.resolve(response.data);
}, 2000)
}, function(response) {
}
}else{
return obj;
}
return dfd.promise;
}
}
}
So this gets the data and puts it in an object. Correct me if I'm wrong, but this method of using a factory for this type of action is so it's not tied to a controller, and can be used anywhere in my application.
With this in mind, I wish to make it so I can get the data anywhere in my app without having to query the server each time. i.e. once it's queried the server the factory saves the response to an object (like I'm doing now). But I'm having trouble accessing the data afterwards in another controller for example.
I've started to make what I think it should look like by using the if(!obj) line, but in the else statement I cant seem to just return the obj object. It throws errors as it's not returning a promise like it's expected too.
I'm not sure if I'm even along the right lines with this.
Thanks
You're returning a promise so you need to always return a promise even on the cached response, you can do it relatively easy at this level by wrapping it in a $q.when (https://docs.angularjs.org/api/ng/service/$q) which will return a promise immediately resolved.
return $q.when(obj);
Although $http service has built in caching, so you may want to take a look under the Cache section.
https://docs.angularjs.org/api/ng/service/$http
https://www.ng-book.com/p/Caching/
This should work:
Insteada assigning obj = [] assign as a null. Usually I prefer callback. You can try this code:
.factory('$factory', function($q, $http, $timeout, $state, $ionicHistory, $localstorage) {
var obj = [];
var functions = {
getData: function(cb) {
// instead of checking !obj you have to check for length or you have to set obj as null
if (obj && obj.length == 0) {
$http({
url: remoteUrl + '/getdata',
method: 'POST',
data: {}
}).then(function(response) {
obj = response.data;
cb(response.data)
}, function(response) {
}
}
else {
cb(obj)
}
}
}
}
})
// You can use callback by following code
$factory.getData(function(response){
// response will come here
})
Related
So following my last question of how to consume multiple $http, I need also to sort the data and match them. It's mean, I have the Albums, Photos and users:
http://jsonplaceholder.typicode.com/albums
http://jsonplaceholder.typicode.com/photos
http://jsonplaceholder.typicode.com/users
For each Album I need to display the owner name and number of photos.
So I try this to get the Album by Id, but it gives me error:
$http.get('http://jsonplaceholder.typicode.com/albums/'+ $scope.id + '/photos')
Or I also try this:
$http.get('http://jsonplaceholder.typicode.com/albums/'+ albumUsers.id + '/photos')
But still get error.
The question is if there's a way to link / create dependency between them, so the second controller basically rely on the first one?
Thanks
So this is the controller to get the albums and the users
MyAlbum.controller('albumList', function($scope, $http, $q) {
$http.get('http://jsonplaceholder.typicode.com/albums').
then(function(response) {
$scope.albumDetails = response.data;
});
$http.get('http://jsonplaceholder.typicode.com/users').
then(function(response) {
$scope.albumUsers = response.data;
});
$q.all(['http://jsonplaceholder.typicode.com/albums','http://jsonplaceholder.typicode.com/users',]).
then(function(response){
$scope.albumDetails = response[0].data;
$scope.albumUsers= response[1].data;
});
});
And this the controller to get each album - for the example, I'm using a link to one specific album:
MyAlbum.controller('photoList', function($scope, $http) {
$http.get('http://jsonplaceholder.typicode.com/albums/1/photos')
.then(function(response) {
$scope.PhotoDetails = response.data;
});
});
The thing is that instead of albums/1 it should be albums/id
You can chained the $http calls together.Success function of the first $http request calls the second $http request.
function firstCall() { // It will return a promise.
return $http({
method: 'GET',
url: firstAPIURL
});
}
function dependantCall() { // It will return a promise
return $http({
method: 'GET',
url: secondAPIURL
});
}
$scope.onloadRequest = function() { // This function will execute on load the view/controller.
firstCall()
.then(function(result) {
$scope.value = result.data; // Now that the server has answered, you can assign the value to $scope.value, and then, call the second function.
dependantCall().then(function(dependentResult) {
// Your code comes here
}, function(dependentError){
// If an error happened during the dependent call, handle it here.
});
}, function(error){
// If an error happened during first call, handle it here.
});
};
$scope.onloadRequest();
There is one thing I can't understand in Angular - how better to sync API data with local data.
My structure object is very big, I just give you simple example.
I have 4 api links.
api_links: {
allCars: '/cars/',
cabrio: '/cars/cabrio',
sedan: '/cars/sedan',
mercedes: 'cars/sedan/mercedes'
}
And this is my local object to keep data locally
$rootScope.apiData = {
cars{
cabrio:{},
sedan: {}
}
}
In my single page app I want to reduce count of requests.
So on common page I'm recieving all Cars and put data to $rootScope.apiData
When I go to sedans page, I check if there any data in $rootScope.apiData.cars.sedan. If it's exist, I don't send request.
But if I start from sedans page, I recieve only sedans. Then when I go to common page - HOW can I check am I need to load more categories.
It's quick abstract example. But I try to find any good solutions or plugins for this and cannot.
Please help me!
UPDATE:
Here is my factory:
var api = angular.module('app.api', []);
api.factory('GetData', ['$http', '$q', '$rootScope', '$location'
function($http, $q, $rootScope, $location) {
var apiUrl = '/api/js/';
var apiMixes = {
dashBoard: 'api/dashboard',
accounts: 'api/accounts',
top: 'top',
logout: 'client/logout',
...
}
var methods = {};
methods.getApi = function(u, params) {
var url = apiMixes[u] + '?' + $.param( params );
var deferred = $q.defer();
// Here I need to check if I have cached data
cachedData(data) = ___TODO___;
if( cachedData(data) ){
deferred.resolve(cachedData(data));
// Turn watching on
$rootScope.$emit("receivedApiUpdate", cachedData(data));
return deferred.promise;
}
// If no - get gata from server and put to cache
$http.get(url, {cache: true}).success(function (res, status, headers, config) {
if(res && res.data){
// Turn watching on
$rootScope.$emit("receivedApiUpdate", res.data);
//LocalData.put(res.data);
deferred.resolve(res.data);
}else{
deferred.reject(status);
}
}).error(function (res, status, headers, config) {
(status==401) && $location.path('login/lock');
deferred.reject(status);
});
return deferred.promise;
};
methods.sendData = function(u, data, o) {
data = data || {};
o = o || {};
var deferred = $q.defer();
var url = '/api/js/'+ apiMixes[u];
$http.post(url, JSON.stringify(data)).success(function(res, status) {
if(res.success && res.data){
// Here I need to update my cache with some new value
o.localUpdate && ___PUT_TO_CACHE_TODO___;
deferred.resolve(res.data);
}
});
return deferred.promise;
};
return methods;
}]);
Here is my Controller
app.controller('MyAccountsCtrl', ["$rootScope", "$scope",
function ($rootScope, $scope) {
// Watcher for api updates
$rootScope.$on('receivedApiUpdate',function(event, response){
if(!response || !response.accounts) return;
renderData(response.accounts);
});
function renderData(accounts){
// This renders to template
$scope.accountList = accounts;
}
}]);
Here is mainCtrl, whick make common request, after it I need to update data in several templates.
app.controller('AppCtrl', ['$rootScope', '$scope', 'GetData',
function ($rootScope, $scope, GetData) {
// Here I fire different request for each page, I keep requests in router.
GetData.getApi( 'accounts' ).then(function(data){
// Show content when page is loaded
$('.main-content').removeClass('hidden');
},function(res){
log('GET DATA FAIL', res);
});
}]);
You need to create a service/factory for this, not use the $rootScope. Also, unless you need the data to be constantly updated, you can use the cache: true option inside your $http call.
angular
.module('app')
.factory('carFactory', carFactory);
function carFactory() {
var factory = {
getData: getData
}
return factory;
function getData(callback) {
$http({
method: 'GET',
url: '/cars',
cache: true
}).success(callback);
}
}
And then inside your controller/directives just inject the carFactory and use carFactory.getData(function(cars) { ... }) when you need the data. If the data doesn't exist, it will $http call for it and afterwards execute the callback function. If it does exist, it will return the data directly to the callback, without an $http call.
I have this service:
angular.module('autotestApp').factory('GroupService', ['$http', function ($http) {
var groups = [];
return{
list: function () {
return groups;
},
retrieve: function () {
$http({
method: "get",
url: "/enterprises/_groups"
}).success(function (response) {
groups = response;
}).error(function () {
console.log("failed")
});
}
}
}]);
and this is my controller:
angular.module('autotestApp').controller('GroupsController', function($scope, $http, GroupService) {
GroupService.retrieve();
$scope.items = GroupService.list();
});
So, in my controller, I am first getting the result from the API so that the variable groups(in the service) gets assigned, then I am using the list() method to assign the values to $scope.items.
I am sure that API is returning results. But the
groups = response
part is not working correctly. When I changed it to
groups.push(response)
it is working but the result is a list inside a list, which I dont't want: [[ Object, Object, Object ]]
I want this: [ Object, Object, Object ]
How to fix this?
The reason
groups = response
is not working is because you're sending an async request that will replace the groups reference after you've already retrieved the old reference via the list function. The reason it works with the push modification is because Angular creates a watch that notices that the collection has changed and updates your view. However, your code is now working, but you don't understand why it works, and it's prone to breaking unexpectedly.
When dealing with asynchronous code, the best way to deal with it is to write your own code to take that into account. Here's a version that does the whole thing in an async fashion:
angular.module('autotestApp').factory('GroupService', ['$http', function ($http) {
var groupsResult;
return{
retrieve: function () {
if (groupsResult) { return groupsResult; }
return groupsResult = $http({
method: "get",
url: "/enterprises/_groups"
}).then(function (response) {
return response.data;
}, function () {
console.log("failed")
});
}
}
}]);
angular.module('autotestApp').controller('GroupsController',
['$scope', 'GroupService', function($scope, GroupService) {
GroupService.retrieve().then(function (groups) {
$scope.items = groups;
});
}]);
One of the fixes you could use is:
for (var i = 0; i < response.length; i++) {
groups.push(response[i]);
});
That way you would have [ Object, Object, Object ]
EDIT:
One thing you could try is the following, change your retrieve function to return your promise:
return{
list: function () {
return groups;
},
retrieve: function () {
var promise = $http({
method: "get",
url: "/enterprises/_groups"
}).success(function (response) {
groups = response;
}).error(function () {
console.log("failed")
});
return promise;
}
}
and then in your controller:
angular.module('autotestApp').controller('GroupsController', function($scope, $http, GroupService) {
GroupService.retrieve().finally(function () {
$scope.items = GroupService.list();
});
});
I think your groups = response is working, but when you do $scope.items = GroupService.list() the request isn't finished yet.
do groups.concat(response)
this will flatten the items & add it to parent list rather than appending a single element.
I am trying to create a service which first loads some data by making an AJAX call using $http.
I am looking at something like:
app.factory('entityFactory', function() {
var service = {};
var entities = {};
// Load the entities using $http
service.getEntityById(entityId)
{
return entities[entityId];
}
return service;
});
app.controller('EntityController', ['$scope', '$routeParams', 'entityFactory', function($scope, $routeParams, entityFactory) {
$scope.entity = entityFactory.getEntityById($routeParams['entityId']);
}]);
I want to make sure that the entities is loaded fully before I return the entity using getEntityById.
Please let me know what would be the right way to do this? One way I know would be to make a synchronous AJAX call, but is there anything better? Can promises be used in this case in a better way?
Tried using $q to check if service is initialized. Clean enough for me, any other methods are welcome :).
app.factory('entityFactory', function($q, $http) {
var service = {};
var _entities = {};
var _initialized = $q.defer();
$http({method: 'GET', url: '/getData'})
.success(function(data, status, headers, config) {
if (data.success)
{
_entities = data.entities;
}
_initialized.resolve(true);
})
.error(function(data, status, headers, config) {
_initialized.reject('Unexpected error occurred :(.');
});
service.getEntityById(entityId)
{
return entities[entityId];
}
service.initialized = _initialized.promise;
return service;
});
app.controller('EntityController', ['$scope', '$routeParams', 'entityFactory', function($scope, $routeParams, entityFactory) {
entityFactory.initialized.then(function() {
$scope.entity = entityFactory.getEntityById($routeParams['entityId']);
});
}]);
You can utilize callbacks within factories to store the data on the first call and then receive the data from the service on every subsequent call:
app.factory('entityFactory', function() {
var service = {};
var entities = null;
// Load the entities using $http
service.getEntityById(entityId, callback)
{
if (entities == null) {
$http(options).success(function(data) {
entities = data;
callback(data);
});
} else {
callback(entities);
}
}
return service;
});
And then you can use this:
entityFactory.getEntityById(id, function(entities) {
//console.log(entities);
});
Passing in a callback or calling $q.defer(), are often signs that you're not taking advantage of promise chaining. I think a reasonable way to do what you're asking is as follows.
app.factory('entityFactory', function($http) {
var service = {};
var _entitiesPromise = $http({method: 'GET', url: '/getData'});
service.getEntityById = function(entityId) {
return _entitiesPromise.then(function(results) {
return results.data.entities[entityId];
});
};
return service;
});
app.controller('EntityController', ['$scope', '$routeParams', 'entityFactory', function($scope, $routeParams, entityFactory) {
entityFactory.getEntityById($routeParams['entityId']).then(function(entity) {
$scope.entity = entity;
}, function() {
// Can still do something in case the original $http call failed
});
}]);
where you only cache the promise returned from $http.
I have a method that calls an angular service and consequently makes an ajax request via the service. I need to make sure that if this is called several times, the previous request in aborted (if it hasn't already been resolved that is).
This method can get called multiple times. This method is actually from ngTable on ngTableParams:
getData = function($defer, params) {
myService.getRecord(params).then(function(res){
...
$defer.resolve(res.Records);
});
}
Here's the method on the service:
this.getRecords = function(params) {
...
return Restangular
.all('/api/records')
.post(filters);
};
If ngTable makes 3 calls I want the first 2 to be aborted (unless of course they returned so fast that it got resolved)
You can abort $http calls via the timeout config property, which can be a Promise, that aborts the request when resolved.
So in restangular, you can do this like
var abort = $q.defer();
Restangular.one('foos', 12345).withHttpConfig({timeout: abort.promise}).get();
abort.resolve();
To integrate it with your usecase, for example, you could have this in your service:
var abortGet;
this.getRecords = function(params) {
...
if (abortGet) abortGet.resolve();
abortGet = $q.defer();
return Restangular
.all('/api/records')
.withHttpConfig({timeout: abortGet.promise})
.post(filters);
}
This way calling getRecords always aborts the previous call if has not been resolved yet!
This is another approach if you want to abort all http requests when change the state for UI-router:
angular
.run(function(HttpHandlerSrv) {
HttpHandlerSrv.abortAllRequestsOn('$stateChangeSuccess');
HttpHandlerSrv.R.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
httpConfig = httpConfig || {};
if(httpConfig.timeout === undefined) {
httpConfig.timeout = HttpHandlerSrv.newTimeout();
}
return { element: element, params: params, headers: headers, httpConfig: httpConfig };
});
})
.factory('HttpHandlerSrv', HttpHandlerSrv);
/** ngInject */
function HttpHandlerSrv($q, $rootScope, Restangular) {
var requests = [];
return {
R: Restangular,
newTimeout: newTimeout,
abortAllRequests: abortAllRequests,
abortAllRequestsOn: abortAllRequestsOn
};
function newTimeout() {
var request = $q.defer();
requests.push(request);
return request.promise;
}
function abortAllRequests() {
angular.forEach(requests, function(request) {
request.resolve();
});
requests = [];
}
function abortAllRequestsOn(event) {
$rootScope.$on(event, function(event, newUrl, oldUrl) {
if(newUrl !== oldUrl) { abortAllRequests(); }
});
}
}