Regenerating data provided by Angular factories after source change - angularjs

I simplified my application to this example, so I know this particular case can be done more easier but I wanted to keep the app's original structure.
So, here I have a rootScope set at the beginning (this mock the data source from the original app):
.run(function($rootScope){
$rootScope.initarray = [1,1];
})
There are two factories:
The priceFactory returns $rootScope.initarray in an asynchronous way.
The doublePrice factory takes the array from priceFactory and doubles every item.
.factory('priceFactory', function ($rootScope, $q, $timeout) {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve($rootScope.initarray)
}, 1000);
console.log('priceFactory');
return deferred.promise;
})
.factory("doublePrice", function (priceFactory) {
console.log('doublePrice');
return priceFactory.then(
function(data){
var temp = [];
for (i in data){
temp.push(data[i]*2)
}
return temp;
});
})
I also having two ui-route states, one showing the data returned by doublePrice, the second state having a button that adds a new element to $rootScope.initarray.
My problem is that when I add a new element to $rootScope.initarray in the second state, i have to update the data shown in the first state.
Here's a codepen example about the issue.
So, the question: if there is any simple way to achieve that update. I'm using angular with ionic, it's not a cache issue, but the factories and ui-router state resolve that returns the same data during the app run.

The usage factory service suggests that the returning value is cached on the first injection and reused on the subsequent injections. This is the desired behaviour and it can't be changed.
The need to get a fresh promise on each route change is the common case for route resolvers. The alternative is to use factory service that returns a function:
.factory('priceFactory', function ($rootScope, $timeout) {
return function () {
// no deferred antipattern is necessary here
return $timeout(function() {
return $rootScope.initarray;
}, 1000);
};
});
So it can be used like that (good if additional parameters from $state should be provided to a function):
priceResolver: function (priceFactory) {
return priceFactory();
}
Or it can be service service, which is a perfect use case for resolvers, because it issues a new instance on each injection. service services get their instance as this but they can ignore it and return a new object instead:
.service('priceService', function ($rootScope, $timeout) {
return $timeout(function() {
return $rootScope.initarray;
}, 1000);
});

Related

Make data from factory http.get accessible to entire controller AngularJS

I am trying to give access to a json file that contains config information for my project (things like rev number, project name, primary contact, etc) I created a factory that retrieves the json file using http.get, I can then pull that data into my controller but I am unable to access it from anywhere in the controller.
I did not write the factory, I found it as an answer to another person's question and it is copied almost entirely so if it not the right way to accomplish what I am trying to do please correct me.
here is the factory:
app.factory('configFactory', ["$http", function($http) {
var configFactory = {
async: function() {
// $http returns a promise, which has a then function, which also returns a promise
var promise = $http.get('assets/json/config.json').then(function(response) {
// The then function here is an opportunity to modify the response
console.log(response.data.config);
// The return value gets picked up by the then in the controller.
return response.data.config;
});
// Return the promise to the controller
return promise;
}
};
return configFactory;
}]);
and here is my controller:
app.controller('footerController', ['$scope', '$rootScope', 'configFactory', function footerController($scope, $rootScope, configFactory) {
var body = angular.element(window.document.body);
$scope.onChange = function(state) {
body.toggleClass('light');
};
configFactory.async().then(function(d) {
$scope.data = d;
// this console log prints out the data that I am trying to access
console.log($scope.data);
});
// this one prints out undefined
console.log($scope.data);
}]);
So essentially I have access to the data within the function used to retrieve it but not outside of that. I can solve this with rootScope but I am trying to avoid that because I think its a bandaid and not a proper solution.
Any help would be great but this is my first experience with http.get and promises and all that stuff so a detailed explanation would be very much appreciated.
[EDIT 1] The variables from the config file will need to be manipulated within the web app, so I can't use constants.
Don't assign your response data to scope variable , create a property in your factory itself and assign the response to this property in your controller when your promise gets resolved.This way you will get the value in all the other controllers.
I have updated your factory and controller like below
app.factory('configFactory', ["$http", function($http) {
var configFactory = {
async: function() {
// $http returns a promise, which has a then function, which also returns a promise
var promise = $http.get('assets/json/config.json').then(function(response) {
// The then function here is an opportunity to modify the response
console.log(response.data.config);
// The return value gets picked up by the then in the controller.
return response.data.config;
});
// Return the promise to the controller
return promise;
},
config:'' // new proprety added
};
return configFactory;
}]);
app.controller('footerController', ['$scope', '$rootScope', 'configFactory', function footerController($scope, $rootScope, configFactory) {
var body = angular.element(window.document.body);
$scope.onChange = function(state) {
body.toggleClass('light');
};
configFactory.async().then(function(d) {
// $scope.data = d;
configFactory.config=d;
// this console log prints out the data that I am trying to access
console.log($scope.data);
});
// this one prints out undefined
console.log($scope.data);
}]);
Have you looked into using angular constants? http://ilikekillnerds.com/2014/11/constants-values-global-variables-in-angularjs-the-right-way/ You can leverage them as global variables accessible from any controller without the ramifications of assigning the values to rootScope

Angular service, how to have simultaneous calls wait

I have two controllers that want the same data around the same time, but I don't want the data binded.
So I have a service to go get user data that has this method:
var getUserData: function() {
if (!angular.isDefined(this.userData)) {
return new RESTUtil.getData('userData').then(function(data) {
this.userData = data;
return this.userData
})
} else {
return this.userData
}
}
Then I have two controllers that pull in the data for different purposes:
controller('ControllerA', ['$scope', '$rootScope', 'DataService'], function($scope, $rootScope, DataService) {
var ctrlA = this;
DataService.getUserData().then(function(data) {
ctrlA.userData = data;
});
});
controller('ControllerB', ['$scope', '$rootScope', 'DataService'], function($scope, $rootScope, DataService) {
var ctrlB = this;
DataService.getUserData().then(function(data) {
ctrlB.userData = data;
});
});
So the problem with what I currently have written is both ControllerA and ControllerB hit the getUserData service before the RESTUtil call returns, so i end up getting two data calls to the service.
I thought that i'd use a $scope.$on in Controller B to listen to a broadcast from ControllerA, but that binds the data.
Basically if ctrlA.userData = this and I change ctrlB.userData = that, i don't want the two userData objects to bind, they should be instantiated.
Any suggestions on how I can either a.) write my service logic where any calls to getUserData while a REST Call is happening will wait and use the same data call, or B.) have a broadcast that doesn't bind?
I saw a similar question posted here: Have multiple calls wait on the same promise in Angular
But i'm not sure how to leverage the $http since my actual webservice call is abstracted inside the RESTUtil.getData();
You're almost there. But your service has a flaw. The first time it's called, it returns a promise. But the second time it's called (if the promise has been resolved), it doesn't return a promise anymore, but the resolved data instead.
The service should simply always return a promise, and it should initialize the promise the first time it's called:
var getUserData: function() {
if (!angular.isDefined(this.userData)) {
this.userData = RESTUtil.getData('userData');
}
return this.userData;
}
If you want to avoid each caller to receive the same object from the promise, then return a new promise each time, that is resolved with a copy of the original data:
var getUserData: function() {
if (!angular.isDefined(this.userData)) {
this.userData = RESTUtil.getData('userData');
}
return $q.when(this.userData).then(angular.copy);
}

How to communicate with server using AngularJS within Google Apps Script

Recently it has become possible to use angularjs within google apps script via the iframe sandbox mode.
My problem comes when trying to communicate with the server (gapps spreadsheet) and receiving asynchronous data in return.
The implementation for receiving data from the server is to use a function with a callback function like so:
google.script.run.withSuccessHandler(dataGatheringFunction).getServerData();
getServerData() would be a function that resides server-side that would return some data, usually from the accompanying spreadsheet. My question is how to use the callback function within the parameters of AngularJS. A typical $http function could be placed in a provider, and the scope value could be populated after then.() returns. I could also invoke $q. But how would I deal with the necessity of google's callback?
Here's a simplified version of what I'm messing with so far:
app.factory("myFactory", function($q){
function ssData(){
var TssData = function(z){
return z;
}
google.script.run.withSuccessHandler(TssData).getServerData();
var deferred = $q.defer();
var d = deferred.resolve(TssData)
console.log("DP: " + deferred.promise);
return deferred.promise;
}
return ssData();
})
Then in the controller resolve the server call similar to this:
myFactory.then(set some variables here with the return data)
My question is simply - How do I deal with that callback function in the provider?
The script throws no errors, but does not return the data from the server. I could use the old $timeout trick to retrieve the data, but there should be a better way.
You only need to $apply the output from the server function:
google.script.run.withSuccessHandler(function(data) {
$scope.$apply(function () {
$scope.data = data;
});
}).withFailureHandler(errorHandler).serverFunction();
Maybe the most elegant solution that makes sure the google.script.run callbacks are registered automatically in the AngularJS digest cycle would be to use the $q constructor to promisify the google callbacks. So, using your example above:
app.factory('myFactory', ['$q', function ($q){
return {ssData: ssData};
function ssData(){
var TssData = function(z){
return z;
};
var NoData = function(error) {
// Error Handling Here
};
return $q(function(resolve, reject) {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.getServerData();
}).then(TssData).catch(NoData);
}
}]);
Then in your controller you can call myFactory.ssData()
Since I don't know exactly what TssData is doing I included it here but note that this simply returns another promise in this context which you will still have to handle in your controller:
myFactory.ssData().then(function(response) {
// Set data to the scope or whatever you want
});
Alternately, you could expose TssData by adding it to the factory's functions if it is doing some kind of data transformation. If it is truly just returning the response, you could refactor the code and omit TssData and NoData and handle the promise entirely in the controller:
app.factory('myFactory', ['$q', function ($q){
return {ssData: ssData};
function ssData(){
return $q(function(resolve, reject) {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.getServerData();
});
}
}]);
app.controller('myController', ['myFactory', function(myFactory) {
var vm = this;
myFactory.ssData()
.then(function(response) {
vm.myData = response;
}).catch(function(error) {
// Handle Any Errors
});
}]);
An excellent article about promises (in Angular and otherwise) is here: http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
This guy seems to be pulling data from a GSheet into angular quite happily without having to do anything fancy.
function gotData(res) {
$scope.validUser = res.validUser;
var data = angular.copy(res.data), obj, i=0;
Object.keys(data).forEach(function(sh) {
obj = {title: sh, checked: {}, showFilters: false, search: {}, sort: {index: 0, reverse: false}, currentPage: 0, checkedAll: true, showBtns: true, searchAll: ''};
obj.heading = data[sh].shift();
obj.list = data[sh];
obj.heading.forEach(function(s,i) {
obj.checked[i] = true;
});
$scope.sheets.push(obj);
});
$scope.sheets.sort(function(a,b) {
return a.title > b.title ? 1 : -1;
});
$scope.gotData = true;
$scope.$apply();
}
google.script.run.withSuccessHandler(gotData).withFailureHandler($scope.gotError).getData();
My solution was to get rid of the $q, promise scenario all together. I used $rootScope.$broadcast to update scope variables from the server.
Link to spreadsheet with script.

Accessing and using JSON within an Angular service for logic flow

I asked the wrong question yesterday (and got a goodanswer that worked), but am realizing it's not what I needed. I need to be able to retrieve JSON data (preferably once), store it, and access it throughout my service. The challenge I'm having is that all the examples I can find talk about using JSON and passing to the app/controller, whereas in this case I need to get it, check it, and then it dictates what my module/service does.
For instance, I have my App and Controller, and then I have a module such as (this is psuedo-code, not meant to run):
angular.module("myModule")
.service("myService1", function($q, myService2, $http) {
this.getModel = function() {
return {
title: "My Title",
desc: "My Desc"
options: function () {
if (condition A)
return "option1";
else
return "option2";
}
};
};
})
.service("myService2", function($q, $http) {
this.getCfgInfo = function () {
var defer = $q.defer();
$http.get("my/json/url").then(function(response) {
defer.resolve(response.data);
});
return defer.promise;
};
})
In this example, I'm wanting to get the JSON, and use it within myService1 for both literal values (title, desc) as well as for conditions (condition A within the if).
I know I can do something like this (thanks to Joel for helping yesterday):
service("myService1", function($q, myService2, $http) {
// get a promise object for the configuration info
var cfgProm = rtDataMapper.getCfgInfo()
this.getModel = function() {
return {
title: cfgProm.then(function(response) {
return response.JSON_NAME;
}),
and it works fine as I've got the title mapped back into my model and there is a watch(), but I'm stumped as to how I get, store, and use the JSON within the service itself as a conditional (i.e. if (condition A) where condition A is coming from the JSON. Trying to wrap these in .then() doesn't seem to make sense, or at least I can't figure out how to do it.
I'm new to Angular and am attempting to modify some code that was left to us. I'm guessing I don't need the myService2 just to get the JSON. Can anyone help point me in the right direction? I've spent several hours online but can't seem to find a relevant reference/example.
Thanks
Live demo (click).
I'm having the service immediately get the data when it is injected (that code will only run once no matter how many times you inject it). That's nice because you won't have to call a function to get the data - it's called for when creating the service.
Your service method that returns that data will need to return the promise of the data, of course, since you aren't guaranteed that it will have come through when you ask for it. You can pass arguments to that method to use to determine your conditions. All you need to do for that is use promise.then in the method and resolve the promise with the modified data. Since that method is returning the promise already, the modification will be updated on the resolve. See all of this below and in the demo.
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, myService) {
myService.getData(15).then(function(data) {
$scope.myData = data;
});
});
app.factory('myService', function($q, $timeout) {
//this code only runs once when you first inject the service
//get data immediately
var deferred = $q.defer();
$timeout(function() { //simulate ajax call
var data = { //ajax response data
foo: 15,
bar: 'Some data!'
};
data = modifyData(data, 1);
deferred.resolve(data);
}, 500);
function modifyData(data, fooVal) {
if (data.foo === fooVal) {
data.baz = 'Conditional data!';
}
return data;
}
var myService = {
//data can be modified when it comes from the server,
//or any time you call this function
getData: function(fooVal) {
if (fooVal) { //if you want to modify the data
deferred.promise.then(function(data) {
data = modifyData(data, fooVal);
deferred.resolve(data);
});
}
return deferred.promise;
}
};
return myService;
});

How to wait till the response comes from the $http request, in angularjs?

I am using some data which is from a RESTful service in multiple pages.
So I am using angular factories for that. So, I required to get the data once from the server, and everytime I am getting the data with that defined service. Just like a global variables. Here is the sample:
var myApp = angular.module('myservices', []);
myApp.factory('myService', function($http) {
$http({method:"GET", url:"/my/url"}).success(function(result){
return result;
});
});
In my controller I am using this service as:
function myFunction($scope, myService) {
$scope.data = myService;
console.log("data.name"+$scope.data.name);
}
Its working fine for me as per my requirements.
But the problem here is, when I reloaded in my webpage the service will gets called again and requests for server. If in between some other function executes which is dependent on the "defined service", It's giving the error like "something" is undefined. So I want to wait in my script till the service is loaded. How can I do that? Is there anyway do that in angularjs?
You should use promises for async operations where you don't know when it will be completed. A promise "represents an operation that hasn't completed yet, but is expected in the future." (https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise)
An example implementation would be like:
myApp.factory('myService', function($http) {
var getData = function() {
// Angular $http() and then() both return promises themselves
return $http({method:"GET", url:"/my/url"}).then(function(result){
// What we return here is the data that will be accessible
// to us after the promise resolves
return result.data;
});
};
return { getData: getData };
});
function myFunction($scope, myService) {
var myDataPromise = myService.getData();
myDataPromise.then(function(result) {
// this is only run after getData() resolves
$scope.data = result;
console.log("data.name"+$scope.data.name);
});
}
Edit: Regarding Sujoys comment that
What do I need to do so that myFuction() call won't return till .then() function finishes execution.
function myFunction($scope, myService) {
var myDataPromise = myService.getData();
myDataPromise.then(function(result) {
$scope.data = result;
console.log("data.name"+$scope.data.name);
});
console.log("This will get printed before data.name inside then. And I don't want that.");
}
Well, let's suppose the call to getData() took 10 seconds to complete. If the function didn't return anything in that time, it would effectively become normal synchronous code and would hang the browser until it completed.
With the promise returning instantly though, the browser is free to continue on with other code in the meantime. Once the promise resolves/fails, the then() call is triggered. So it makes much more sense this way, even if it might make the flow of your code a bit more complex (complexity is a common problem of async/parallel programming in general after all!)
for people new to this you can also use a callback for example:
In your service:
.factory('DataHandler',function ($http){
var GetRandomArtists = function(data, callback){
$http.post(URL, data).success(function (response) {
callback(response);
});
}
})
In your controller:
DataHandler.GetRandomArtists(3, function(response){
$scope.data.random_artists = response;
});
I was having the same problem and none if these worked for me. Here is what did work though...
app.factory('myService', function($http) {
var data = function (value) {
return $http.get(value);
}
return { data: data }
});
and then the function that uses it is...
vm.search = function(value) {
var recieved_data = myService.data(value);
recieved_data.then(
function(fulfillment){
vm.tags = fulfillment.data;
}, function(){
console.log("Server did not send tag data.");
});
};
The service isn't that necessary but I think its a good practise for extensibility. Most of what you will need for one will for any other, especially when using APIs. Anyway I hope this was helpful.
FYI, this is using Angularfire so it may vary a bit for a different service or other use but should solve the same isse $http has. I had this same issue only solution that fit for me the best was to combine all services/factories into a single promise on the scope. On each route/view that needed these services/etc to be loaded I put any functions that require loaded data inside the controller function i.e. myfunct() and the main app.js on run after auth i put
myservice.$loaded().then(function() {$rootScope.myservice = myservice;});
and in the view I just did
ng-if="myservice" ng-init="somevar=myfunct()"
in the first/parent view element/wrapper so the controller can run everything inside
myfunct()
without worrying about async promises/order/queue issues. I hope that helps someone with the same issues I had.

Resources