I'm having some trouble wrapping my head around data sharing between controllers. What I want to do is fetch data from a database (through a $http request), store it in a service variable, then share it between different controllers. From what I understand, that would allow my view to update automatically as the data is modified.
It seems quite easy with variables simply declared inside a service and accessed through a getter by the controllers. But the data I'm trying to share comes from an async operation, and I'm struggling to access it.
I came up with the following code, and I don't understand why I keep getting an "undefined" variable.
File: userController.js
function userController($scope, user) //user = userService
{
user.getChallengeList(25)
.then(function(defiList)
{
$scope.allDefis = defiList;
console.log($scope.allDefis); //ok
console.log(user.allDefis); //undefined
});
}
File: userService.js
function userService($http)
{
this.allDefis;
this.getChallengeList = function(id)
{
return $http
.post('http://localhost:3131/defi/defiList', {'id': id})
.then(function(response)
{
this.allDefis = response.data;
return this.allDefis;
});
}
}
From this piece of code, shouldn't the allDefis variable be accessible inside the controller?
Doesn't using .then in the controller "force" it to wait for the getChallengeList() method to be executed?
In that case, why is the user.allDefis variable undefined?
I think I could be solving this problem by using $rootscope, but I'd prefer not to, as it doesn't seem like a recommended solution.
Your issue lies in your implementation of getChallengeList:
this.getChallengeList = function(id)
{
return $http
.post('http://localhost:3131/defi/defiList', {'id': id})
.then(function(response)
{
this.allDefis = response.data; //<-- this is scoped here
console.log(this.allDefis);
return this.allDefis;
});
}
When you assign the web request response data to allDefis, the this is scoped to be within the anonymous function, rather than the overall function.
A technique to address this is to define a local variable that points to this, and use it instead.
You would adjust your implementation like this:
this.getChallengeList = function(id)
{
var _this = this; // a reference to "this"
return $http
.post('http://localhost:3131/defi/defiList', {'id': id})
.then(function(response)
{
_this.allDefis = response.data; // now references to correct scope
console.log(this.allDefis);
return this.allDefis;
});
}
Related
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
I'm trying to take the response of an $http request and save it to a custom cache. I want to then use that cache to display data into the view. I thought the cache would be checked automatically on each request before fetching new data, but that doesn't seem to be working for me.
The problem I'm having is I can't seem to save the data. The following function needs to make 2 requests: articles and images.
getImages: function() {
var cache = $cacheFactory('articlesCache');
$http.get(posts)
.then(function (data) {
var articles = data;
angular.forEach(articles, function (article) {
var imageId = {id: article.image_id};
$http.post(images, imageId)
.then(function (response) {
article.image = response;
cache.put(article.url, article);
});
});
});
return cache;
}
This creates the custom cache, but there's no data in the returned object. I know now that I can't save the data this way, but I don't know why or how I would go about doing it.
Can anyone explain how storing response data works? Where, if at all, does using promises come in here?
Your return statement executes before the code in your then function does. If you want to return the cache you'll want to run everything through the $q service and then return the resolved promise.
This is probably not the best way to use $cacheFactory. Typically you'd expose your cache as a service at a higher level and then access the cache via the service where needed.
So on your main module you'd have something like this to create the cache.
.factory('cache', function ($cacheFactory) {
var results = $cacheFactory('articleCache');
return results;
})
Then where ever you need the cache you inject it into the controller and use cache.get to retrieve the data from it.
If you want to use $q to implement this, your code would look something like the code below. (Disclaimer: I've never used $q with $cacheFactory like this, so without all of your components, I can't really test it, but this should be close.)
var imageService = function ($http, $q,$cacheFactory) {
var imageFactory = {};
imageService.cache = $cacheFactory('articlesCache');
imageFactory.getImages = function () {
var images = $q.defer();
$http.get(posts)
.then(function (data) {
var articles = data;
angular.forEach(articles, function (article) {
var imageId = {id: article.image_id};
$http.post(images, imageId)
.then(function (response) {
article.image = response;
cache.put(article.url, article);
});
images.resolve(cache.get('articlesCache'))
});
});
return images.promise
app.factory('ImageService', ['$http', '$q', '$cacheFactory', imageService]);
});
I adapted the code from this answer: How to get data by service and $cacheFactory by one method
That answer is just doing a straight $http.get though. If I understand what you're doing, you already have the data, you are posting it to your server and you want to avoid making get call to retrieve the list, since you have it locally.
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.
How can I make an Angular service code "look synchronous"?
My questions arose when I cleaned my controller and put the business logic code into a service instead. So far so good. Now I would like to "wait" in the service function until all asynchronous calls have returned and then return. How can I do that?
To illustrate my problem, suppose you have a controller code which just:
requests some data from the backend
does some processing with the data and
hands the data over to the scope
Like that:
DataController before refactoring:
$scope.submitForm = function() {
RestBackend.query('something').then(function(data) {
// do some additional things ...
...
$scope.data = data;
});
};
Pretty straightforward. Fetch data and fill scope.
After refactoring into controller + service, I ended up with:
DataController refactored:
$scope.submitForm = function() {
DataService.getData().then(function(data) {
$scope.data = data;
});
};
DataService refactored:
this.query = function() {
var dataDefer = $q.defer();
RestBackend.query('something').then(function(data) {
// do some additional things ...
...
dataDefer.resolve(data);
});
return dataDefer.promise;
};
I dislike the fact that I have to work with a promise in the controller also. I like promises but I want to keep the controller agnostic of this "implementation detail" of the service. This is what I would like the controller code to look like:
DataController (as it should be):
$scope.submitForm = function() {
$scope.data = DataService.getData();
};
You get the point? In the controller I don't want to care about promise or not. Just wait for the data to be fetched and then use it. Thus, I am looking for a possibility to implement the service like this:
query the data (asynchronously)
do not return until the data has been fetched
return the fetched data
Now item 2. is not clear to me: How can I "wait until data has been fetched" and only proceed afterwards? The goal is that the service function looks synchronous.
I too think your solution is fine.
Returning a promise is not an implementation detail of the service. It is part of the service's API (the "contract" between the service and the service-consumer).
The controller expects a promise that resolves with the data and handles that as it sees fit.
How that promise is constructed, how the data is fetched etc, these are the implementation details.
You can swap the service at any time with one that does totally different things as long as it fulfills the contract (i.e. returns a promise that resolves with the data onve ready).
That said, if you only use the data in the view (i.e. do not directly manipulate it in the controller right after it is fetched), which seems to be the case, you can use ngResources approach:
Return an empty array and populate it with the data once it is fetched:
$scope.data = DataService.getData();
// DataService refactored:
this.getData = function () {
var data = [];
RestBackend.query('something').then(function(responseData) {
// do some additional things ...
...
angular.forEach(responseData, function (item) {
data.push(item);
});
});
return data;
};
BTW, in your current (fine) setup, you need $q.defer(). You can just use promise-chaining:
this.query = function() {
return RestBackend.query('something').then(function(data) {
// do some additional things ...
...
return data;
});
};
I think what you have is a very good solution. You should not have to wait for promise to be resolved, it defeats the purpose of async javascript. Just ask yourself why do you need to make it run sync?
If you rely in html on promise to be resolve you can do something like this
<div class="alert alert-warning text-center" data-ng-hide="!data.$resolved">
Got data from service.
</div>
As you use ngRoute, I would recommend you to resolve you data in your route config, and the view will be loaded once all your data will be resolved.
$routeProvider
.when('/your-url', {
templateUrl: 'path/to/your/template.html',
controller: 'YourCtrl',
// that's the point !
resolve: {
superAwesomeData: function (DataService) {
return DataService.getData();
}
}
});
Now, superAwesomeData can be injected in your controller and it will contains the data, resolved.
angular.module('youModule')
.controller('YourCtrl', function (superAwesomeData) {
// superAwesomeData === [...];
});
I have service:
angular.module('app1App')
.service('Fullcontactservice', function Fullcontactservice(Restangular, $http, $q) {
// AngularJS will instantiate a singleton by calling "new" on this function
var self = this;
self.apiKey = "..........."
self.route = "person"
self.getProfile = function(email){
console.log("called")
console.log(email)
Restangular.one("person").get({email: email, apiKey: self.apiKey})
.then(function(response){
console.log(response)
return response
})
}
return self;
});
Controller:
angular.module('app1App')
.controller('FullcontactCtrl', function ($scope, Fullcontactservice) {
$scope.searchFullcontact = function(){
$scope.data = Fullcontactservice.getProfile($scope.email)
}
});
When I call the searchFullcontact(), Restangular calls fullcontact and returns data but that's not pushed to the scope - I understand why. When I use promises, just results to a {} and no data is pushed.
How can I have it do that. I am trying to avoid the .then() function within my controller to keep it being slim because traditionally I had very large controllers.
Thanks!
Restangular has a handy feature allowing you to make this code work:
self.getProfile = function(email){
return Restangular.one("person").get({email: email, apiKey: self.apiKey}).$object
}
$object is effectively a shortcut where Restangular first creates an empty object which is filled with the data from the REST call once it is available. The code working with $scope.data inside your controller must be flexible to handle the initially empty object. This is usually not an issue if you use data inside the template (html) as angular gracefully handles missing (undefined).