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.
Related
I'm trying to take the response of an $http request and save it to a custom cache. I want to then use that cache to display data into the view. I thought the cache would be checked automatically on each request before fetching new data, but that doesn't seem to be working for me.
The problem I'm having is I can't seem to save the data. The following function needs to make 2 requests: articles and images.
getImages: function() {
var cache = $cacheFactory('articlesCache');
$http.get(posts)
.then(function (data) {
var articles = data;
angular.forEach(articles, function (article) {
var imageId = {id: article.image_id};
$http.post(images, imageId)
.then(function (response) {
article.image = response;
cache.put(article.url, article);
});
});
});
return cache;
}
This creates the custom cache, but there's no data in the returned object. I know now that I can't save the data this way, but I don't know why or how I would go about doing it.
Can anyone explain how storing response data works? Where, if at all, does using promises come in here?
Your return statement executes before the code in your then function does. If you want to return the cache you'll want to run everything through the $q service and then return the resolved promise.
This is probably not the best way to use $cacheFactory. Typically you'd expose your cache as a service at a higher level and then access the cache via the service where needed.
So on your main module you'd have something like this to create the cache.
.factory('cache', function ($cacheFactory) {
var results = $cacheFactory('articleCache');
return results;
})
Then where ever you need the cache you inject it into the controller and use cache.get to retrieve the data from it.
If you want to use $q to implement this, your code would look something like the code below. (Disclaimer: I've never used $q with $cacheFactory like this, so without all of your components, I can't really test it, but this should be close.)
var imageService = function ($http, $q,$cacheFactory) {
var imageFactory = {};
imageService.cache = $cacheFactory('articlesCache');
imageFactory.getImages = function () {
var images = $q.defer();
$http.get(posts)
.then(function (data) {
var articles = data;
angular.forEach(articles, function (article) {
var imageId = {id: article.image_id};
$http.post(images, imageId)
.then(function (response) {
article.image = response;
cache.put(article.url, article);
});
images.resolve(cache.get('articlesCache'))
});
});
return images.promise
app.factory('ImageService', ['$http', '$q', '$cacheFactory', imageService]);
});
I adapted the code from this answer: How to get data by service and $cacheFactory by one method
That answer is just doing a straight $http.get though. If I understand what you're doing, you already have the data, you are posting it to your server and you want to avoid making get call to retrieve the list, since you have it locally.
Am getting an error Error: [$injector:undef] when am using service and http. I couldn't find why is it coming after changing all the changes that has been suggested for this particular error. Kindly check the below code
mainApp.controller('deviceDataController',["$scope","mainService", function ($scope,mainService) {
console.log(mainService);
mainService.deviceDataVar().success(function (data) {
// $scope.deviceDetails = mainService.devices;
console.log(data);
});
}]);
mainApp.factory("mainService",["$http", function ($http) {
angular.forEach(deviceIds, function(value, key) {
var timezone = user_response.timezone;
var DataUrl = LAX_API + "device_info.php?deviceid=" + value + "&limit=288&timezone=" + timezone;
return {
deviceDataVar: function () {
return $http.get(DataUrl).success(function (response) {
devices = response.data;
return devices;
}).error(function (data, status, headers, config) {
// log error
console.log(data)
});;
}
}
});
}]);
kindly help me out with my issue
Thanks!!
Your factory declaration is not valid.
Your factory should return only single method
Create a method that returns $http promises
Agregate all promises inside $q, that will wait for all of them to return a response
Agregate all responses
Return them inside a promise- you cannot return a value, because AJAX calls are async.
Your factory should look like this:
mainApp.factory("mainService", function ($http, $q) {
/* generate single link for deviceID */
function getDevice(value){
var timezone = user_response.timezone;
var dataUrl= LAX_API + "device_info.php?deviceid=" + value + "&limit=288&timezone=" + timezone;
return $http.get(dataUrl);
}
function deviceDataVar(){
// map method will transform list of deviceIds into a list of $http promises
var allRequests = deviceIds.map(getDevice);
// this promise will wait for all calls to end, then return a list of their results
$q.all(allRequests).then(function(arrayOfResults) {
// here you will have all responses, you just need to agregate them into single collection.
// secondly you will need to wrap them into a promise and return
return ...
});
}
/* Return only public methods, at the end */
return {
deviceDataVar: deviceDataVar
}
});
(I was writing this on fly, so there could be few mistakes :), but the conception is right though )
Useful links:
Aggregating promises: angular -- accessing data of multiple http calls - how to resolve the promises
Array.prototype.map: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
How to apss a promise to a factory: How can I pass back a promise from a service in AngularJS?
UPDATE:
To make it work by invokiing small steps you should :
Mock the factory method, let it return a hardcoded value. Return it through a promise.
replace hardcoded value with single (hardcoded) URL to a selected device.
use aggregation ($q.all) for this single $http call.
replace single $http with array made from deviceIds list.
I have two REST APIs I need to use: One from Mongolab for development purposes and one from the actual API that is not accessible at the moment. The problem is that the ID is handled a bit differently in these and the object structure differs. Mongo uses the object._id.$oid notation and the actual API object.ID notation. The Mongolab resource is:
app.factory('Items', function ($resource) {
var items = $resource('https://api.mongolab.com/api/1/databases/x/collections/items/:id',
{
apiKey:'x',
id:'#_id.$oid'
}
});
return items;
});
And the query call (currently using):
$scope.items = Items.query({}, function () {
if (API == 'Mongo') {
angular.forEach($scope.items, function(item) {
item.ID = item._id.$oid;
});
};
});
I want to be able to easily switch the different APIs without modifying the code in every query call or link (I have dozens of calls and links with resource IDs). So I want to move the API == 'Mongo' check to upper level: I tried to use the forEach ID altering directly in the factory where I create the Items resource but it doesn't work that way. How can I modify the results directly before the results are populated through query ?
Or should I just create different branches for different APIs?
I think you can just add two distinct factories, and query the one you need depending on the value of "API" variable.
app.factory('ItemsApi', function ($resource) {
var items = $resource('https://api.mongolab.com/api/1/databases/x/collections/items/:id',
{
apiKOey:'x',
id:'#_id.$oid'
}
});
return items;
});
app.factory('ItemsMongo', function ($resource) {
var items = $resource('https://api.mongolab.com/api/1/databases/x/collections/items/:id',
{
apiKey:'x',
id:'#id'
}
});
return items;
});
Then in the controller (instead of in the resource factory) you could use:
if (API == 'Mongo')
$scope.items = ItemsApi.query();
else
$scope.items = ItemsMongo.query();
UPDATE:
If this is what you currently have, then you may want to consider adding an additional property to each element in the returned array. You can do this by means of overriding the default factory query() method and then iterating over each element adding a duplicate ID field. Check this out:
$resource('https://../:id',{id: '#id'}, {
query: {
isArray: true,
method: 'GET',
params: {},
transformResponse: function (data) {
var wrapped = angular.fromJson(data);
angular.forEach(wrapped, function(item) {
--do something to wrapped items --
});
return wrapped;
}
}
transformResponse and angular.forEach should do the trick
Recently it has become possible to use angularjs within google apps script via the iframe sandbox mode.
My problem comes when trying to communicate with the server (gapps spreadsheet) and receiving asynchronous data in return.
The implementation for receiving data from the server is to use a function with a callback function like so:
google.script.run.withSuccessHandler(dataGatheringFunction).getServerData();
getServerData() would be a function that resides server-side that would return some data, usually from the accompanying spreadsheet. My question is how to use the callback function within the parameters of AngularJS. A typical $http function could be placed in a provider, and the scope value could be populated after then.() returns. I could also invoke $q. But how would I deal with the necessity of google's callback?
Here's a simplified version of what I'm messing with so far:
app.factory("myFactory", function($q){
function ssData(){
var TssData = function(z){
return z;
}
google.script.run.withSuccessHandler(TssData).getServerData();
var deferred = $q.defer();
var d = deferred.resolve(TssData)
console.log("DP: " + deferred.promise);
return deferred.promise;
}
return ssData();
})
Then in the controller resolve the server call similar to this:
myFactory.then(set some variables here with the return data)
My question is simply - How do I deal with that callback function in the provider?
The script throws no errors, but does not return the data from the server. I could use the old $timeout trick to retrieve the data, but there should be a better way.
You only need to $apply the output from the server function:
google.script.run.withSuccessHandler(function(data) {
$scope.$apply(function () {
$scope.data = data;
});
}).withFailureHandler(errorHandler).serverFunction();
Maybe the most elegant solution that makes sure the google.script.run callbacks are registered automatically in the AngularJS digest cycle would be to use the $q constructor to promisify the google callbacks. So, using your example above:
app.factory('myFactory', ['$q', function ($q){
return {ssData: ssData};
function ssData(){
var TssData = function(z){
return z;
};
var NoData = function(error) {
// Error Handling Here
};
return $q(function(resolve, reject) {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.getServerData();
}).then(TssData).catch(NoData);
}
}]);
Then in your controller you can call myFactory.ssData()
Since I don't know exactly what TssData is doing I included it here but note that this simply returns another promise in this context which you will still have to handle in your controller:
myFactory.ssData().then(function(response) {
// Set data to the scope or whatever you want
});
Alternately, you could expose TssData by adding it to the factory's functions if it is doing some kind of data transformation. If it is truly just returning the response, you could refactor the code and omit TssData and NoData and handle the promise entirely in the controller:
app.factory('myFactory', ['$q', function ($q){
return {ssData: ssData};
function ssData(){
return $q(function(resolve, reject) {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.getServerData();
});
}
}]);
app.controller('myController', ['myFactory', function(myFactory) {
var vm = this;
myFactory.ssData()
.then(function(response) {
vm.myData = response;
}).catch(function(error) {
// Handle Any Errors
});
}]);
An excellent article about promises (in Angular and otherwise) is here: http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
This guy seems to be pulling data from a GSheet into angular quite happily without having to do anything fancy.
function gotData(res) {
$scope.validUser = res.validUser;
var data = angular.copy(res.data), obj, i=0;
Object.keys(data).forEach(function(sh) {
obj = {title: sh, checked: {}, showFilters: false, search: {}, sort: {index: 0, reverse: false}, currentPage: 0, checkedAll: true, showBtns: true, searchAll: ''};
obj.heading = data[sh].shift();
obj.list = data[sh];
obj.heading.forEach(function(s,i) {
obj.checked[i] = true;
});
$scope.sheets.push(obj);
});
$scope.sheets.sort(function(a,b) {
return a.title > b.title ? 1 : -1;
});
$scope.gotData = true;
$scope.$apply();
}
google.script.run.withSuccessHandler(gotData).withFailureHandler($scope.gotError).getData();
My solution was to get rid of the $q, promise scenario all together. I used $rootScope.$broadcast to update scope variables from the server.
Link to spreadsheet with script.
As I scour the internet, I am finding so many different ways to manage the data model that is used in our Angular templates, but they all only show a small part of the bigger picture. In a large application we need to glue together API data to some form of JavaScript model which in turn is used within our template, but I am not sure how this should all be managed.
After reading this article on AngularJS models I am now aware that I should be wrapping all of my models in a service so the information is available across multiple controllers. One of the only things is that it does not explain how to tie these Services in with API requests.
Here is my current implementation.
Customer Model
var Customer = function (obj) {
var self = this;
if (!obj) {
self.Name = null;
self.Address = null;
self.PrimaryEmailAddress = null;
self.SecondaryEmailAddress = null;
self.PhoneNumber = null;
} else {
self = obj;
}
return self;
}
Then in my controller I use this model on my $scope like
Customer Controller
app.controller('CustomerController', [function($scope, API){
$scope.model = {};
API.Account.getCustomer({id:'12345'}, function(data){
$scope.model = new Customer(data);
});
}]);
This is what my API service looks like
API Service
app.factory("API", ["$resource", function ($resource) {
var service = {};
service.Account = $resource('/WebApi/Account/:type/:id', {},
{
getCustomer: { method: 'GET', params: { type: 'Customer' } }
});
return service;
}])
This has worked fairly well up until now, when I realized that there is API information that is gathered in a parent controller that is now needed in a child controller
Going back to the article linked above, I now can change my models around so they are wrapped in an Angular Service and are therefore available across multiple controllers. This new CustomerService may look like this
NEW CustomerService
app.factory('CustomerService', function() {
var CustomerService = {};
var customer = new Customer();
CustomerService.getCustomer = function() { return customer; }
CustomerService.setCustomer = function(obj) { customer = obj; }
return CustomerService;
});
This isn't quite like the article (much simpler) but it basically contains the OO-like functions to get and set the Customer object.
The problem with this way is we still don't have access to the actual API endpoint to get the customer data from the server? Should we keep the API Service and the Model Services seperate or try to combine them into a single Service? If we do this then we also need a way to differentiate between actually getting fresh data from the server and just getting the currently set Customer object in the singleton object.
I would like to know your initial response to this predicament?
UPDATE
I came across this article that brings together all the functionality of a model including the API requests into a factory service
Model service and API service should be separate. The API service can depend on model service and return model objects instead of directly returning what came from api.
app.factory("API", ["$resource",'Customer','$q' function ($resource,Customer,$q) {
var service = {};
service.Account = function(accountId) {
var defer=$q.defer();
var r=$resource('/WebApi/Account/:type/:id', {},
{
getCustomer: { method: 'GET', params: { type: 'Customer' }}
});
r.getCustomer({id:'12345'}, function(data){
defer.resolve(new Customer(data));
})
return defer.promise;
}
return service;
}]);
This way you have combined the model service and API service. I am assuming Customer is a model service rather than the Customer object.
Update: Based on your follow up question, i thought there could be a better way, but i have not tried it. Rather than creating own promise every time we can use the resource promise:
service.Account = function(accountId) {
var r=$resource('/WebApi/Account/:type/:id', {},
{
getCustomer: { method: 'GET', params: { type: 'Customer' }}
});
return r.getCustomer({id:'12345'}).$promise.then(function(data) {
return new Customer(data);
});
}
Since the then method itself returns promise that is resolved to return value of the then callback statement this should work.
Try this approach and share your findings