Assuming my service is returning a promise from a $resource get, I'm wondering if this is the proper way to cache data. In this example, after hitting the back arrow and returning to the search results, I don't want to query the webserver again since I already have them. Is this the proper pattern to handle this situation? The example below is querying the Flixter (Rotten Tomatoes) Api.
Boilded down code:
Controller:
function SearchCtrl($scope, $route, $routeParams, $location, DataService) {
DataService.search($routeParams.q).then(function(data){
$scope.movies = data.movies;
});
}
Service:
angular.module('myApp.services', []).
factory('DataService', ['$q', '$rootScope', 'JsonService', function ($q, $rootScope, JsonService) {
var movie = {};
var searchResults = {};
var searchq = '';
var service = {
search: function(q) {
var d = $q.defer();
// checking search query, if is the same as the last one,
//resolve the results since we already have them and don't call service
// IS THIS THE CORRECT PATTERN
if (q==searchq) {
d.resolve(searchResults);
} else {
// returns a $resource with defined getdata
JsonService.search.movieSearch(q, 20, 1).getdata(function(data){
searchResults = data;
searchq = q;
d.resolve(searchResults);
});
}
return d.promise;
},
getSearchResults: function() {
return searchResults;
}
};
return service;
}]);
I can't provide a working example as it would expose my API key.
I've faked out the actual ajax request but I think the general idea should apply, you can see the full demo here
Here is the controller, it just executes the search and then sets the results:
myApp.controller('MyCtrl', function($scope, DataService) {
$scope.search = function(){
DataService
.search($scope.q)
.then(function(response){
$scope.fromCache = response.fromCache;
$scope.results = response.results;
});
};
});
In the DataService I am just saving results into an object keyed off the query. It is simplistic but hopefully will get you started. You could save it in html5 storage or something if you want something like that.
You will need to put in your actual ajax call here, but the principle remains.
myApp.factory('DataService', function($q){
var resultsCache = {};
return {
search: function(query){
var deferred = $q.defer();
if (resultsCache[query]) {
resultsCache[query].fromCache = true;
}
else {
resultsCache[query] = {results: [{name: 'result one'}, {name: 'result two'}]};
}
deferred.resolve(resultsCache[query]);
return deferred.promise;
}
};
});
Hope that helps
Related
(function () {
angular.module("app").controller('DashboardController', ['$q', 'dashboardService', function ($scope, $q,dashboardService) {
var DashboardController = this;
dashboardService.loadFromServer(DashboardController );
console.log("DashboardController ", DashboardController);
}])
})();
angular.module("app").service('dashboardService', ['$http', '$q', function ($http, $q) {
return {
loadFromServer: function (controller) {
var getDashboardEntries = $http.get('http://someUrl');
var getEmailData = $http.get('http://someOtherUrl');
var getSidebarData = $http.get('http://yetAnotherUrl');
return $q.all([getDashboardEntries, getSidebarData, getEmailData])
.then(function (results) {
controller.dashboardData = results[0].data;
controller.chartData = results[1].data;
controller.emailData = results[2].data;
});
},
};
}]);
1.The service returns the three bits of data and this is the results when logged using:
console.log("DashboardController ", DashboardController);
When I try to drill down on the data in this manner it logs "undefined"
console.log("DashboardController "DashboardController.dashboardData);
console.log("DashboardController "DashboardController.chartData);
console.log("DashboardController "DashboardController.emailData);
Do you realize that console.log is executed right after invoking loadFromServer before the server has chance to respond and promise resolves? The actual order is:
loadFromServer
console.log
promise success method - where you actually have your data
Change your controller's code to this:
dashboardService.loadFromServer(DashboardController ).then(function() {
console.log("DashboardController ", DashboardController);
});
What would be even better is to construct some object from parts of responses and assign it in the controller itself - not the service. In current implementation if you wanted to have another controller then service would assign response parts to same fields. I'd propose sth like this:
return $q.all([getDashboardEntries, getSidebarData, getEmailData])
.then(function (results) {
var data = {
dashboardData = results[0].data;
chartData = results[1].data;
emailData = results[2].data;
};
return data;
});
and then in controller:
dashboardService.loadFromServer().then(function(data) {
DashboardController.dashboardData = data.dashboardData;
DashboardController.chartData = data.chartData;
DashboardController.emailData = data.emailData;
});
In this solution the controller decides what to do with data, not the other way around.
So I have created a Notifications service in AngularJS and well I'm not getting any meaning full data back in my $scope.Notifications variable.
I can see the service is being called and running at the correct interval, and the correct data is being returned from the API:
[{"id":1,"user_id":1,"content":"You have new mail: Test","read":null,"type":"mail","deleted_at":null,"created_at":"2015-06-23 20:16:38","updated_at":"2015-06-23 20:16:38"},{"id":2,"user_id":1,"content":"You have new mail: Test","read":null,"type":"mail","deleted_at":null,"created_at":"2015-06-23 20:16:38","updated_at":"2015-06-23 20:16:38"},{"id":3,"user_id":1,"content":"You have new mail: Test","read":null,"type":"mail","deleted_at":null,"created_at":"2015-06-23 20:16:38","updated_at":"2015-06-23 20:16:38"}]
Essentially, all I need from this is a simple array of the users notifications.
Here is my service:
app.services.Notifications = ['$http', '$timeout', function($http, $timeout){
var timeoutId;
var notificationService = this;
function checkForNotifications(){
console.log('checking')
return $http.get('/api/notifications')
.then(function(res){
return res.data.filter(function(notification){
return notification.unread === true;
})
})
.then(function(unreadNotifications){
//fake for effect
notificationService.count = Math.floor(Math.random() * (100 - 1)) + 1;
//notificationService.count = unreadNotifications.length;
})
.then(waitAndCheck)
}
function waitAndCheck(){
return $timeout(function(){
checkForNotifications()
},5000);
}
return {
Notifications: waitAndCheck()
}
}];
And my controller:
app.controllers.notificationsController = ['$scope', 'Notifications', function($scope, Notifications) {
$scope.Notifications = Notifications;
}];
If I console log $scope.Notifications in the controller I being return this:
Object {Notifications: d}Notifications: d$$state: Objectstatus: 1value: undefined__proto__: Object$$timeoutId: 1__proto__: Object__proto__: Object
Set your $scope.Notifications = Notifications.Notifications;
app.controllers.notificationsController = ['$scope', 'Notifications', function($scope, Notifications) {
$scope.Notifications = Notifications.Notifications;
}];
Currently the thing which you are getting in console is nothing but a promise object which is conceptually correct. If you want the data from it then you use resolve that promise chain using .then function on Notification service.
Notifications.Notifications.then(function(data){
$scope.Notifications = data;
})
What I am trying to achieve:
Pass the parameter specId from the controller to the factory
In the factory: perform a $http.get to get JSON data
Return the JSON data to the controller
Displaying the information by assigning it to the $scope.formData
I tried many different ways but it only returns undefined. Before I created the factory I performed the $http.get directly in the controller without any issues, but now I am trying to structure the app in a better way.
Factory:
app.factory("dataFactory", function($http) {
var factory = {};
factory.getSpec = function(specId) {
return $http.get('getSpec.aspx?specId=' + specId)
};
return factory;
});
Controller
app.controller('EditSpecController', function ($scope, $stateParams, $http, dataFactory) {
$scope.specId = $stateParams.specId;
$scope.formData = [];
if($scope.specId) { //If EDIT MODE
dataFactory.getSpec($scope.specId).then(function(response) {
$scope.formData = response.data;
$scope.use_unit = response.data.use_unit;
});
}
As you noticed $http returns promise already, so you should do something more like this
factory.getSpec = function(specId) {
return $http.get('getSpec.aspx' + specId)
};
and then in controller
dataFactory.getSpec().then(function(response) {
$scope.formData = response.data;
});
I really like the clean (and I think easy to follow) way that promises were autounwrapped:
$scope.myData = DataService.query({something:"etc"}); // done;
And I really don't care for what seems to be the standard way of doing it now, without the automatic unwrapping:
DataService.query({something:"etc"}).$promise.then(function (data){
$scope.myData = data;
});
What I'd like to see is something like this:
$scope.pulseData = $scope.setPromise(CitsciAnalytics.pulse({
projId:"yardmap"
}));
But I can't see how to make that happen. The closest I got was:
$scope.pulseData = $scope.setPromise("pulseData", CitsciAnalytics.pulse({
projId:"yardmap"
}));
Using a function added to the root scope:
.run(["$rootScope", "$log", function ($rootScope, $log) {
//parent method to avoid promise unwrapping boilerplate
$rootScope.setPromise = function (scopeVar, promise) {
if (arguments.length === 2 && promise && promise.$promise) {
var scope = this;
promise.$promise.then(function (data){
scope[scopeVar] = data;
});
} else {
$log.error("$rootScope.setPromise has invalid arguments");
}
};
}]);
but I don't like the unDRY requirement of having to pass the scope variable name as an additional string. Has anyone else tackled this, or see a way to do this more cleanly?
First of all you don't need to use
DataService.query({something:"etc"}).$promise.then(function(data){
$scope.myData = data;
});
which clearly refers to a $resource, because $resource will return an empty array or object and fill it with data as they arrive.
So, with a $resource class, you can still use
$scope.myData = DetaService.query(...);
$resource's paradigm is also a good approach to follow in your own "data-fetching" services: Return and empty array and fill it with data as they arrive.
E.g.:
.factory('DataService', function ($http) {
function query(params) {
var data = [];
$http.get('/some/url/with/params').then(function (response) {
response.data.forEach(function (item) {
data.push(item);
});
});
return data;
}
return {
query: query
};
});
.controller('someCtrl', function ($scope, DataService) {
$scope.data = DataService.query({...});
If you have to use thrid-party services that return a promise, you can implement your generic function to offer similar functionality:
.run(function ($rootScope) {
$rootScope.promiseToArray = function (promise) {
var arr = [];
promise.then(function (data) {
data.forEach(function (item) {
arr.push(item);
});
});
return arr;
};
});
.controller('someCtrl', function ($scope, ThirdPartyService) {
$scope.data = $scope.promiseToArray(ThirdPartyService.fetchData());
});
See, also, this short demo.
The above samples are just for illustration purposes and not production-ready code.
I a real-world app, you would need to gracefully handle exceptions, rejections etc.
We can re-enable unwrapping if we really want to by setting the option to true in a .config() function:
.config(function($parseProvider) {
$parseProvider.unwrapPromises(true) ;
});
I'm using a service to make user data available to various controllers in my Angular app. I'm stuck trying to figure out how to use the $http service to update a variable local to the service (in my case "this.users"). I've tried with and without promises. The server is responding correctly.
I've read several excellent articles for how to use $http within a service to update the scope of a controller. The best being this one: http://sravi-kiran.blogspot.com/2013/03/MovingAjaxCallsToACustomServiceInAngularJS.html. That does not help me though because it negates the benefits of using a service. Mainly, modifying the scope in one controller does not modify throughout the rest of the app.
Here is what I have thus far.
app.service('UserService', ['$http', function($http) {
this.users = [];
this.load = function() {
var promise = $http.get('users.json')
.success(function(data){
// this.users is undefined here
console.log(this.users);
}
};
promise.then(function() {
// this.users is undefined here
console.log('this.users');
});
}]);
Any help is greatly appreciated. Thank you.
Try using
var users = [];
rather than
this.users = [];
and see what
console.log(users);
outputs in each of those cases.
Your service is oddly defined, but if you have a return in it you can access it from any controller:
app.service('UserService', ['$http', function($http) {
var users = [];
this.load = function() {
var promise = $http.get('users.json')
.success(function(data){
// this.users is undefined here
console.log(users);
users = data.data;
}
};
return {
getUsers: function(){
return users;
}
}
}]);
so in your controller, you can use:
var myUsers = UserService.getUsers();
UPDATE to use a service correctly here, your service should return a promise and the promise should be accessed in the controller: Here's an example from another answer I gave
// your service should return a promise
app.service('PickerService', [$http', function($http) {
return {
getFiles: function(){
return $http.get('files.json'); // this returns a promise, the promise is not executed here
}
}
}]);
then in your controller do this:
PickerService.getFiles().then(function(returnValues){ // the promise is executed here as the return values are here
$scope.myDirectiveData = returnValues.data;
});
this does not have scope anymore where you are trying to use it do this instead:
app.service('UserService', [$http', function($http) {
var users = [];
this.load = function() {
var promise = $http.get('users.json')
.success(function(data){
console.log(users);
}
};
promise.then(function() {
console.log(users);
});
}]);
all local variables to a service should just be vars if you assign them to this as a property than they will be included every time the service is injected into a controller which is bad practice.
I think what your asking for is a solution along the lines of defining your service like this:
angular.module('app')
.service('User', function($http, $q) {
var users = null;
var deferred = $q.defer()
return {
getUsers: function() {
if(users) {
deferred.resolve(users);
} else {
$http.get('users.json');
.success(function(result) {
deferred.resolve(result);
})
.error(function(error) {
deferred.reject(error);
});
}
return deferred.promise;
}
};
});
Then in one Each controller you would have to do this:
angular.module('app')
.controller('ACtrl', function($scope, User) {
User.getUsers().then(function(users) {
// Same object that's in BCtrl
$scope.users = users;
});
});
angular.module('app')
.controller('BCtrl', function($scope, User) {
User.getUsers().then(function(users) {
// Same object that's in ACtrl
$scope.users = users;
});
});
NOTE: Because the deferred.promise the same promise passed to all controllers, executing deferred.resolve(users) in the future will cause all then success callbacks in each of your controllers to be called essentially overwriting the old users list.
All operations on the list will be noticed in all controllers because the users array is a shared object at that point. This will only handle updates to the user list/each individual user on the client side of your application. If you want to persist changes to the server, you're going to have to add other $http methods to your service to handle CRUD operations on a user. This can generally be tricky and I highly advise that you check out ngResource, which takes care of basic RESTful operations