The InvoiceModel below has a getByCustomer() function that lists the invoices for a given customer.
I'm just wondering what's the difference between caching data to a regular javascript variable compared to using $cacheFactory.
Regular Javascipt Variable:
angular.module('app', ['ngResource'])
.factory('Invoice', ['$resource',
function($resource) {
return $resource('http://foo.bar/invoices');
}
])
.factory('InvoiceModel', ['Invoice',
function(Invoice) {
var customer_invoices = [];
return {
getByCustomer: function(customer_id) {
if (customer_invoices[customer_id] == undefined) {
customer_invoices[customer_id] = Invoice.get({customer_id: customer_id});
}
return customer_invoices[customer_id];
}
};
}
]);
$cacheFactory
angular.module('app', ['ngResource'])
.factory('Invoice', ['$resource',
function($resource) {
return $resource('http://foo.bar/products');
}
])
.factory('InvoiceModel', ['Invoice', '$cacheFactory',
function(Invoice, $cacheFactory) {
var customerInvoicesCache = $cacheFactory('customerInvoicesCache');
return {
getByCustomer: function(customer_id) {
var invoices = customerInvoicesCache.get(customer_id);
if (!invoices) {
customerInvoicesCache.put(Invoice.get({customer_id: customer_id}));
}
return invoices;
}
};
}
]);
I wouldn't use a $cacheFactory in the fashion you have shown. You're kind of using it the way $http or $resource would use it internally.
Instead, you can configure an $http or $resource object to cache the responses for specific queries. You then merely use the $http or $resource as normal. It handles the caching for you.
Example with $resource:
.factory('Invoice', ['$resource',
function($resource) {
return $resource('http://foo.bar/products', {}, {
query: { cache: true }
});
}
])
The above overrides the query method of the resource to enable caching with the default $cacheFactory. The first time your code calls the query method the response will get cached. Any subsequent calls to the query method will use the cached response.
Example with $http:
$http.get('/the-url', { cache: true }).then(...)
// or by passing in your own cache factory
var cache = $cacheFactory('myCacheFactory');
$http.get('/the-url', { cache: cache }).then(...);
Clearing the cache:
When you need to clear the cache, you get the $cacheFactory and call it's remove() function to clear the cache for the associated URL:
// default cache factory
var defaultCache = $cacheFactory('$http');
defaultCache.remove('/some/url');
// custom cache factory
var cache = $cacheFactory('myCacheFactory');
cache.remove('/custom-cache-url');
Related
I have a similar situation like in AngularJS : Initialize service with asynchronous data, but I need to inject my base service with asynchronous data into depend services. It looks like this:
Base service:
angular.module('my.app').factory('baseService', baseService);
baseService.$inject = ['$http'];
function baseService($http) {
var myData = null;
var promise = $http.get('api/getMyData').success(function (data) {
myData = data;
});
return {
promise: promise,
getData: function() {
return myData;
}};
}
Dependent service (in which I inject base service)
angular.module('my.app').factory('depentService', depentService);
depentService.$inject = ['baseService'];
function depentService(baseService) {
var myData = baseService.getData();
....
return {...};
}
Route:
angular.module('my.app', ["ngRoute"])
.config(function ($routeProvider) {
$routeProvider.when('/',
{
resolve: {
'baseService': function (baseService) {
return baseService.promise;
}
}
});
});
But nothing happens, baseService.getData() still returns null (coz async api call still in progress). Seems like my ngRoute config is invalid, but I cant indicate any controller/template into it.
How can I correctly resolve my baseService and get data in Depend service?
Dependent services should work with promises and return promises:
angular.module('my.app').factory('depentService', depentService);
depentService.$inject = ['baseService'];
function depentService(baseService) {
̶v̶a̶r̶ ̶m̶y̶D̶a̶t̶a̶ ̶=̶ ̶b̶a̶s̶e̶S̶e̶r̶v̶i̶c̶e̶.̶g̶e̶t̶D̶a̶t̶a̶(̶)̶;̶
var promise = baseService.promise;
var newPromise = promise.then(function(response) {
var data = response.data;
//...
var newData //...
return newData;
});
return newPromise;
}
To attempt to use raw data risks race conditions where the dependent service operates before the primary data returns from the server.
From the Docs:
The .then method returns a new promise which is resolved or rejected via the return value of the successCallback, errorCallback (unless that value is a promise, in which case it is resolved with the value which is resolved in that promise using promise chaining).
— AngularJS $q Service API Reference - The Promise API
Is it possible to add values to my $resource $cacheFactory from my controller & keep this data in sync across multiple controllers? Essentially what I'm trying to accomplish is:
1) pull JSON resource from API
2) manipulate JSON as if it weren't a $resource anymore, just a plain JSON object that I can use between controllers.
Is there an "angular way" to do this or should I just cache the whole place list using local storage & read and write everything else from there?
.factory('places', ['$resource','environment','$cacheFactory',
function($resource, environment, $cacheFactory) {
var cache = $cacheFactory('places');
return $resource(environment.apis.places, {}, {
query: {
isArray:true,
method: 'GET',
cache: cache
}
});
}
])
.controller('ItemCtrl', function($scope, places) {
places.query({}, function(result){
$scope.item = result[0]
})
$scope.makeFav = function(index){
//add a new key to cached places data
$scope.item.fav = true
}
}
.controller('ListCtrl', function($scope, places) {
places.query({}, function(result){
$scope.item = result //should get the updated cache changed by ItemCtrl
})
console.log($scope.item[0].fav) //should = true
}
Use the following process:
Create a constant recipe
Inject cacheFactory
Create an instance of cacheFactory
Inject the constant into each controller
Reference the instance of cacheFactory
Manipulate the instance of cacheFactory in each controller
function sharedCache($cacheFactory)
{
var sharedCache = $cacheFactory.get('sharedCache') ? $cacheFactory.get('sharedCache') : $cacheFactory('sharedCache');
return sharedCache;
}
function bar(sharedCache, $scope)
{
sharedCache.put('config', $scope.config);
}
bar.$inject = ['sharedCache', '$scope'];
sharedCache.$inject = ['$cacheFactory'];
angular.module('foo',[]);
angular.module('foo').constant('sharedCache', sharedCache);
angular.module('foo').controller('bar', bar);
References
AngularJS Documentation for iid
ng-book: caching through HTTP
I have the following pattern in my AngularJS which calls for refactoring:
$scope.advertisers = Advertiser.query()
$scope.advertisersMap = {};
$scope.advertiser.$then(function (response) {
arrayToMap(response.resource, $scope.advertisersMap)
}
arrayToMap is function that adds each item in an array to the object with it's ID as key.
Now, I would have liked that this will happen in Advertiser itself, e.g.
$scope.allAdvertisers = Advertiser.query()
// Somewhere else
var advertiser = Advertiser.get({id: 2})
Where Advertiser.get({id: 2}) will return from a cache populated earlier by the query method.
Advertiser is defined in factory:
.factory('Advertiser', ['djResource', 'Group', function ($djResource, Group) {
var Advertiser = $djResource('/campaigns/advertisers/:advertiserId/', {advertiserId: '#id'});
Advertiser.prototype.getGroups = function () {
return Group.getByAdvertiser({advertiserId: this.id});
};
return Advertiser;
}])
Sadly, DjangoRestResource (and $resource which it wraps) caches by URLs, so query() will cache /advertisers/ while get(2) will cache /advertisers/2/, but I want query to cache in such way that get will be able to retrieve it as well.
I've tried replacing the query function by a wrapper function that does the caching, but I want it to return an promise which is also an Array like $resource does. It was something like:
var oldQuery = Advertiser.query;
var cache = $cacheFactory('advertisers');
Advertiser.query = function () {
var promise = oldQuery();
return promise.then(function (response) {
angular.forEach(response.resource, function (resource) {
cache.put(resource.id, resource)
})
})
};
But then the returned promise is no longer an Array-like object, and it doesn't not encapsulate the returned results in an Advertiser object as it used to, which breaks most of my code expects that Advertiser.query() will eventually be an Array.
What other approaches should I try? This snippet repeats itself in every controller for multiple factories and it hurts my eyes.
Here is solution to the problem:
function makeCachingMethod(object, method, cache, config) {
var $q = angular.injector(['services']).get('$q');
var oldMethod = object[method];
object[method] = function () {
var result;
if (!config.isArray) {
var id = config.idParam ? arguments[0][config.idParam] : arguments[0];
result = cache.get(id);
if (result !== undefined) {
if (result.$promise === undefined) {
result.$promise = $q.when(result);
result.$resolved = true;
}
return result;
}
}
result = oldMethod.apply(this, arguments);
result.$promise.then(function (data) {
if (config.isArray) {
angular.forEach(data, function (item) {
cache.put(item.id, item);
})
} else {
cache.put(data.id, data);
}
return data;
});
return result;
}
}
And an example usage:
app.factory('Country', ['$resource', '$cacheFactory', function ($resource, $cacheFactory) {
var Country = $resource('/campaigns/countries/:id', {id: "#id"}, {
query: {method: 'GET', url: '/campaigns/countries/', isArray: true},
get: {method: 'GET', url: '/campaigns/countries/:id/', isArray: false}
});
var cache = $cacheFactory('countries');
makeCachingMethod(Country, 'query', cache, {isArray: true});
makeCachingMethod(Country, 'get', cache, {idParam: 'id'});
return Country;
}])
What is happening here?
I use makeCachingMethod to decorate the original method created by $resource. Following the pattern used by $resource itself, I use a configuration object to signal whether the decorated method returns an array or not, on how the id is passed in queries. I assume, though, that the key of the ID to save is 'id', which is correct for my models but might need to be changed.
Noticed that before returning an object from the cache, the decorator adds to it $promise and $resolved attributes, since my application expects objects originated from $resource which have these properties, and in order to keep using the promises API, e.g.:
$scope.advertiser = Advertiser.get({advertiserId: $scope.advertiserId});
$scope.advertiser.$promise.then(function () {
$scope.doSomething();
});
Notice that since the function is defined outside the scope of any Angular module it is required to inject the $q service using angular.injector(). A nicer solution will be to return a service for invoking the decorator function. Such service could also handle the generation of the caches themselves.
This solution does not handle the expiration of cached models, which isn't much of problem in my scenario, as these rarely change.
I'm not aware of any cache support directly with $resource, you could create a factory that abstracts the Advertiser resource and handle the caching yourself.
Here is some helpful info on caching:
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#validating-cached-responses-with-etags
I'm working on an app at the minute using $resource and every method is abstracting so I can check the cache to see if what I'm requesting is there and return it, if not, make the call using $resource.
I'm using AngularJS and Angular Routing. I have template 'home.html' and controller for that template 'homeController'. Also, I have this factory:
app.factory('therapyService', function ($http) {
return {
getTherapies: function () {
//return the promise directly.
return $http.get('http://localhost:8080/application/api/rest/therapy').then(function (result) {
//resolve the promise as the data
return result.data;
});
}
}
});
In controller:
therapyService.getTherapies().then(function (results) {
$scope.therapies = results;
...
}
Problem is whenever I click on that page, that http call is called all over again. I want it called only first time when page is loaded first time. How to do it?
You can enable caching for individual calls by passing a configuration argument to the $http call:
return $http.get('url', { cache: true }).then(...)
If you see that most of your calls need to be cached and very few uncached, then you can enable the caching by default for your calls during the configuration phase of your application:
var myApp = angular.module('myApp',[])
.config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.cache = true;
}])
Then you can explicitly disable caching for some calls by passing { cache: false }. I would recommend explicitly enabling caching. Caching is an optimization and enabling it by default can risk breaking the application if you forget to disable it at some point.
Say I need to include a GroupId parameter to every request the user makes, but I don't want to modify every service call to include that. Is it possible to make that GroupId appended automatically to all requests, whether it is POST or GET query string?
I have been looking into the interceptor request function, but can't figure out how to make the change
** Edit **
Current working sample below is a combo of Morgan Delaney and haimlit's suggestions (I think it is a combom anyway). The basic idea is that if the request is a POST, modify config.data. For GET, modify params. Seems to work so far.
Still not clear on how the provider system works in Angular, so I am not sure if it is entirely approriate to modify the data.params properties here.
.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function ($rootScope, $q, httpBuffer) {
return {
request: function (config) {
if (config.data === undefined) {
//Do nothing if data is not originally supplied from the calling method
}
else {
config.data.GroupId = 7;
}
if (config.method === 'GET') {
if (config.params === undefined) {
config.params = {};
}
config.params.GroupId = 7;
console.log(config.params);
}
return config;
}
};
} ]);
} ]);
If your example works, great. But it seems to lack semantics IMHO.
In my comments I mentioned creating a service but I've set up an example Plunker using a factory.
Plunker
Relevant code:
angular.module( 'myApp', [] )
.factory('myHttp', ['$http', function($http)
{
return function(method, url, args)
{
// This is where the magic happens: the default config
var data = angular.extend({
GroupId: 7
}, args );
// Return the $http promise as normal, as if we had just
// called get or post
return $http[ method ]( url, data );
};
}])
.controller( 'myCtrl', function( $scope, $http, myHttp )
{
// We'll loop through config when we hear back from $http
$scope.config = {};
// Just for highlighting
$scope.approved_keys = [ 'GroupId', 'newkey' ];
// Call our custom factory
myHttp( 'get', 'index.html', { newkey: 'arg' }).then(function( json )
{
$scope.config = json.config;
});
});