I just started out coding angular, and I'm not sure what I'm exactly doing wrong.
I have a factory with a method that uses an $http get:
angular.module('d3_survey')
.factory('currentSurvey', ['$http', function($http) {
return{
sweep : function(number) {
console.log('resources/currentSurvey/' + number + '.json');
return $http.get('resources/currentSurvey/' + number + '.json').then(function (response) {
return response;
});
}
}
}]);
I thought that I could do this since sweep is a closure and everything and it would still have access to $http. I resolve the service in my ui-router config.
resolve : {
surveyDetails : ['surveyQuestions', function(surveyQuestions){
return surveyQuestions.all();
}],
messages : ['currentSurvey', function(currentSurvey) {
return currentSurvey;
}]
}
I inject "messages" onto my controller and try to call the sweep method:
var messages = $scope.currentSurvey.sweep('1');
$scope.result = JSON.stringify(messages);
But when I bind $scope.result to my view I get something weird on the page:
{"$$state":{"status":0}}
I saw other questions where some people have done a further .then() on the result of the factory method, but even this didn't work out.
This is what I see on the console, but it doesn't make any sense:
SyntaxError: Unexpected token m
at Object.parse (native)
at pc (http://localhost:63342/SurveyTool/js/angular.min.js:14:208)
at Zb (http://localhost:63342/SurveyTool/js/angular.min.js:76:379)
at http://localhost:63342/SurveyTool/js/angular.min.js:77:237
at s (http://localhost:63342/SurveyTool/js/angular.min.js:7:302)
at Zc (http://localhost:63342/SurveyTool/js/angular.min.js:77:219)
at c (http://localhost:63342/SurveyTool/js/angular.min.js:78:349)
at http://localhost:63342/SurveyTool/js/angular.min.js:112:20
at l.$eval (http://localhost:63342/SurveyTool/js/angular.min.js:125:305)
at l.$digest (http://localhost:63342/SurveyTool/js/angular.min.js:122:398) angular.js:11939
You can't just return from asynchronous operation like AJAX request. In your case currentSurvey.sweep returns a Promise object, so you should use its API to chain callback with then method. This callback will be invoked once data is available:
messages.sweep('1').then(function(response) {
$scope.result = JSON.stringify(response.data);
});
Another thing, you don't really need to provide messages key in resolve section of the router config. You can inject currentSurvey service directly to controller.
Related
I am having trouble figuring out proper use of angular's $q and Javascript's promise api. Then again, I am pretty new to Javascript, and may be making a syntax error.
This question seems similar, but it doesn't seem to be an exact copy...
My service has a method like this (called by my controller):
self.getResources = function() {
let promises = [];
for (let resourceName in $rootScope.resourceNameList) {
promises.push($http({
url : '/' + resourceName,
method : 'GET'
}).then(function (res) {
return { resourceName : res.data};
}));
}
return promises;
};
And my controller passes the returned promise list into angular's $q.all():
let promises = resourceService.getResources();
$q.all(promises).then(function (resultsArray) {
// resultsArray should be an array of elements like:
// [ {cars : [...]}, {coffeePots : [...]}, {motorBoats : [...]} ]
// with size = resourceNameList.length(), correct?
// this log is never called.
console.log(resultsArray);
$scope.resources = resultsArray;
});
Finally, in my HTML, I try to iterate over and display the resources:
.
.
.
<!-- 'cars' is and example, but I am trying to retrieve this data in the same way -->
<tr ng-repeat="item in resources[cars]">
<td>{{item.property1}}</td>
<td>{{item.property2}}</td>
</tr>
The table is not displaying any results.
I can see from the console that the HTTP requests are successful and returning data, so I must be making a mistake with either the promise or $q apis.
Any suggestions?
Thanks much.
I know this is probably a non-answer since you've already figured that you actually have a mistake on your input data set, but you should really detect those kinds of problems earlier by adding a .catch on (at least) your root promise so you can immediately see what's actually up.
In your case, this is your root promise (with the catch already attached).
I've broken it into newlines for clarity:
let promises = resourceService.getResources();
$q
.all(promises)
// unfortunately, .then only gets executed if all of these promises succeed.
.then(function (resultsArray) {
console.log(resultsArray);
$scope.resources = resultsArray;
})
// there is a disturbance in the force!
.catch(function (error) {
// you can console.log(error) OR
window.open("https://stackoverflow.com/search?q="+error.message);
})
;
EDIT: If you need to filter out the failed promises on $q.all, just replace it with $q.allSettled! Full API Here: https://github.com/kriskowal/q
Some additional thoughts to share: soon enough, you'll find yourself complaining $q to be missing some features like combining .push or .map with .all and such. There's already a library for that; you can use BluebirdJS! and here's a nifty wrapper for it for angular. instead, your code would look like this:
Promise.map($rootScope.resourceNameList, function(resourceName) {
return $http({
url : '/' + resourceName,
method : 'GET'
});
}, {concurrency: 3}) // did I mention you could control concurrency?
.then(function(results) {
console.log(results); // $http promises that were resolved!
},function(rejects) {
console.log(rejects); // $http promises that were rejected :(
})
.catch(function (error) {
// you can console.log(error) OR
window.open("https://stackoverflow.com/search?q="+error.message);
})
;
Much better, right?
Happy coding! Here's a great read about promises: http://taoofcode.net/promise-anti-patterns/
You need to inject $q and $http in the directive like this
self.getResources = function($q,$http) {
let promises = [];
for (let resourceName in $rootScope.resourceNameList) {
promises.push($http({
url : '/' + resourceName,
method : 'GET'
}).then(function (res) {
return { resourceName : res.data};
}));
}
return promises;
};
I try to get some important things like: companyid,employeeid etc. with every request that a user makes. So this has to be received before everything else is done.
After that the user receives information based on his companyid that he sets with every request (get/company/{companyid}).
The problem that I have is that the response for receiving the companyid takes to long and angular already tries to make a request to (get/company/{companyid}) obviously there is no companyid yet.
I've tried to fix this whit promise but it's not working.
Here I try to receive some important information about the user(that I do with every request) :
Service
(function () {
angular.module('employeeApp')
.service('authenticationservice', authenticationservice);
function authenticationservice($http,$location,authenticationFactory,$q,GLOBALS,$cookies) {
this.validateUser = function () {
var vm = this;
vm.deferred = $q.defer();
data = {"api_token": api_token};
return $http.post(GLOBALS.url+'show/employee/' + $cookies.get('employeeid'),data)
.success(function(response)
{
vm.deferred.resolve(response);
})
.error(function(err,response)
{
vm.deferred.reject(err);
});
return vm.deferred.promise;
}
}
})();
Routes file
(In my routes file I use the authenticationservice to set all important users variables.)
employeeAppModule.run([
'authenticationservice',
'constants',
function(authenticationservice,constants) {
authenticationservice.validateUser()
.then(function(response)
{
constants.companyid = response.result.Employee;
constants.role = response.result.Role;
constants.name = response.result.FirstName;
console.log('test');
},
function(response){
console.log('error');
});
}
]);
So the problem is that the user information is set to late and angular already goes to my homeController where he uses the companyId that is not being set yet.
Thankyou
The problem in your current code is return $http.post are having two return statement in your validateUser method. Which is returning $http.get before returning return vm.deferred.promise; & that why customly created promise doesn't get returned from your method. Though by removing first return from $http.get will fix your problem, I'd not suggest to go for such fix, because it is considered as bad pattern to implement.
Rather I'd say, you should utilize promise return by $http method, & use .then to return data to chain promise mechanism.
Code
function authenticationservice($http, $location, authenticationFactory, $q, GLOBALS, $cookies) {
this.validateUser = function() {
var vm = this;
data = {
"api_token": api_token
};
return $http.post(GLOBALS.url + 'show/employee/' + $cookies.get('employeeid'), data)
.then(function(response) {
var data = response.data;
retrun data;
}, function(err) {
return $q.reject(err);
});
}
}
To make sure that $ http return a $ promise object you need to check that the action in the controller returns a value and it is not a void action.
BACKGROUND
As my app grows I’m struggling more and more with AngularJS promise synchronization / sequencing across multiple controllers and services. In my example below I have an articles controller ArticleController and related service ArticleDataService that in the initial load process
gets / loads articles from a server,
selects the first article from articles and
uses this current article currentArticle to get the related images from the server.
THE PROBLEM
Data load from the server takes approx. 1 second to return the articles records, then as above select first article and then also return the related image records from the server. The problem is that during that latency period the second controller (ImagesController) is looking for the cached data in the second Services module (ImageDataService) and cannot find it because the first promises have obviously not resolved from the Article Controller due to server latency and as such the ArticleController hasn't cached the images data yet, which then blows up any following image related code. As you can see below, if I try to return a $q.when(cachedImages) on cachedImages, it will returns a promise, but that promise is never resolved. As both controllers are using separate $q resolve sequences it makes sense, but without building an uber controller I'm unsure how to fix the sequencing issues.
EDIT
I'm trying to solve for the n:n chaining / sequencing problem
Most tutorials or discussions tend to focus on 1:1 or 1:n chaining which works perfectly. No problem there.
It is the n:n where I'm having problems i.e. ctrl to service, service to service and n ctrl to service. Most of what I can find on n:n are basic tuts loading simple static arrays or object which doesn't have the latency issue.
ATTEMPTED APPROACHES
rootscope / watch : I've tried $rootscope.$watch() events inside of services as well as watch in controllers i.e. an event based approach on the imageCached var inside of the ImageDataService, but frankly I find that messy as there can be unnecessary overhead, debugging and testing issues.That said, it does work but every now and then I will see lots of iteration when console logging a deeply nested var which makes the event approach seem black boxish.
EDIT - watch approach
Example: I can add the following to the 2nd controller ImageController or in the ImageDataService which works, as would $emit, and then kill the watcher, but as I said this does require a bit of time management for dependent methods such as chart data directives. Also, I wondering if mixing promises and events is bad practice or is that the accepted best practice in JS?
var articleModelListener = $scope.$watch(function () {
return ImageDataService.getImages();
},
function (newValue, oldValue) {
if (newValue !== undefined && newValue !== null) {
if (Object.keys(newValue).length > 0) {
iCtrl.dataUrls = newValue;
// kill $watcher
articleModelListener();
}
}
});
timeout : I've also tried to wrap all ImageController code in timeout AND also document ready, but I find that has further repercussions down the line e.g. in my Chart and Poll controllers I have to wrap directives in additional $timeouts or $intervals to adjust for the ImagesController time intervals or the directives won't load the DOM attributes correctly, so it becomes a chain of app performance death.
uber DataService service or factory data resolution : I've tried to resolve all data in an uber DataServices service provider but I find I have the same issue now in all controllers as although the uber service fixes the sequencing I now need to get synchronization with uber and all controllers. I know async ... give me state programming any day :)
QUESTION & ASSUMPTION
Assumption: timeout and interval wrapping are bad practices / anti-pattern as that is waterfall?
Is the best approach to stick with promises and if so is there a way to get promises to synchronize or said better sequentially resolve across multiple controllers / services OR do I keep going down the events approach using rotoscope watches and controller scope watches?
Example of my code / problem below:
PLEASE NOTE:
1. I've removed code for brevity sake i.e. I have not tested this summary code, but rather using it to example the problem above.
2. All and any help is much appreciated. Apologies for any terminology I've misused.
HTML
<section ng-controller="MainController as mCtrl">
// removed HTML for brevity sakes
</section>
<section ng-controller="ImagesController as iCtrl">
// removed HTML for brevity sakes
</section>
Angular JS (1.4.*)
<pre><code>
angular.module('articles', [
])
.controller('ArticlesController', ['ArticleDataServices', 'ImageDataService', function(ArticleDataServices, ImageDataService) {
var mCtrl = this;
mCtrl.articles = {};
mCtrl.currentArticle = {};
mCtrl.images = {};
var loadArticles = function () {
return ArticleDataServices
.getArticles()
.then(function (articles) {
if(articles.data) {
mCtrl.articles = articles.data;
return mCtrl.articles[Object.keys(mCtrl.articles)[0]];
}
});
},
loadCurrentArticleImages = function (currentArticle) {
return ImageDataService
.getArticleImages(currentChannel)
.then(function (imagesOfArticles) {
if(imagesOfArticles.data) {
return mCtrl.images = imagesOfArticles.data;
}
});
},
cacheImages = function (images) {
return ImageDataService
.parseImages(images)
.then(function () {
});
};
loadChannels()
.then(loadArticles)
.then(loadCurrentArticleImages)
.then(cacheImages);
}])
</code></pre>
NOTE : it is in the ImagesController below where things go wrong as this controller is executing its methods ahead of the first controller which is still waiting on data from server i.e. cachedImages or promise is not returning.
<pre><code>
.controller('ImagesController', ['ImageDataService', function(ImageDataService) {
var iCtrl = this;
mCtrl.images = {};
var getCachedImageData = function () {
return ImageDataService
.getCachedImageData()
.then(function (images) {
if(images) {
return mCtrl.images = images;
}
});
}
}])
.service('ArticleDataServices', ['$http',', $q', function($http, $q){
var model = this,
URLS = {
ARTICLES: 'http://localhost:8888/articles'
},
config = {
params: {
'callback': 'JSON_CALLBACK',
'method': 'GET',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
};
model.getArticles = function() {
config.params['url'] = URLS.ARTICLES;
return $http(config.params);
};
return {
getArticles: function () {
var deffered = $q.defer();
deffered.resolve(model.getArticles());
return deffered.promise;
}
}
}])
.service('ImageDataService',['$http',', $q', function($http, $q){
var model = this,
URLS = {
IMAGES: 'http://localhost:8888/images'
},
cachedImages,
config = {
params: {
'callback': 'JSON_CALLBACK',
'method': 'GET',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
};
model.getArticleImages = function(currentArticle) {
config.params['url'] = URLS.IMAGES + '/' . currentArticle.slug;
return $http(config.params);
};
// Return images or $q.when
model.getCachedImageData = function() {
if (cachedImages) {
return cachedImages
} else {
return $q.when(cachedImages);
}
};
model.setImageCache = function(images) {
cachedImages = images;
};
return {
getArticleImages: function (currentArticle) {
var deffered = $q.defer();
deffered.resolve(model.getArticleImages(currentArticle));
return deffered.promise;
},
setImageCache:function (images) {
return model.setImageCache(images);
},
getCachedImageData:function () {
return getCachedImageData();
}
};
}]);
</code></pre>
Your problem is common for people when initializing with angular. The correct is return promise in your service as:
app.controller("AppController", function($scope, $ajax){
$ajax.call("/people", "", "POST").then(function(req) {
$scope.people = req.data;
});
});
app.factory("$ajax", function($http) {
function ajax(url, param, method) {
var requisicao = $http({
method: method,
url: url,
data:param
});
var promise = requisicao.then(
function(resposta) {
return(resposta.data);
}
);
return promise;
}
return({
call:ajax
});
});
Note that the variable is populated only in the return of service. It is important you put all methods or anything else that makes use of such a variable within Then method. This will ensure that these other methods will only be executed after returning from the backend
I am new to Angular, and am trying to get up to speed with ngResource.
I created a factory in my chapter.service.js file
angular.module('myApp')
.factory('Chapter', function ($resource) {
return $resource('/api/book/chapter/:id'); // Note the full endpoint address
});
matchescontroller.js
angular.module('myApp').controller('matchesCtrl', function($scope, $location, Chapter) {
// This is used to get URL parameters
$scope.url = $location.path();
$scope.paths = $scope.url.split('/');
$scope.id = $scope.paths[2];
$scope.action = $scope.paths[3];
//Trying to call the test data
var chapters = Chapter.query();
$scope.myFunction = function() {
alert(chapters.length);
}
My view where I test the function
<button ng-click="myFunction()">Click Here</button>
I created a test function to test whether my query returned any results. When I click on the button, I'm alerted with 0, which means the query didn't work.
When I change the function to
$scope.myFunction = function() {
console.log(Object.keys(chapters));
}
I get [$promise, $resolve], but none of the Schema keys
I must be doing something wrong, but I was looking at this tutorial
http://www.masnun.com/2013/08/28/rest-access-in-angularjs-using-ngresource.html
Any help will be appreciated.
Edit: Here is the response I got from the server
GET http://localhost:9000/api/book/chapter/1 500 (Internal Server Error)
$scope.myFunction = function() {
Chapter.query({}, function(data) {
$scope.chapters = data;
}, function(error) {
// custom error code
});
}
When working with $resource I prefer to use the success/error handlers that the API comes with as opposed to dealing the promise directly. The important thing to realize is that just because you called query does not mean that the result is immediately available. Thus the use of a callback that handles success/error depending on what your backend returns. Only then can you bind and update the reuslt in the UI.
Also, while we're talking about it I notice that you didn't wire up the optional paramter in your $resouce URL. $resource takes a second paramter which is an object that supplies mapping for the /:id part of your route.
return $resource('/api/book/chapter/:id', {id: '#id'});
What this notation means is that you pass an object to $resource that has a property called id, it will be subbed into your URL.
So this:
$scope.item = {id: 42, someProp: "something"};
Chapter.get({$scope.item}....
Will result in an API call that looks like '/api/book/chapter/42'
You get a promise from $resource and not the "result" from your database. The result is inside the promise. So try this
var chapters = Chapter.query();
$scope.myFunction = function() {
chapters.then(function(data) {
console.log(data);
});
}
I must admit, that I am not thaaaaat familiar with ngResource, so Jessie Carters is right, the correct syntax is:
chapters.get({...}, function callback() {...})
A controller makes 2 calls to a remote http location to get data.
When data comes a procedure is called. When both requests return data, then data merging is done and some aggregation is performed.
The purpose of a unit test would be to test if the controller works as expected no matter the order of responses.
it("downloads all data and combines it", function() {
...
$httpBackend.expectGET(responsePerDomainQuery).respond(
{ result: [ { result: 2 }, { result: 3 } ] });
$httpBackend.expectGET(responsePerTrQuery).respond(
{ result: [{ result: 1 }, { result: 4 }] });
$controller("Ctrl", { '$scope': $scope });
$httpBackend.flush();
... some expectations ...
}
The test passes but it does not guarantee that any order of successfully responding requests will not break the controller's logic. How can this be achieved?
When I said "no need to test this case" i was referring to the fact that using $q.all already guarantees that the callback is executed only when all of the requests are satisfied. That being said I agree that preparing tests for your own implementation is a good practice, so here's I would do it.
(Mind that this is just pseudo code, some things may need to be tweaked in order to work properly, but that's just to explain how i would tackle this one.)
First of all I would move my AJAX calls away from my controller and provide a dedicated service for them (maybe you already did it this way, if so that's great, bear with me for now).
As an example:
angular.service('myQueries', function($http){
this.myReq1 = function(){
return $http.get(API.url1);
};
this.myReq1 = function(){
return $http.get(API.url2);
};
});
Then I would test this service on its own normally using $httpBackend.expectGET().
I would then get back to the controller and use that service in there as specified in my comments to the question:
angular.controller('myCtrl', function($scope, myQueries, $q){
// at load time query for results
$q.all([myQueries.myReq1(), myQueries.myReq2()])
// everything after this is guaranteed to be run ONLY when
// both responses are in our hands
.then(doSomethingWithBoth)
// one or both requests went bad
// let's handle this situation too.
.catch(someThingWentBad);
function doSomethingWithBoth(data){
$scope.myData = data;
}
function someThingWentBad(data){
$scope.disaster = true;
}
});
At this point we can test our controller and inject a mocked service into it. Many ways to do it but something similar should do:
var scope, controller, fakeService, q, dfd1, dfd2;
beforeEach(function(){
fakeService = {
myReq1: function(){
dfd1 = q.defer();
return dfd1.promise;
},
myReq2: function(){
dfd2 = q.defer();
return dfd2.promise;
},
};
})
beforeEach(inject(function ($rootScope, $controller, $q) {
q = $q;
scope = $rootScope.$new();
controller = $controller('myCtrl', { $scope: scope, myQueries: fakeService });
}));
At this point you are free to resolve/reject the promises exactly when you want. You can check what happens when the first response is faster than the second:
it('should do this when one response is faster', function(){
dfd1.resolve('blabla');
// myReq2 is still pending so doSomethingWithBoth() has not yet been called
scope.$apply();
expect(scope.myData).toBe(undefined);
dfd2.resolve('i am late, sorry');
scope.$apply();
expect(scope.myData).not.toBe(undefined);
});
You can check what happens when the second response is faster than the first:
it('should do this when the other response is faster', function(){
dfd2.resolve('here is a response');
// myReq1 is still pending so doSomethingWithBoth() has not yet been called
scope.$apply();
expect(scope.myData).toBe(undefined);
dfd1.resolve('i am late, sorry');
scope.$apply();
expect(scope.myData).not.toBe(undefined);
});
Or what happens when one of those fails:
it('should do this when one response fails', function(){
dfd1.resolve('blabla');
dfd2.reject();
scope.$apply();
expect(scope.disaster).toBeTruthy();
});
We can use something like where alpha var will have response from 1st call n so on....
var promiseAlpha= $http({method: 'GET', url: 'a/pi-one-url', cache: 'true'});
var promiseBeta= $http({method: 'GET', url: '/api-two-url', cache: 'true'});
let promises = {
alpha: promiseAlpha,
beta: promiseBeta
}
$q.all(promises).then((values) => {
console.log(values.alpha); // value alpha
console.log(values.beta); // value beta
console.log(values.gamma); // value gamma
complete();
});