I've created a service (angular.js) that represents a model in my application:
angular.module("MyApp").factory('ItemService', function() {
this.items = [1, 2, 3];
this.getItems = function() {
return items;
};
this.addItem = function(i) { ... };
this.deleteItem = function(i) { ... };
return this;
});
and my controller:
angular.module('ItemController', function($scope, ItemService) {
$scope.getItems = function() {
return ItemService.getItems();
};
});
Now I want to wire up my service to a REST backend that retrieves, adds, deletes the items permanently. So I want to get rid of the initialization of the items array in my service and want to retrieve it from the server instead. Some things I came across:
The getItems function gets called from Angular multiple times (like 5-6 times when the page loads up and more often when page elements change). So I want to avoid to make each time a REST api call in the getItems function.
I could create an init() method in my service that initializes the items array using a REST request. But how can I ensure that this initialization is done before getItems gets called.
I could create a condition in getItems() that checks if items array is null. If so, initialize it from backend, if not return the saved array.
Note: getItems is not the only function that uses the items array. e.g. addItem, deleteItem etc. use it too.
Many ways to do the same thing, but what is the correct/best way to do this using Angular.js?
I would do it by leveraging the $cacheFactory service that is available as part of the angular ng module. This service will help you put,remove and get objects from cache on the browser side.
Coming to your implementation :
1) Inject $cacheFactory in your service
angular.module("MyApp").factory('ItemService', ['$cacheFactory',function($cacheFactory)
2) Modify your getItems method using the basic caching pattern
var items = $cacheFactory.get('cachedItems')
if (!items){
//make a $http call
items = $http.get to your url
//set the retrieved object in cache
$cacheFactory.put('cachedItems',items);
}
return items;
3) In your other Add/Delete/Update methods i will clear the cache after your server side update is successful
$cacheFactory.remove('cachedItems');
You can find more details about $cacheFactory here : https://docs.angularjs.org/api/ng/service/$cacheFactory
Angular $resources (start from 1.2) https://docs.angularjs.org/api/ngResource/service/$resource
It provides wrapper to interact (CRUD) with your REST service and supports caching out of the box.
Example:
var User = $resource('/user/:userId', {userId:'#id'});
User.get({userId:123}, function(user) {
user.abc = true;
user.$save();
});
Related
I have this simple factory to get products list from data base and also to toggle if the product is favorite or not (based on user interaction).
function factoryProduct($http,$filter) {
var data = {};
data.list = [];
var service = {
getData: _getData,
toggleFav: _toggleFav
};
return service;
function _getData() {
return $http.get('my/url/get.php').then(function(res){
data.list = res;
return res;
});
};
function _toggleFav(value) {
/* data manipulation here... */
return $http.post('my/url/post.php', data).then(function(res){
if (res==1) {
return $filter('filter')(data.list)[index].inFav = value;
};
};
};
}
This is used in multiple views, such as home, category page, favorite list, wishlist, etc.. And thus it's used inside multiple controllers, where I inject the factory and then pass the data to the view.
The toggle function, since it's the same, it's called from within a directive, but is also simple, like this:
scope.toggleFav = function(data, index) {
/*data verification here*/
factoryProduct.toggleFav(value);
}
And in the controller, like this:
function MainCtrl(factoryProduct) {
var vm = this;
factoryProduct.getData().then(function(res){
vm.list = res;
})
}
function CategoryCtrl(factoryProduct) {
var vm = this;
/* category taken from url parameter */
factoryProduct.getData().then(function(res){
$filter('filter')(res, {category: urlParam});
vm.list = res;
})
}
I can get data properly, make the filter and show the correct product list on each view. I also can toggle the favorite and both, the filter and the database, are properly updated.
The problem
The problem starts when I need to change view. For example:
If I'm on the home page, set a product as favorite, go to the contact page and then comeback to the home page, the product I just set as favorite is now as 'non-favorite' item, even if it's favorite in the database and also was updated before going to the contact page.
I'm using this:
var data = {};
data.list = [];
Because I saw many answers saying to have a static array and only manipulate the data inside this array. But it's not working for me.
Any ideas?
There are numerous ways to set up storing data in factory for the duration of the life of the page.
Using a resolve in router can be very helpful to populate data stores. Then when controllers fire the data already exists.
Another one is to check if data exists and if it does return a $q.resolve(data).
If it doesn't exist you return the $http promise that first stores the data and then returns the stored object reference.
Another I just picked up on recently that is helpful if numerous parts of app may ask for same data before a single request has completed.
You can store the original promise made. You can use then() on that promise any time you want within the app lifecycle
app.factory('Factory', function($http){
var dataPromise=null, data=null;
function getData() {
if (!dataPromise) {
console.warn('NEW REQUEST BEING MADE')
dataPromise = $http.get('data.json').then(function(resp) {
// store the data
data = resp.data
// return data stored in factory
return data;
})
}else{
console.info('Existing promise being returned')
}
return dataPromise;
}
return {
getData: getData
}
});
This last approach will prevent 2 parts of the app making 2 simultaneous requests if the first one hasn't completed
A final approach that is used by $resource is to return an empty object that is stored in factory. Then when requests are completed they merge data into that object without breaking the original object reference. Then when that object is passed directly through to views angular watchers will catch changes and do updates
DEMO
This is my task: Get search selections from server when route to a search page or it child pages (eg: #/search/option1). The problem is how to share the selections to all search relative pages and don't request server twice and don't expose selections to root scope?
I don't know weather I describe clearly, not good at it. Thanks for your reading. Appreciate any tip, any.
You could get the result from the server and then reuse the result throughout you application.
Create a factory (or service) that retrieves and stores the values from the server:
app.factory('DataService', function($http) {
var values;
var requestValues = function() {
$http.get("/api/getValues").then(
function(results){
values = results;
});
};
var getValues = function() {
return values;
};
return {
requestValues : requestValues, // this will make a http request and store the result
getValues: getValues // this will call the stored result (without making a http request)
}
});
Now you have two functions in your factory.
requestValues() to make the http request and save the result locally
getValues() to get the locally saved values without making a http request.
Once requestValues() has been called, you should be able to call getValues() from anywhere to get the values without making a new http request.
myApp.controller('MyController', function ($scope, DataService) {
var init = function (){
DataService.requestValues(); // this will make the http request and store the result
$scope.items = DataService.getValues(); // this will get the result
};
var justGetValues = function(){
$scope.items = DataService.getValues(); // this will get the result (without making a http request)
};
});
Now you simply have to call DataService.getValues() whenever you need the values. (You might want to wrap these in a promise. I have refrained from doing this due to simplicity)
How do I update/refresh my $scope.list when a new record is added to the db/collection - storage.set() method - please see comment in the code.
Please see code below.
angular.module("app", [])
.factory('Storage', function() {
var storage = {};
storage.get = function() {
return GetStuffHere();
}
storage.set = function(obj) {
return SetStuffHere(obj);
}
return storage;
})
.controller("MainCtrl", function($scope, Storage) {
$scope.addStuff = function(){
var obj = {
"key1" : "data1",
"key2" : "data2"
};
Storage.set(obj);
// update $scope.list here, after adding new record
}
$scope.list = Storage.get();
});
Here's an approach that stores the received data in the service as an array. It uses promises within the service to either send the previously stored array (if it exists) or makes an HTTP request and stores the response. Using promise of $http, it returns the newly stored array.
This now allows sharing of the stored array across other controllers or directives. When adding, editing, or deleting, it is now done on the stored array in the service.
app.controller('MainCtrl',function($scope, Storage){
Storage.get(function(data){
$scope.items=data
});
$scope.addItem=function(){
Storage.set({name: 'Sue'});
}
})
app.factory('Storage', function($q,$http) {
var storage = {};
storage.get = function(callback) {
/* see if already cached */
if( ! storage.storedData){
/* if not, get data from sever*/
return $http.get('data.json').then(function(res){
/* create the array in Storage that will be shared across app*/
storage.storedData=res.data;
/* return local array*/
return storage.storedData
}).then(callback)
}else{
/* is in cache so return the cached version*/
var def= $q.defer();
def.done(callback);
defer.resolve(storage.storedData);
return def.promise;
}
}
storage.set = function(obj) {
/* do ajax update and on success*/
storage.storedData.push(obj);
}
return storage;
})
DEMO
It's not 100% clear what you want to do, but assuming the storage is only going to update when the user updates it (i.e. there's no chance that two users in different locations are going to be changing the same stuff), then your approach should be to either:
Return a promise containing the newly stored object from the storage service after it's completed, and use .then(function() {...}) to set the $scope.list once it's complete.
You would want to take this approach if the storage service somehow mutates the information in a way that needs to be reflected in the front-end (for example an id used to handle future interaction gets added to the object). Note that $http calls return a promise by default so this isn't much extra code if you're using a web service for storage.
Just add the object to the list on the line after you call it with $scope.list.push(obj)
If you have something that changes on the server side without input from that particular client, then I would look into using a websocket (maybe use socket.io) to keep it up to date.
Solution below will work. However, I am not sure if it is best practice to put this in a function and call when needed (within MainCtrl):
i.e:
On first load
and then after new item added
.controller("MainCtrl", function($scope, Storage) {
$scope.addStuff = function(){
var obj = {
"key1" : "data1",
"key2" : "data2"
};
Storage.set(obj);
// rebuild $scope.list after new record added
$scope.readList();
}
// function to bind data from factory to a $scope.item
$scope.readList = function(){
$scope.list = Storage.get();
}
// on first load
$scope.readList();
});
You have to use
$scope.list = Storage.get;
and in template you can then use i.e.
<ul>
<li ng-repeat="item in list()">{{whateverYouWant}}</li>
</ul>
With this approach you will always have the current state of Storage.get() on the scope
couldn't
return SetStuffHere(obj)
just return the updated list as well? and assign that:
$scope.list = Storage.set(obj);
If this is an API endpoint that returns the single inserted item you could push() it to the $scope.list object.
but maybe I'm missing something you are trying to do...
Updating your backend/Factory stuff is a basic Angular binding done by calling a set/post service. But if you want to automatically refresh your controller variable ($scope.list) based on changes occuring in your factory then you need to create a pooler like function and do something like :
.run(function(Check) {});
.factory('Storage', function() {
var storage = {};
var Check = function(){
storage = GetStuffHere();
$timeout(Check, 2000);
}
// set...
Check();
return storage;
})
.controller("MainCtrl", function($scope, Storage) {
$scope.list = Storage.storage;
Let say I want to retrieve user info from firebase,
and this user info will be displayed in several routes/controllers
Should I $rootScope the returned user info?
or
Call below code in each controller?
firebaseAuth.firebaseRef.child('/people/' + user.id).on('value', function(snapshot) {
$scope.user = snapshot.val();
})
UPDATE
I have a following service with a getUserInfo() function then what is the best way
to use it in several controllers?
calling firebaseAuth.getUserInfo().then() in each controller?
If the user data I have to use in several controller. Why don't I set it $rootScope?
So I don't need to call it again and again in different controllers.
myapp.service('firebaseAuth', ['$rootScope', 'angularFire', function($rootScope, angularFire) {
this.firebaseRef = new Firebase("https://test.firebaseio.com");
this.getUserInfo = function(id) {
var userRef = this.firebaseRef.child('/human/' + id);
var promise = angularFire(userRef, $rootScope, 'user', {});
return promise;
}
});
The point of AngularFire is to keep your javascript data model in sync with Firebase at all times. You don't want to create a new AngularFire promise every time you need to fetch data. You just initialize AngularFire once, and your local data will always be up to date.
myapp.service('firebaseAuth', ['angularFireCollection', function(angularFireCollection) {
this.firebaseRef = new Firebase("https://test.firebaseio.com");
this.initUserInfo = function(id) {
if (!this.userRef) {
this.userRef = this.firebaseRef.child('/human/' + id);
this.userInfo = angularFireCollection(this.userRef);
}
else {
// already initialized
}
}
}]);
Remember that all properties of your service (i.e. everything you assign using the this keyword) are accessible from controllers injected with this service. So you can do things like console.log(firebaseAuth.userInfo) or firebaseAuth.userRef.on('value', function(snap) { ... });
Also, you may eventually want to use the FirebaseAuthClient for your user authentication.
I would recommend creating a service to perform the authentication and store the user data. Then you can inject the service into any controller that needs access to the user.
In an angularjs app, I am attempting to use a custom service to hold the app's main data, "items". However, I would like to bootstrap the data into the service upon the initial page load, in order to avoid a separate ajax request to the server to get it. What would be cleanest way to go about doing this?
Here is a snippet of my service for reference:
app.factory('items', function() {
var itemsService = {},
items = [];
itemsService.list = function() {
return items;
};
itemsService.add = function() {
/* ... */
};
return itemsService;
});
And on the backend I am using node.js + expressjs + jade, so I would be injecting the data into the page using:
!{JSON.stringify(items)}
Put your bootstrapped data from the server into a global JavaScript variable. Have your service assign items to that global variable (or copy the data into items).
How about something like this:
app.run(['items', function (items) {
items.load();
}])
This presumes your items service has a load function (or something like it) that does the actual Ajax work using either $http or $resource.