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
Related
I want to display view once the list of content is retrieved from the database display it in the view.
On a high level what I am doing now works more or less but upon first access the previous results appears to be cached or still saved in my storage service.
This is what the service looks like.
function StorageService() {
var opportunities;
var getOpportunities = function() {
return opportunities;
}
var setOpportunities = function(data) {
opportunities = data;
}
return {
getOpportunities: getOpportunities,
setOpportunities: setOpportunities
}
}
So when I click on the tab to getOpportunities I go direct to the view first and the load the data.
$scope.getAllOpportunities = function() {
$state.go("app.opportunities");
communicationService.getOpportunitiesAll().then(function(data) {
StorageService.setOpportunities(data);
if (!data.length) {
$("#noShow").html("There are no opportunities");
}
}, function(error) {})
}
once the view is rendered I retrieve the results and bind it to the view.
controller('OpportunitiesController', function($scope) {
$scope.$on("$ionicView.afterEnter", function(scopes, states) {
$scope.opportunities = StorageService.getOpportunities();
});
What I am doing here also feels a bit wrong.
Is there a better way or a way that I can improve on the existing.
I want to load the view and the replace the loader with the data once the data is ready.
Thanks in advance
You should resolve the promise in the route, using the resolve property. That way, the data will always be available when the controller is instantiated.
https://toddmotto.com/resolve-promises-in-angular-routes/
Unless the resource is huge and you want to show som loading animation while getting the data. Then it would probably be more proper to just get the data in the controller.
controller('OpportunitiesController', function($scope) {
communicationService.getOpportunitiesAll().then(function(response){
$scope.opportunities = response;
})
});
html:
<span ng-if="!opportunities">Getting stuff</span>
<span ng-if="opportunities">Stuff fetched</span>
Also, there is no use to have getter and setters in the service. Javascript objects are passed by reference so you can just expose the property directly.
Currently developing an app which requires pulling in data using an API, and running into the following problem.
In my controller I pull in a list of products from an API, add them to $rootScope, than loop over in order to display a list of all products.
When viewing an individual product, I loop over the list and display the product with the requested ID into a view like so
getProduct: function(productID) {
var products = $rootScope.products;
for (var i =0; i<products.length; i++) {
if (products[i].id == parseInt(productID)) {
return products[i];
}
}
}
This all works fine, except for if you visit the individual product URL without first going through the main list page, the list of products is unavailable as the call to the API is never made.
What is the best way to about resolving this?
Creating another separate API call from this view seems very messy and overly complicated and I was wondering if there is a standard or better approach!
I would create an angular service for product that has a private array for products with a getter & setter. This will allow you to stay away from using $rootScope to pass data between controllers (which is a good thing).
Then have a function that makes a call to the API for each product detail page. You can then pass the ID to the page in the URL using $routeParams
By putting this all in a service it becomes reusable to all your controllers and quite clean.
var myModule = angular.module('myModule', []);
myModule.service('productService', function() {
var products = [1,2,3,4,5];
this.addProduct = function(id){
products.push(id);
}
this.getProducts = function(){
return products;
}
this.getProductDetail = function(id){
//$http.get(...
}
});
Edit: example implementation as requested
// Don't forget to inject your service into your controller
myModule.controller('myCtrl', ['productService', function (productService) {
// Add a product to your array
productService.addProduct(6); //products = [1,2,3,4,5,6]
// Retrieve products
var myProducts = productService.getProducts(); //myProducts = [1,2,3,4,5,6]
// You can do this a few different ways but assuming
// getProductDetail returns a promise from its $http call
productService.getProductDetail(4).then(function (productDetail) {
$scope.productDetails = productDetail;
});
}]);
Came up with a solution based on your answer (removing the dependency on $rootScope really helped) so marking your answer as good to go but wanted to share what I came up with in case anyone finds it useful!
Essentially, in the product detail controller which deals with displaying an individual product we say, if we already have the product information within the application, pull it from that, and, if we don't then make a call to the API.
I have an extra service called config which I need to access the my API in case anyone is wondering whats going on there :)
Service
productsApp.factory('$productService', function($q,$http,$rootScope,$cookies,$state,$configService) {
var products = '';
return {
//Get all products
getProducts: function() {
//$http.get....
//Dont forget to use $q.defer() for your promise
},
returnProducts: function() {
return products;
},
//Get specific product
getProduct: function(productID) {
for (var i =0; i<products.length; i++) {
if (products[i].id == parseInt(productID)) {
return products[i];
}
}
}
}
});
Controller
productsApp.controller('productDetailCtrl', function($scope,$rootScope,$productService,$stateParams,$configService,$cookies) {
var products = $productService.returnProducts();
if (products == '') {
$configService.getToken().then(function(response){
$productService.getProducts().then(function(response){
$scope.product=$productService.getProduct($stateParams.productID);
}, function(error){
console.log(error);
});
}, function(error){
console.log(error);
});
} else {
$scope.product = $productService.getProduct($stateParams.jobID);
};
});
EDIT: As asked, I'll explain a bit more efficiently !
I've been sitting in front of an annoying problem recently, which is that whenever I update a value inside a directive, the controllers I'm not currently "in" are the only ones to be updated properly.
Scenario example: Profile page is made of two controllers. Navbar_controller which is just currently displaying the user name :
<div ng-if="Auth.isAuthenticated">Hello, {{Auth.getCurrentUser().name}}</div>
The second controller , Profile_controller is here to update user values. This is a simple function in the angular first controller, which updates CurrentUser:
$scope.updateUser = function (type, form) {
if (!$scope.modif)
return ;
$http.put('/api/users/' + Auth.getCurrentUser()._id + '/update', {type:type, modif:$scope.modif})
.success(function (data, status) {
$scope.user = Auth.setNewUser(data);
})
.error(function () {
console.log("error");
});
};
When I update, for example, the name. I can see that the database has been modified properly. And indeed, navbar_controller got the update because a new name is printed in the div. However, Profile_controller doesn't get the update: the name printed in the profile page didn't change.
Here are the two basic functions in Auth.service.js :
getCurrentUser: function() {
return currentUser;
},
// 'user' is the data retrieved in http put request dot success
setNewUser: function(user) {
currentUser = user;
$rootScope.$broadcast(); // Navbar_controller is updated with or without this line
return currentUser;
}
Anyway, if I look at the navbar and its controller, which is calling Auth.getCurrentUser() method, the user values are instantly modified. I'e been using an ugly method consisting in modifying the controller values manually or by refreshing the page... But this isn't the way to go, right ?
There must be something with "$rootScope.$broadcast();", but I'm really new to Angular and other questions on stackoverflow are too specific to help me understand properly.
Thank you !
Your question was a little difficult to understand, but I think the problem is that you are reference a changing object in your various controllers. Here is an example to explain:
Service:
var myObject = { ... };
return {
getObject() { return myObject; }
setObject(obj) { myObject = obj; }
};
Controller 1:
$scope.myObjA = Service.getObject();
Controller 2:
$scope.myObjB = Service.getObject();
Now on initialisation both controllers will be referencing the same object, so if you changed a property inside either controller (eg. $scope.myObjB.name = 'bob';), then the other controller would also see the name.
However if you changed the object itself in a controller (eg. Service.setObject(newObj);), then the controller will be referencing the new object, while the other controller will still be referencing the old one.
You can fix this by wrapping your service object in a container:
var cont = {
user: ...
};
function getContainer() { return cont; }
function setNewUser(user) { cont.user = user; }
Then inside your controllers, get the container (not the user):
$scope.cont = Service.getContainer();
And inside your html:
<div>{{cont.user.name}}</div>
Now when you update the user, all attached controllers will be updated.
Well I'd try to change and store the user information in $rootScope, for your scenario could be a good fit.
getCurrentUser: function() {
$rootScope.currentUser===undefined ? 'no User': $rootScope.currentUser;
},
setNewUser: function(user) {
$rootScope.currentUser = user;
//$rootScope.$broadcast(); no need to broadcast
return getCurrentUser();
}
in that way currentUser will be updated in different scopes as needed!
I'll quote AnuglarJs FAQ regarding to $rootscope:
$rootScope exists, but it can be used for evil
Occasionally there are pieces of data that you want to make global to
the whole app. For these, you can inject $rootScope and set values on
it like any other scope. Since the scopes inherit from the root scope,
these values will be available to the expressions attached to
directives like ng-show just like values on your local $scope.
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.
I have arrays stored in Firebase, one of which I need to retrieve when a user logs in. Each user has their own array which requires authentication for read. (It would be inconvenient to switch to another data structure). Since $firebase() always returns an object, as per the docs, I'm using the orderByPriority filter. However, if I do simply
$scope.songs = $filter('orderByPriority')($firebase(myref));
that doesn't work as songs always get an empty array.
I don't understand why this happens, but what I've done to solve it is use the $firebase().$on('loaded',cb) form and applied the filter in the callback. Is this a good solution?
The drawback is that I cannot do $scope.songs.$save()
Here's my controller, including this solution:
.controller('songListController', function($scope, $rootScope, $firebase, $filter, $firebaseSimpleLogin){
var authRef = new Firebase('https://my-firebase.firebaseio.com/users'),
dataRef;
$scope.loginObj = $firebaseSimpleLogin(authRef);
$scope.songs = [];
$rootScope.$on("$firebaseSimpleLogin:login", function(event, user) {
// user authenticated with Firebase
dataRef = $firebase(authRef.child(user.id));
dataRef.$on('loaded', function(data){
$scope.songs = $filter('orderByPriority')(data);
});
});
//other controller methods go here
$scope.save = function(){
if (!$scope.loginObj.user)
{
alert('not logged in. login or join.');
return;
}
//Was hoping to do this
//$scope.songs.$save().then(function(error) {
//but having to do this instead:
dataRef.$set($scope.songs).then(function(error) {
if (error) {
alert('Data could not be saved.' + error);
} else {
alert('Data saved successfully.');
}
});
};
});
---Edit in response to Kato's answer---
This part of my app uses Firebase as a simple CRUD json store without any realtime aspects. I use $set to store changes, so I think I'm okay to use arrays. (I'm using jQueryUI's Sortable so that an HTML UL can be re-ordered with drag and drop, which seems to need an array).
I don't need realtime synchronisation with the server for this part of the app. I have a save button, which triggers the use of the $scope.save method above.
The problem with the approach above is that orderByPriority makes a single copy of the data. It's empty because $firebase hasn't finished retrieving results from the server yet.
If you were to wait for the loaded event, it would contain data:
var data = $firebase(myref);
data.$on('loaded', function() {
$scope.songs = $filter('orderByPriority')(data);
});
However, it's still not going to be synchronized. You'll need to watch for changes and update it after each change event (this happens automagically when you use orderByPriority as part of the DOM/view).
var data = $firebase(myref);
data.$on('change', function() {
$scope.songs = $filter('orderByPriority')(data);
});
Note that the 0.8 release will have a $asArray() which will work closer to what you want here. Additionally, you should avoid arrays most of the time.
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;