I'm new to AngularJS and am still trying to wrap my head around using services to pull data into my application.
I am looking for a way to cache the result of a $http.get() which will be a JSON array. In this case, it is a static list of events:
[{ id: 1, name: "First Event"}, { id: 2, name: "Second Event"},...]
I have a service that I am trying to use to cache these results:
appServices.service("eventListService", function($http) {
var eventListCache;
this.get = function (ignoreCache) {
if (ignoreCache || !eventListCache) {
eventListCache = $http.get("/events.json", {cache: true});
}
return eventListCache;
}
});
Now from what I can understand I am returning a "promise" from the $http.get function, which in my controller I add in a success callback:
appControllers.controller("EventListCtrl", ["$scope", "eventListService",
function ($scope, eventListService) {
eventListService.get().success(function (data) { $scope.events = data; });
}
]);
This is working fine for me. What I'd like to do is add an event to the eventListService to pull out a specific event object from eventListCache.
appServices.service("eventListService", function($http) {
var eventListCache;
this.get = function (ignoreCache) { ... }
//added
this.getEvent = function (id) {
//TODO: add some sort of call to this.get() in order to make sure the
//eventListCache is there... stumped
}
});
I do not know if this is the best way to approach caching or if this is a stupid thing to do, but I am trying to get a single object from an array that may or may not be cached. OR maybe I'm supposed to call the original event and pull the object out of the resulting array in the controller.
You're on the right track. Services in Angularjs are singeltons, so using it to cache your $http request is fine. If you want to expose several functions in your service I would do something like this. I used the $q promise/deferred service implementation in Angularjs to handle the asynchronus http request.
appServices.service("eventListService", function($http, $q) {
var eventListCache;
var get = function (callback) {
$http({method: "GET", url: "/events.json"}).
success(function(data, status) {
eventListCache = data;
return callback(eventListCache);
}).
}
}
return {
getEventList : function(callback) {
if(eventListCache.length > 0) {
return callback(eventListCache);
} else {
var deferred = $q.defer();
get(function(data) {
deferred.resolve(data);
}
deferred.promise.then(function(res) {
return callback(res);
});
}
},
getSpecificEvent: function(id, callback) {
// Same as in getEventList(), but with a filter or sorting of the array
// ...
// return callback(....);
}
}
});
Now, in your controller, all you have to do is this;
appControllers.controller("EventListCtrl", ["$scope", "eventListService",
function ($scope, eventListService) {
// First time your controller runs, it will send http-request, second time it
// will use the cached variable
eventListService.getEventList(function(eventlist) {
$scope.myEventList = eventlist;
});
eventListService.getSpecificEvent($scope.someEventID, function(event) {
// This one is cached, and fetched from local variable in service
$scope.mySpecificEvent = event;
});
}
]);
You are on the right track. Here's a little help:
appServices.service("eventListService", function($http, $q) {
var eventListCache = [];
function getList(forceReload) {
var defObj = $q.defer(), listHolder;
if (eventListCache.length || forceReload) {
listHolder= $http.get("/events.json", {cache: true});
listHolder.then(function(data){
eventListCache = data;
defObj.resolve(eventListCache);
});
} else {
defObj.resolve(eventListCache);
}
return defObj.promise;
}
function getDetails(eventId){
var defObj = $q.defer();
if(eventId === undefined){
throw new Error('Event Id is Required.');
}
if(eventListCache.length === 0){
defObj.reject('No Events Loaded.');
} else {
defObj.resolve(eventListCache[eventId]);
}
return defObj.promise;
}
return {
eventList:getList,
eventDetails:getDetails
};
});
Then, in your controller, you handle it like this:
appControllers.controller("EventListCtrl", ["$scope", "eventListService",
function ($scope, eventListService) {
var eventList = eventListService.getList();
eventList.then(function(data){
$scope.events = data;
});
$scope.getEventsList = function(reloadList){
eventList = eventListService.getList(reloadList);
eventList.then(function(data){
$scope.events = data;
});
};
$scope.getEventDetails = function(eventID){
var detailsPromise = eventListService.getDetails(eventID);
detailsPromise.then(function(data){
$scope.eventDetails = data;
}, function(reason){
window.alert(reason);
});
}
}
]);
This way, your events are loaded when the controller first loads, and then you have the option to request a new list by simply passing in a boolean. Getting event details is also handled by an internal promise to give you some error handling without throwing a disruptive error.
Related
I have been searching for an answer to this, and cannot seem to find anything. I have a service, in the first block I am successfully logging a url that I then need to pass into my getData() function. But it comes back undefined, I have tried the method below, and I tried moving the first $http.get into the controller where I am calling it, as well as moving the first $http.get into the getData() function. Am I going about this all wrong?
di.service('testService', function($http) {
$http.get('https://us.api.data/tichondrius?locale=en_US&apikey=xxxxxxxx').
then(function(response) {
var urlToJsonFileUncut = response.data.files[0].url;
console.log(urlToJsonFileUncut);
urlToJsonFile = urlToJsonFileUncut.slice(7);
console.log(urlToJsonFile);
return urlToJsonFile;
});
this.getData = function(urlToJsonFile) {
console.log(urlToJsonFile);
return $http.get('http://localhost:1337/' + urlToJsonFile).
then(function(response) {
console.log(response.data.realms[0].name);
return response.data.realms[0].name;
});
}});
$http is an async request. so you need to chain it inside the first request to ensure the value of first response is available when second request is called.
di.service('testService', function($http) {
var getData = function () {
return $http.get('https://us.api.data/tichondrius?locale=en_US&apikey=xxxxxxxx').
then(function(response) {
var urlToJsonFileUncut = response.data.files[0].url;
console.log(urlToJsonFileUncut);
var urlToJsonFile = urlToJsonFileUncut.slice(7);
console.log(urlToJsonFile);
$http.get('http://localhost:1337/' + urlToJsonFile).
then(function(response) {
console.log(response.data.realms[0].name);
return response.data.realms[0].name;
});
});
}
return { getData: getData; }
});
I would suggest you to use a factory instead of a service
Check out the below code
di.factory('testService', function ($http) {
var variable_name;
var serviceMethodName = function () {
$http.get('https://us.api.data/tichondrius?locale=en_US&apikey=xxxxxxxx').
then(function (response) {
var urlToJsonFileUncut = response.data.files[0].url;
console.log(urlToJsonFileUncut);
urlToJsonFile = urlToJsonFileUncut.slice(7);
console.log(urlToJsonFile);
variable_name = urlToJsonFile; //added
});
}
//modified parameter in below method
var getData = function (variable_name) {
var urlToJsonFile = variable_name; //added
console.log(urlToJsonFile);
return $http.get('http://localhost:1337/' + urlToJsonFile).
then(function (response) {
console.log(response.data.realms[0].name);
return response.data.realms[0].name;
});
}
//Exposes the two methods and accessbile through out the app unless it is modified
return {
serviceMethodName: serviceMethodName,
getData:getData
}
});
I have an factory that gets data from my backend:
as.factory("abbdata", function GetAbbData($http,$rootScope,$routeParams,$q) { //$q = promise
var deffered = $q.defer();
var data = [];
var abbdata = {};
abbdata.async = function () {
$http.get($rootScope.appUrl + '/nao/summary/' + $routeParams['id']).success(function(d) {
data = d.abbData;
deffered.resolve();
});
return deffered.promise;
};
abbdata.data = function() {
return data;
};
return abbdata;
});
A call my factory like this in my controller:
abbdata.async().then(function() {
$scope.abbData = abbdata.data(); //Contains data
});
When I do a console.log($scope.abbData) outside my service call, just underneath, the result Is undifined. Why? Should not the $scope.abbData contain the data from my service after I call it?
EDIT:
You need to pass the data that should be returned into the resolve function like this:
deffered.resolve(data);
EDIT:
To get the data in the controller do this:
abbdata.async().then(function(data) {
$scope.abbData = data; //Contains data
});
Why don't you simply return that value from the async call in the first place?
You can chain promises so by attaching a success handler in your factory and returning a value from that you can simplify your code to:
as.factory("abbdata", function GetAbbData($http,$rootScope,$routeParams) {
return {
async: function () {
return $http.get($rootScope.appUrl + '/nao/summary/' + $routeParams['id']).success(function(d) {
return d.data.abbData;
});
}
}
});
And then use it like
abbdata.async().then(function(data) {
$scope.abbData = data; //Contains data
});
if you console.log($scope.abbData) outside the service call it should show undefined, since the call is asynchronous.
abbdata.async().then(function() {
$scope.abbData = abbdata.data(); //Contains data
});
console.log($scope.abbData) // this should show undefined
The console.log($scope.abbData) just after setting the abbData should show the data
abbdata.async().then(function() {
$scope.abbData = abbdata.data(); //Contains data
console.log($scope.abbData) // this should show the data
});
EDIT
you can use abbData from your service call like for example
angular.module('myApp', []).controller('HomeCtrl', function($scope, abbdata){
var updateUI;
$scope.abbData = [];
abbdata.async().then(function() {
$scope.abbData = abbdata.data(); //Contains data
updateUI();
});
updateUI = function(){
//do something with $scope.abbData
}
});
EDIT 2
On response to your query, I would do something like,
angular.module('myApp', [])
.controller('JobsCtrl', function($scope, $jobService) {
$scope.jobs = [];
$jobService.all().then(function(jobs) {
$scope.jobs = jobs;
});
})
.service('$jobService', function ($q, $http) {
return {
all: function () {
var deferred = $q.defer();
$http({
url: 'http://url',
method: "GET"
}).success(function (data) {
deferred.resolve(data);
}).error(function () {
deferred.reject("connection issue");
});
return deferred.promise;
}
}
});
associated view
<body ng-app = "myApp">
<div ng-controller = "JobsCtrl">
<div ng-repeat="job in jobs track by job.id">
<a href="#/tab/jobs/{{job.id}}" class="item item-icon-right">
<h2>{{job.job_name}}</h2>
<p>DUE DATE: {{job.job_due_date}}</p>
</a>
</div>
<div>
</body>
Here the service an all function which returns a promise, i.e. it will notify when data is fetched.
in the controller the service is called and as soon the service call is resolved the $scope.jobs is assigned by the resolved data.
the $scope.jobs is used in the angular view. as soon as the jobs data are resolved, i.e. $scope.jobs is assigned, the view is updated.
hope this helps
I had a quick look, I have 2 ideas:
First theory: your service is returning undefined.
Second theory: you need to run $scope.$apply();
See this fiddler: https://jsfiddle.net/Lgfxtfm2/1/
'use strict';
var GetAbbData = function($q) {
//$q = promise
var deffered = $q.defer();
var data = [];
var abbdata = {};
abbdata.async = function () {
setTimeout(function() {
//1: set dummy data
//data = [200, 201];
//2: do nothing
//
//3: set data as undefined
//data = undefined;
deffered.resolve();
}, 100);
return deffered.promise;
};
abbdata.data = function() {
return data;
};
return abbdata;
};
var abbdata = GetAbbData(Q)
abbdata.async().then(function() {
console.log(abbdata.data()); //Contains data
});
I have stripped away a lot of dependencies and replaced $q with Q just for my own ease.
In the above example, I first attempted to run the code with dummy data, the console output the expected data, then I tried to not assign the data, and I get an empty array. This is why I assume that if you are seeing 'undefined' you must be explicitly setting the value to 'undefined'.
That aside, I also noticed that you were testing the result by reading directly from $scope. I know that when not inside the angular scope, doing operations on the $scope object does not necessarily happen in a timely manner, and typing $scope.$apply() usually fixes this. Usually, when using $http, angular keeps you in the appropriate scope, but you are creating your own promise using $q so this could be another potential issue.
Finally, the other two answers have pointed out that you are not using promises in the standard way. Although your code works fine, it is not normal to set your data directly onto your service and retrieve it from there. You can keep your service stateless by simply resolving your promise with the data that you want to process in the then method as shown by the answers by Anzeo and Markus.
I hope I was able to find the solution, good luck.
Dipun
as.factory("abbdata", function GetAbbData($http,$rootScope,$routeParams,$q) { //$q = promise
var deffered = $q.defer();
var data = [];
var abbdata = {};
abbdata.async = function () {
$http.get($rootScope.appUrl + '/nao/summary/' + $routeParams['id']).success(function(d) {
data = d.abbData;
deffered.resolve(data);
});
return deffered.promise;
};
abbdata.data = function() {
return data;
};
return abbdata;
});
I have the following situation: When my app first runs, in the .run function, I issue an http request to get a list of "monitors". Once received, it populates a monitors array in a service. This service is used by many controllers to share data. This service is called ZMDataModel. ZMDataModel offers a function called isMonitorsLoaded(). When this returns 1, I know the monitors array is populated (and that the http call is complete)
Now, I have a page called Monitors, the controller for which is zmApp.MonitorCtrl, shown below. What I need to do, in this MonitorCtrl is to basically, right at the start, do an equivalent of:
while (ZMData.isMonitorsLoaded()!=1);
Now I obviously can't do that because it locks my browser up, and the browser never gets a chance to set isMonitorLoaded to 1 in the first place, so it becomes an endless loop.
I understand I need to put in a timeout somehow, but can't quite follow what I need to do in the controller. My controller code is below:
angular.module('zmApp.controllers').controller('zmApp.MonitorCtrl', function($scope, $http, ZMHttpFactory, ZMDataModel) {
$scope.monitors = [];
console.log("***Waiting for Monitors to load before I proceed");
// I can't do a tight loop waiting for ZMDataModel.isMonitorsLoaded
// so some timeout?
$scope.monitors = ZMDataModel.getMonitors();
console.log("I GOT " + $scope.monitors);
$scope.doRefresh = function() {
console.log("***Pull to Refresh");
$scope.monitors = [];
ZMHttpFactory.getMonitors().then(function(data) {
$scope.monitors = data;
$scope.$broadcast('scroll.refreshComplete');
console.log("I GOT " + $scope.monitors);
});
};
});
You can use $rootScope.$emit('eventName') which works like a broadcast of events for anyone who is subscribe to them whit $rootScope.$on('eventName'):
// In your monitor loaded method:
onload: function(){
//Here you can pass optional information to the listeners
// for Example an array of monitor or an object
$rootScope.$emit('MONITORS_LOADED',{
monitors: getMonitors()
});
}
// In your controller:
angular.module('zmApp.controllers').controller('zmApp.MonitorCtrl', function($rootScope, $scope, $http, ZMHttpFactory, ZMDataModel) {
$scope.monitors = [];
$rootScope.$on('MONITOR_LOADED', function(event, data) {
$scope.monitors = data;
// or
// $scope.monitors = ZMDataModel.getMonitors();
console.log("I GOT " + $scope.monitors);
}
$scope.doRefresh = function() {
//...
});
};
});
Why not using a promise which will resolve when your monitor servers is loaded? You can set up your service as:
angular.module('myApp')
.service ('ZMDataModel', ['$http', function ($http) {
function MyServices () {
var _deferred;
var _isLoading;
var me = this;
this.isLoaded = false;
this.load = function (reload) {
if (!_deferred || (!_isLoading && reload)) {
this.isLoaded = false;
_deferred = $q.defer();
_isLoading = true;
// make your call
$http ({get : 'http://your-site.com'})
.then (
function success (rawData) {
me.isLoaded = true;
// parse your data
me.monitors = rawData;
_deferred.resolve(me);
},
function fail (error) {
_deferred.reject(error);
_deferred = null;
if (onFail) {
me.monitors = [];
}
}
)
.finally (
function () {
_isLoading = false;
}
);
}
return _deferred.promise;
};
}
return MyServices;
}
]);
Now you can use your service everywhere like this:
angular.module('zmApp.controllers').controller('zmApp.MonitorCtrl', ['$scope', 'ZMDataModel', function($scope, ZMDataModel) {
$scope.monitors = [];
console.log("***Waiting for Monitors to load before I proceed");
// I can't do a tight loop waiting for ZMDataModel.isMonitorsLoaded
// so some timeout?
ZMDataModel.load ().then (
function (response) {
$scope.monitors = ZMDataModel.monitors;
}
);
$scope.doRefresh = function() {
console.log("***Pull to Refresh");
$scope.monitors = [];
ZMDataModel.load (true).then (
function (response) {
$scope.monitors = ZMDataModel.monitors;
}
);
};
}]);
It doesn't matter if an other controller loads the service first. As long as you use the load function without the 'refresh' flag set to true, the service won't load again
I'm trying to call an API that takes parameters start and count, like this:
function handleSuccess() {
if (!!response.data) {
return (response.data);
} else {
return q.reject(response.data);
}
}
function handleError() {
// do some handling
}
function getData(url, sortBy) {
var count = 10;
var start = 1;
var request = http({
cache: true,
method: "GET",
url: url,
params: {
sortBy: sortBy,
sortOrder: "ASC",
count: count, // e.g. 10
start: start // e.g. 1
}
});
return (request.then(handleSuccess, handleError));
}
The JSON response from the API could contain a "next" link that would give me the URL to call to get the next set of data if there exists more...this is how the pagination works.
What's the best way to do this and concatenate all the data returned into one JSON response?
Assuming some part of the data response is an array, then simply use normal array concat() to combine it with previous pages of data in your handleSuccess() callback.
I've found that a service-oriented way is most useful when trying to get paged data from the same endpoint as it is easy to share services and objects between controllers and directives.
First, I would set up the service layer of your application so that all objects being requested have common, generic methods like so (I highly recommend you use ngResource or RESTAngular or something like that):
angular.module('myModule')
.factory('ApiObject', function($http, $q) {
ApiObject = function ApiObject(attributes) {
angular.extend(this, attributes);
};
ApiObject.query = function(url, parameters) {
var deferred = $q.defer();
$http.get(url, {params: parameters}).then(function(data) {
var results = [];
angular.forEach(data, function(apiObject) {
results.push(new ApiObject(apiObject));
});
deferred.resolve(results);
}, function(error) {
// Do error stuff
deferred.reject(error);
});
return deferred.promise;
};
return ApiObject;
});
Then set up a service to manage your paging data that accepts your generic services as well as parameters and configuration options. Also allow for events to be triggered within the service (see trigger and on methods) so that you know when new results are fetched. I've also written in a way for the results to be automatically concatenated onto the current result set:
angular.module('myModule')
.factory('SearchService', function() {
SearchService = function SearchService(service, params, config) {
this.searchParams = params || {};
this.config = config || {};
this.service = service;
this.results = [];
this.listeners = {};
};
SearchService.prototype.fetch = function(params) {
var _this = this;
this.service.query().then(function(results) {
if(_this.config.concatResults) {
_this.results = _this.results.concat(results);
// You probably should make sure results are unique at this point as that is a common problem with paging a changing API
} else {
_this.results = results;
}
_this.trigger('searchSuccess', _this.results);
});
};
SearchService.prototype.on = function(event, listener) {
(this.listeners[event] = (this.listeners[event] || [])).push(listener);
};
SearchService.prototype.trigger = function(event, payload) {
angular.forEach(this.listeners[event], function(listener) {
listener(payload);
});
};
SearchService.prototype.isLastPage = function() {
//logic here to determine last page
};
SearchService.prototype.nextPage = function() {
if(this.isLastPage()) {
return;
}
if(this.searchParams.page) {
this.searchParams.page++;
} else {
this.searchParams.page = 2;
}
this.fetch();
};
// Write more methods for previousPage, lastPage, firstPage, goToPage... etc.
return SearchService;
});
Then in your controller, you're going to want to instantiate the search service with some default parameters and configuration and then fetch the first page:
angular.module('myModule')
.controller('MyCtrl', function($scope, ApiObject, SearchService) {
$scope.searchService = new SearchService(ApiObject, {page: 1}, {concatResults: true});
$scope.searchService.on('searchSuccess', function(results) {
// Do something with results if you wish, but they'll already be stored in $scope.searchService
});
// Get the first page of data
$scope.searchService.fetch();
});
This is obviously a rough cut with a lot of room for improvement, but I hope this will be a good jumping off point to get you pointed in some sort of angular-style direction. In my experience, this is the best way to abstract out paging logic from the data/request layer in your services.
How would I handle loading data from a service before a page loads exactly?
I've tried to use resolve but the problem is that the service that I need to call hasn't been created yet so I would have to do all of the service logic in the resolve function, which I don't really want to do.
I currently have a working way of handling it, but of course there's the delay between when the view content is loaded and when the service has actually returned the data needed to populate:
myApp.service('WorkshopGetStoreState', ['Transaction', 'ExpServices', 'CacheService', function(Transaction, ExpressServices, CacheService) {
return {
submit:function(defer) {
var requestTemplate = ExpServices.getRequestTemplate('WorkshopGetStoreState');
var request = requestTemplate.template;
var params = {};
params.action = "getStoreAddress";
params.locationInfo = [{'storeId': '004141' }];
for (var attrname in params) {
request.service.serviceRequest[attrname] = params[attrname];
}
Transaction.submit(requestTemplate.url, request, defer);
return defer.promise;
}
};
}]);
myApp.factory('WorkShopFlowService',['WorkShopGetStoreState', function(WorkshopGetStoreState) {
var address={};
return {
getStoreAddress : function() {
return address;
},
setStoreAddress : function(addressParam) {
address = addressParam;
},
init : function(defer){
var promise = WorkshopGetStoreState.submit(defer);
return promise;
}
};
}]);
myApp.controller('WorkshopLandingCtrl', [ '$scope', 'WorkShopFlowService', '$q', function($scope, WorkShopFlowService, $q) {
var defer = $q.defer();
var storedAddress = WorkShopFlowService.getStoreAddress();
if (storedAddress) {
$scope.address = storedAddress;
}
else {
WorkShopFlowService.getDefaultLocation(defer).then(function(result) {
$scope.address = result.serviceResponse.storeInfo[0];
WorkShopFlowService.setStoreAddress($scope.address);
});
}
}]);
Edit: It seems that there is now support for referencing objects as string names in the newer version of AngularJS so now doing it in a resolve is the way to do it.