I would like to make a service which loads a JSON file and the provide some methods to work with the result.
Blog.service('ArticleService', ['$http', function ($http) {
this.loadArticles = function() {
return $http.get('data/articles.json');
};
this.getArticles = function () {
// return the json
};
this.getArticle = function (id) {
// work with the json
};
}]);
And the controller:
Blog.controller('BlogController', function ($scope, ArticleService) {
console.log(ArticleService.getArticles());
console.log(ArticleService.getArticle(1));
});
I'd like to cache the result of my request and then work with this result in my methods, getArticles and getArticle.
Following your comment, if you want to do only 1 request, then I'll suggest saving the $http promise in a variable in your service and returning that variable in getArticles.
Blog.service('ArticleService', ['$http', function ($http) {
this.loadArticles = function() {
return $http.get('data/articles.json');
};
var articles;
this.getArticles = function () {
if (!articles) {
articles = this.loadArticles();
}
return articles;
};
this.getArticle = function (id) {
// work with the json
};
}]);
Or better yet, load the articles variable on init:
Blog.service('ArticleService', ['$http' '$q', function ($http, $q) {
var articles = (function() {
return $http.get('data/articles.json');
})();
this.getArticles = function () {
return articles;
};
this.getArticle = function (id) {
// Return a promise that will be resolved with the correct article.
var deferred = $q.defer();
articles.then(function (arts) {
arts.forEach(function (art) {
if (art.id === id) {
deferred.resolve(art);
}
});
});
return deferred.promise;
};
}]);
Angular used to unwrap promises, but that was deprecated because it proved to be troublesome and very "magical" (leading to misunderstandings). See https://github.com/angular/angular.js/issues/5153
Alternatively, you could use $http's cache property:
Blog.service('ArticleService', ['$http', function ($http) {
this.getArticles = function() {
return $http.get('data/articles.json', {cache: 'myCache'});
};
this.getArticle = function (id) {
return $http.get('data/articles.json', {cache: 'myCache'}).then(function(response) {
// Parse the response, return the article in question.
});
};
}]);
Blog.service('ArticleService', ['$http', '$q', function ($http, $q)
{
var self = this;
this.loadArticles = function ()
{
var deffered = $q.defer();
$http.get("data/articles.json").then(
function (result)
{
deffered.resolve(result);
},
function ()
{
deffered.reject()
}
);
return deffered.promise;
};
this.getArticles = function ()
{
return self.loadArticles;
};
this.getArticle = function (id)
{
// some stuff to retrieve the object
};
}]);
then in your controller
Blog.controller('BlogController', function ($scope, ArticleService)
{
ArticleService.getArticles().then(function (data)
{
console.log(data);
}, then(function ()
{
alert("error")
})
);
});
});
If you want your cache to persist from session to session, then see user3904's answer.
If it is acceptable for the cache to die with the page, then you might consider the following approach :
Blog.service('ArticleService', ['$http', '$q', function ($http, $q) {
var articles = {};
var loadArticles = function () {
articles.multi = $http.get("data/articles.json").done(function(a) {
articles.multi = a;
});
return articles.multi;
};
var loadArticle = function (id) {
articles[id] = $http.get("data/articles.json/" + id).done(function(a) {
articles[id] = a;
});
return articles[id];
};
this.getArticles = function () {
return articles.multi ? $q.when(articles.multi) : loadArticles();
};
this.getArticle = function(id) {
id = '' + id;
return articles[id] ? $q.when(articles[id]) : loadArticle(id);
};
}]);
Explanation
The cache is the js plain object articles.
The two load...() functions are private - assuming, as seems reasonable, that they are to be called only by their corresponding get...() functions. (This is not particularly significant to the overall solution).
On first call, the two loadArticles() caches a promise, which is overwritten with the data delivered by the promise, when that data arrives. The promise is temporarily cached for one reason - in case a second request is made before the data is returned from the server. The $q.when(...) in this.getArticles() ensures that loadArticles() returns promise-wrapped data, regardless of what is currently cached - promise or data.
this.getArticle() works similarly to this.getArticles(), but accepts (and passes) an id.
This strategy is designed to be storage efficient. By caching only the data long term, the (small) overhead of promise wrappers is avoided. This might be an issue if users are likely to request many individual articles per session.
Storage efficiency is bought at the cost of slightly less efficient delivery of cached articles. $q.when(...) will take some extra clock cycles to complete.
It should be quite simple to adapt the code to work the other way round - ie to optimise for delivery at the cost of storage efficiency - ie to store promises long term and avoid the need for $q.when(...).
Both approaches will work and for most applications it's pretty academic which one is adopted.
Related
I've been struggling with this for a few days now and can't seem to find a solution.
I have a simple listing in my view, fetched from MongoDB and I want it to refresh whenever I call the delete or update function.
Although it seems simple that I should be able to call a previously declared function within the same scope, it just doesn't work.
I tried setting the getDispositivos on a third service, but then the Injection gets all messed up. Declaring the function simply as var function () {...} but it doesn't work as well.
Any help is appreciated.
Here's my code:
var myApp = angular.module('appDispositivos', []);
/* My service */
myApp.service('dispositivosService',
['$http',
function($http) {
//...
this.getDispositivos = function(response) {
$http.get('http://localhost:3000/dispositivos').then(response);
}
//...
}
]
);
myApp.controller('dispositivoController',
['$scope', 'dispositivosService',
function($scope, dispositivosService) {
//This fetches data from Mongo...
$scope.getDispositivos = function () {
dispositivosService.getDispositivos(function(response) {
$scope.dispositivos = response.data;
});
};
//... and on page load it fills in the list
$scope.getDispositivos();
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo);
$scope.getDispositivos(); //it should reload the view here...
$scope.dispositivo = '';
};
$scope.removeDispositivo = function (id) {
dispositivosService.removerDispositivo(id);
$scope.getDispositivos(); //... here
};
$scope.editDispositivo = function (id) {
dispositivosService.editDispositivo(id);
$scope.getDispositivos(); //... and here.
};
}
]
);
On service
this.getDispositivos = function(response) {
return $http.get('http://localhost:3000/dispositivos');
}
on controller
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo).then(function(){
$scope.getDispositivos(); //it should reload the view here...
$scope.dispositivo = '';
});
};
None of the solutions worked. Later on I found that the GET request does execute, asynchronously however. This means that it loads the data into $scope before the POST request has finished, thus not including the just-included new data.
The solution is to synchronize the tasks (somewhat like in multithread programming), using the $q module, and to work with deferred objects and promises. So, on my service
.factory('dispositivosService',
['$http', '$q',
function($http, $q) {
return {
getDispositivos: function (id) {
getDef = $q.defer();
$http.get('http://myUrlAddress'+id)
.success(function(response){
getDef.resolve(response);
})
.error(function () {
getDef.reject('Failed GET request');
});
return getDef.promise;
}
}
}
}
])
On my controller:
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo)
.then(function(){
dispositivosService.getDispositivos()
.then(function(dispositivos){
$scope.dispositivos = dispositivos;
$scope.dispositivo = '';
})
});
};
Being my 'response' object a $q.defer type object, then I can tell Angular that the response is asynchronous, and .then(---).then(---); logic completes the tasks, as the asynchronous requests finish.
This question already has answers here:
Share data between AngularJS controllers
(11 answers)
Closed 2 years ago.
i have tow controller in angularjs. if one controller change data other controller display updated data. in fact first controller has a event that it occur second controller display it. for this propose i wrote a service. this service has tow function. here is my service code.
app.service('sharedData', function ($http) {
var data=[]
return {
setData: function () {
$http.get('/getData').success(function(response){
data = response;
})
},
getData: function(){
return data;
}
}
});
in first controller
app.controller("FirstController", function ($scope, $http,sharedData)
{
$scope.handleGesture = function ($event)
{
sharedData.setData();
};
});
in second controller:
app.controller("SecondController", function ($scope,sharedData) {
var data=[];
data = sharedData.getData();
}
);
in first controller setData work with out any problem but in second controller not work correctly. how to share data dynamically between tow controllers?
You are on the right track with trying to share data between controllers but you are missing some key points. The problem is that SecondController gets loaded when the app runs so it calls sharedData.getData() even though the call to setData in the firstController does not happen yet. Therefore, you will always get an empty array when you call sharedData.getData().To solve this, you must use promises which tells you when the service has data available to you. Modify your service like below:
app.service('sharedData', function ($http, $q) {
var data=[];
var deferred = $q.defer();
return {
setData: function () {
$http.get('/getData').success(function(response){
data = response;
deferred.resolve(response);
})
},
init: function(){
return deferred.promise;
},
data: data
}
})
And the secondController like this:
app.controller("SecondController", function ($scope,sharedData) {
var data=[];
sharedData.init().then(function() {
data = sharedData.data;
});
});
For more info on promises, https://docs.angularjs.org/api/ng/service/$q
You had multiple syntax problems, like service name is SharedData and you using it as SharedDataRange, the service is getting returned before the get function.
What I have done is corrected all the syntax errors and compiled into a plunkr for you to have a look. Just look at the console and I am getting the data array which was set earlier in the setter.
Javascript:
var app = angular.module('plunker', []);
app.controller("FirstController", function ($scope,sharedDateRange)
{
sharedDateRange.setData();
});
app.controller("SecondController", function ($scope,sharedDateRange) {
var data=[];
data = sharedDateRange.getData();
console.log(data);
});
app.service('sharedDateRange', function ($http) {
var data=[];
return {
setData: function () {
data = ['1','2','3'];
}
,
getData: function(){
return data;
}
}
});
Working Example
If you want to keep sharedDataRange as the variable name and service name as sharedData have a look at this example
javascript:
var app = angular.module('plunker', []);
app.controller("FirstController", ['$scope','sharedData', function ($scope,sharedDateRange)
{
sharedDateRange.setData();
}]);
app.controller("SecondController", ['$scope','sharedData', function ($scope,sharedDateRange) {
var data=[];
data = sharedDateRange.getData();
console.log(data);
}]);
app.service('sharedData', function ($http) {
var data=[];
return {
setData: function () {
data = ['1','2','3'];
}
,
getData: function(){
return data;
}
}
});
You can bind the data object on the service to your second controller.
app.service('sharedData', function ($http) {
var ret = {
data: [],
setData: function () {
$http.get('/getData').success(function(response){
data = response;
});
}
};
return ret;
});
app.controller("FirstController", function ($scope, sharedData) {
$scope.handleGesture = function () {
sharedData.setData();
};
});
app.controller("SecondController", function ($scope, sharedData) {
$scope.data = sharedData.data;
});
What you need is a singleton. The service sharedData needs to be a single instance preferably a static object having a static data member. That way you can share the data between different controllers. Here is the modified version
var app = angular.module('app', []);
app.factory('sharedData', function ($http) {
var sharedData = function()
{
this.data = [];
}
sharedData.setData = function()
{
//$http.get('/getData').success(function(response){
this.data = "dummy";
//})
}
sharedData.getData = function()
{
return this.data;
}
return sharedData;
})
.controller("FirstController", function ($scope, $http,sharedData)
{
sharedData.setData();
})
.controller("SecondController", function ($scope,sharedData) {
$scope.data=sharedData.getData();
});
I have removed the event for testing and removed the $http get for now. You can check out this link for a working demo:
http://jsfiddle.net/p8zzuju9/
One of my promises during feeds.forEach cycle ends up with error. Maybe because this in LoadData never executes line $rootScope.links = urls; which is inside then. How to fix it?
app.service('LoadData', ['FeedService', 'EntryStateUrlService', '$q',
function(FeedService, EntryStateUrlService, $q) {
this.loadData = function(feedSrc) {
FeedService.parseFeed(feedSrc).then(EntryStateUrlService.getEntryStateUrl).then(function(data) {
console.log(data);
$rootScope.links = data;
});
}
}
]);
app.service('FeedService', function($http, $q) {
this.parseFeed = function(url) {
var deferred = $q.defer();
$http.jsonp('//ajax.googleapis.com/ajax/services/feed/load?v=1.0&callback=JSON_CALLBACK&q=' + encodeURIComponent(url))
.success(function(res) {
deferred.resolve(res.data.responseData.feed.entries);
}).error(function() {
deferred.reject();
});
return deferred.promise;
}
});
app.service('EntryStateUrlService', ['$state', '$q',
function($state, $q) {
this.getEntryStateUrl = function(feeds) {
var deferred = $q.defer();
var idx = 0,
promises = [],
promise = null;
feeds.forEach(function(e) {
promise = $http.jsonp(e.link).success(function(data) {
/*stuff*/
e['entryStateUrl'] = data.link; // UPDATED
deferred.resolve(data);
});
promises.push(promise);
}); //forEach
return $q.all(promises);
}
}
]);
UPDATE
I don't really understand how $q.all as a big promise object composite of many other promises will deliver data to service LoadData....
UPDATE2
It seems like since one promise fails (because the one of the urls is invalid) $q.all fails and never makes to then(). How to go around that? I need to get all data from all successful promises.
One thing is you'll need to inject $rootScope in order to use it:
app.service('LoadData', ['FeedService', 'EntryStateUrlService', '$q', '$rootScope',
function(FeedService, EntryStateUrlService, $q, $rootScope) {
//...
}
]);
Also in the Q library there's a method called allSettled that will allow you to view all the promise results even if one fails. The angular $q does not have this method, but several people have found it useful so they've created extensions for it. Here's an example of one on a Github Gist:
angular.module('qAllSettled', []).config(function($provide) {
$provide.decorator('$q', function($delegate) {
var $q = $delegate;
$q.allSettled = function(promises) {
return $q.all(promises.map(function(promise) {
return promise.then(function(value) {
return { state: 'fulfilled', value: value };
}, function(reason) {
return { state: 'rejected', reason: reason };
});
}));
};
return $q;
});
});
Using this method will return all results, whether rejected or not. The returned object will have a state property that will tell you the status of the promise:
return $q.allSettled(promises);
Then you can check the status of each promise and add it to the links as needed:
FeedService.parseFeed(feedSrc)
.then(EntryStateUrlService.getEntryStateUrl)
.then(function(results) {
$scope.links = [];
results.forEach(function (result) {
if (result.state === "fulfilled") {
$scope.links.push(result.value);
} else {
var reason = result.reason;
}
});
});
Plunker Demonstration
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 am an angular newbie, I am building an application, one thing really puzzling me is there are couple of ways of defining a service, and I read more from this link: How to define service
then it seems there is no big difference among the ways of defining a service.
but I just noticed one difference which I think is different:
see this service I get from here http://jsfiddle.net/2by3X/5/
var app = angular.module('myApp', []);
app.service('test', function($timeout, $q) {
var self = this;
this.getSomething = function() {
return self.getData().then(function(data) {
return self.compactData(data);
});
};
this.getData = function() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve("foo");
}, 2000);
return deferred.promise;
};
this.compactData = function(data) {
var deferred = $q.defer();
console.log(data);
$timeout(function() {
deferred.resolve("bar");
}, 2000);
return deferred.promise;
};
});
if I define this service using "factory" like below, one function cannot call other functions of the service.
app.factory('test', function($timeout, $q) {
return {
getSomething : function() {
return getData().then(function(data) {
return compactData(data);
});
},
getData : function() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve("foo");
}, 2000);
return deferred.promise;
},
compactData : function(data) {
var deferred = $q.defer();
console.log(data);
$timeout(function() {
deferred.resolve("bar");
}, 2000);
return deferred.promise;
},
};
});
I will get this in the browser console:
[08:41:13.701] "Error: getData is not defined
.getSomething#http://fiddle.jshell.net/_display/:47
Ctrl1#http://fiddle.jshell.net/_display/:75
invoke#http://code.angularjs.org/1.0.0/angular-1.0.0.js:2795
instantiate#http://code.angularjs.org/1.0.0/angular-1.0.0.js:2805
Can anyone explain this? thanks.
you have two big problems there:
the factory returns an object with incorrect syntax.
javascript scope of variables is functional
That is,
You should be returning an object like {key: value, key: value}
values can be functions. however, you return {key = value, key = value}
First fix:
return {
getSomething : function() {...},
getData : function...
}
Secondly, not being able to call functions is normal. See this jsfiddle. I mocked everything. You can call one of the functions returned by the service. However, when from getSomething try to call getData, you get "undefined":
app.factory('testSO', function () {
return {
getSomething: function () {
console.log('return getsomething');
getData();
},
getData: function () {
console.log('return getData');
}...
This would be the same as declaring everything in the scope of the factory function and return references see in jsfiddle:
app.factory('testSO', function () {
var getSomething = function () {
console.log('return getsomething');
};
...
return {
getSomething: getSomething,
...
}
and now you can call local functions as shown in the final version of the jsfiddle:
app.factory('testSO', function () {
var getSomething = function () {
console.log('return getsomething');
getData();
};
...
The original service has something important in it: var self = this; . Some people use var that = this. It's a workaround for an error in ECMA.
In the case of the original code, it's used to "put everything in one object". Functions (properties that happen to be functions) in self need a reference to know where the function you want to call is. Try it yourself here http://jsfiddle.net/Z2MVt/7/