Dynamically adding key/value pairs to Javascript object in a loop - javascript

Looking to create an object of data in an angular factory i can return to a controller but having some issues:
app.factory('simpleFactory', function ($http) {
var mykey = 'akjhas3892kh8adsak9';
var apiResource = [{
type: 'type1',
abbr: 'abc'
}, {
type: 'type2',
abbr: 'efg'
}, {
type: 'type3',
abbr: 'hij'
}];
var data = {};
var setDataKeyValues = function (key, value) {
data[key] = value
};
for (var i = 0; i < apiResource.length; i++) {
var key = apiResource[i].type;
$http.get('http://api.com/v1/path/' + key + '/' + apiResource[i].abbr + '/anotherpath?apikey=' + mykey).success(function (info) {
setDataKeyValues(key, info);
});
}
return data;
});
The Angular http request is returning a JSON object, but when the data object is returned, it is empty. What is the correct why to add these key/values to this object?

$http.get is asynchronous, and your return is synchronous. You need to use a queue. Angular has this built in.
app.factory('simpleFactory', function ($http) {
...
var promises = [];
for (var i = 0; i < apiResource.length; i++) {
var key = apiResource[i].type;
var promise = $http.get('http://api.com/v1/path/' + key + '/' + apiResource[i].abbr + '/anotherpath?apikey=' + mykey).success(function (info) {
setDataKeyValues(key, info);
});
promises.push(promise);
}
// Once all asynchronous HTTP GET requests are complete
$q.all(promises).then(function () {
// Do something with the data, e.g.
$rootScope.apply();
});
return data;
});
simpleFactory is a misnomer. The factory method synchronously returns an object, which is then provided wherever you inject it. The values you set in your callbacks will be available later.

Since the requests are asynchronous, the data values are only set after the callback runs, but you are returning data before any of the callbacks have a chance to run.
If you want to do something after all data has returned, increment a counter every time you request some data and decrement the counter after setting the data. Then check if the counter is at 0, and run the function that depends on the data.

Related

Wait for promises inside of a angular.forEach loop

I know this has been asked quite a few times already but after a day of search I still don't get it to work, although it's just like what is shown as a solution everywhere...
I have a async request to a database which returns an array of data. For each object in this array I need to start another async request to the database and as soon as ALL of these async requests resolve, I want to return them. I read you could do it with $q.all(...)
So here's the code:
Factory.firstAsyncRequest(id).then(function (arrayWithObjects) {
var promises = [];
var dataArr = [];
angular.forEach(arrayWithObjects, function (object, key) {
var deferred = $q.defer();
promises.push(deferred);
Factory.otherAsyncRequest(key).then(function (objectData) {
dataArr.push({
name: objectData.name,
key: key,
status: objectData.status
});
deferred.resolve();
console.info('Object ' + key + ' resolved');
});
});
$q.all(promises).then(function () {
$rootScope.data = dataArr;
console.info('All resolved');
});});
From the console I see that the $q.all is resolved BEFORE each object. Did I get something wrong? This seems to work for everyone...
Your help is highly appreciated, been looking the whole night, it's 5:30am now lol..
Cheers
EDIT:
So for anyone who's coming here later: It was just the promises.push(deferred.PROMISE) bit. Tho, I read that anguar.forEach is actually not a recommended method to loop through array because it was originally not constructed to be used by the end-user. Don't know if that's correct but I figured out another way if you don't want to use angular.forEach:
Users.getAll(uid).then(function (users) {
var uids = ObjHandler.getKeys(users); //own function just iterating through Object.keys and pushing them to the array
var cntr = 0;
function next() {
if (cntr < uids.length) {
Users.getProfile(uids[cntr]).then(function (profile) {
var Profile = {
name: profile.name,
key: uids[cntr],
status: profile.status
});
dataArr[uids[cntr]] = Profile;
if(cntr===uids.length-1) {
defer.resolve();
console.info('Service: query finished');
} else {cntr++;next}
});
}
}
next();
});
And the getKey function:
.factory('ObjHandler', [
function () {
return {
getKeys: function(obj) {
var r = [];
for (var k in obj) {
if (!obj.hasOwnProperty(k))
continue;
r.push(k)
}
return r
}
};
}])
Instead of
promises.push(deferred);
Try this:
promises.push(deferred.promise);

Promise in JavaScript (angular)

I'm quite new to angular and http calls.
Then i read something about Promise to aync calls. And i cannot understand it at all.
So i need some help to know if im even going the right direction.
I'm programming an API for getting Videoes, and details about them (Views etc) with Youtube API v3.
But i seem to get an error with the getting the details since my array is empty all the time.
/*var promises = [];*/ // PROMISE
var videometrics;
var videodetails = [];
var deferred = $q.defer();
$http.get('https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=' + playlistId + '&key=AIzaSyDQv-WpATIWLinCB3H_sH4W1sKx7plyvRA')
.success(function (response) {
for (var i = 0; i < response.items.length; i++) {
var video = {
id: response.items[i].snippet.resourceId.videoId,
title: response.items[i].snippet.title
};
$scope.video.push(video)
}
/*console.log($scope.video)
var promises = [];*/
for (i = 0; i < $scope.video.length; i++) {
/*console.log("looping")
console.log($scope.video)
console.log("Vi henter fra id:")
console.log($scope.video[i].id)*/
$http.get('https://www.googleapis.com/youtube/v3/videos?part=statistics&id=' + $scope.video[i].id + '&key=AIzaSyDQv-WpATIWLinCB3H_sH4W1sKx7plyvRA')
.success(function (responsevideo) {
/*console.log($scope.video[i].id);*/
// console.log("we are in the metric loop")
// console.log($scope.video[i].id)
// console.log($scope.video[i].title)
// console.log(responsevideo)
videometrics = {
id: responsevideo.items[0].id,
views: responsevideo.items[0].statistics.viewCount,
likes: responsevideo.items[0].statistics.likeCount,
dislikes: responsevideo.items[0].statistics.dislikeCount,
favorites: responsevideo.items[0].statistics.favoriteCount,
comments: responsevideo.items[0].statistics.commentCount
};
videodetails.push(videometrics);
deferred.resolve(responsevideo);
/*detailsOnVideos = $scope.videometrics;*/
})
/* videodetails.push(videometrics);*/
}
/*promises.push(videodetails);*/ // PROMISE
console.log(videodetails);
/* console.log(promises);*/ //PROMISE LOGGER
/* console.log(videodetails);*/
console.log($scope.video);
console.log("")
pagetokenarr = response.nextPageToken;
console.log(pagetokenarr)
});
return deferred;
/*return $q.all(promises);*/ // PROMISE
as you see my first http get, is functionel, but the next one is not. And i cant understand why. But if i push my videodetails array into a promise array it works. And again. I donno why.
You don't need $q with $http. There's basically two ways (that I know of) to work with $http:
1) make a service, isolate $http there. Return the promise as such:
return $http.get({params...}).then(function(data) {
return data.result;
});
then in the controller you'd have:
SuperService.get(params...).then(function(data) { $scope.something = data; });
2) databinding! make a service, put the $http in there. But this time you bind the result to an object inside the service. In the service you also have a method to return that object.
$http.get({params...}).then(function(data) {
myObject = data.result;
});
Then in the controller:
$scope.spiderman = SuperService.getObj();
SuperService.get(params...);
When you call .get() AngularJS does its magic and updates $scope.spiderman (as soon as you get a response from the server, of course).
Relevant literature:
ngDocs
"We have a problem with promises" by Lawson
Here is a solution working in this plunker
Your code was pretty much working. The problem was that your console.log() was fired too early because it wasn't in a .success() or .then() of your promises. It was fired before the $http calls resolved.
I reworked a bit your code (specially theses for loops) to make it more readable.
Also you don't need to build your own promises till $http calls return promises
Here is the final code :
$http.get('https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=' + playlistId + '&key=AIzaSyDQv-WpATIWLinCB3H_sH4W1sKx7plyvRA')
.success(function (response) {
angular.forEach(response.items, function(item){
var video = {
id: item.snippet.resourceId.videoId,
title: item.snippet.title
};
$scope.video.push(video);
})
console.log($scope.video);
angular.forEach($scope.video, function(video){
httpcalls.push($http.get('https://www.googleapis.com/youtube/v3/videos?part=statistics&id=' + video.id + '&key=AIzaSyDQv-WpATIWLinCB3H_sH4W1sKx7plyvRA')
.success(function (responsevideo) {
videometrics = {
id: responsevideo.items[0].id,
views: responsevideo.items[0].statistics.viewCount,
likes: responsevideo.items[0].statistics.likeCount,
dislikes: responsevideo.items[0].statistics.dislikeCount,
favorites: responsevideo.items[0].statistics.favoriteCount,
comments: responsevideo.items[0].statistics.commentCount
};
$scope.videodetails.push(videometrics);
}));
});
pagetokenarr = response.nextPageToken;
$q.all(httpcalls).then(function(){
console.log($scope.videodetails);
})
});
Note that i push all the $http call into a collection (httpcalls) and wrap your console.log into a $q.all(httpcalls).then() function. This will wait until all the $http calls into the collection are resolved.
Hope it helped.
Your code seems to working fine. I have removed all but the essentials, added the variables that I needed as placeholders as I'm not sure of the playlistId etc. Here is the code that I have tested.
var app = angular.module('app', []);
app.controller('ctrl', function ($scope, $http) {
var videometrics;
$scope.videodetails = [];
var playlistId = "PL63F0C78739B09958";
$scope.video = [];
$http.get('https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=' + playlistId + '&key=AIzaSyDQv-WpATIWLinCB3H_sH4W1sKx7plyvRA')
.success(function (response) {
for (var i = 0; i < response.items.length; i++) {
var video = {
id: response.items[i].snippet.resourceId.videoId,
title: response.items[i].snippet.title
};
$scope.video.push(video);
}
for (i = 0; i < $scope.video.length; i++) {
$http.get('https://www.googleapis.com/youtube/v3/videos?part=statistics&id=' + $scope.video[i].id + '&key=AIzaSyDQv-WpATIWLinCB3H_sH4W1sKx7plyvRA')
.success(function (responsevideo) {
var videometrics = {
id: responsevideo.items[0].id,
views: responsevideo.items[0].statistics.viewCount,
likes: responsevideo.items[0].statistics.likeCount,
dislikes: responsevideo.items[0].statistics.dislikeCount,
favorites: responsevideo.items[0].statistics.favoriteCount,
comments: responsevideo.items[0].statistics.commentCount
};
$scope.videodetails.push(videometrics);
});
}
});
});
This will display in the browser using {{videodetails}} when it available on the scope.
If you need to work with the data as opposed to merely displaying it you will likely need to use a service as the other answer suggests.

Angular loop through object array

I have the following function which creates an array of objects inside models, however when I come to use models further down the app I'm unable to loop through it's contents to pull out data.
Every loop method I've tried so far containing a single console.log() message just prints out the message once message when models will contain two objects so I think the problem actually lies with the creation of models. If I create a promise and print out the value of models.devices when it's finished processing an empty array is returned.
Any ideas?
var d = devices.split(','),
count = 0,
models = {devices:[]};
angular.forEach(d, function (device, i) {
var index = i;
if (index <= 1) {
var deviceName = device.replace(/ /g,"+").toLowerCase(),
req = '?__url_path_param=' + deviceName;
$http
.get('/api/cheapest_by_name' + req)
.success(function (obj) {
models.devices.push(obj.device);
count++;
});
}
});
$q.all(models).then(function (data) {
apiDeal.multi(data, 3, 2);
});
Then... (in api-deal.factory.js)
function apiDeal($http, $rootScope) {
return {
multi: function (devices, limit, type) {
console.log(devices); // equal to below image
console.log(devices.devices); // equal to '[]'
}
}
}
I then need to loop through devices in apiDeal.multi
You need to keep an array of promises, which should replace the models you're using the $q.all on. It has to be an array of promises.
So change your code to this:
var d = devices.split(','),
count = 0,
models = {devices:[]},
promises = [];
var promise = $http
.get('/api/cheapest_by_name' + req)
.success(function (obj) {
models.devices.push(obj.device);
count++;
});
promises.push(promise);
And then, do:
$q.all(promises).then(function (data) {
apiDeal.multi(data, 3, 2);
});
Simple Fiddle demonstration

Dojo using deferred functions to get data in ajax callback function

I have a function with a return however in the function there is an async request which holds the value that is suppose to be returned by the function. I understand with the nature of async request the function will complete and not return a value while waiting on the async function to complete.
I attempted to use dojo deferred functions to have my function PostInformation() to return a value within the ajax request callback. I am having some issues and i am not sure where my issue is. Under is my code:
Dojo Deferred Function
function PostInformation(){
var hasErrors = false;
var containers = [dijit.byId("container1"), dijit.byId("container2")];
var Employee = {
//data
};
var def = new dojo.Deferred();
def = dojo.xhrPost({
url: 'hello',
content: Employee,
load: function (data) {
formErrors = {
"errors": true,
"fName": "123",
"surname": "456",
"oNames": "789",
"bSurname": "784585"
};
//formErrors = (JSON.parse(data)).formErrors;
$.each(formErrors, function (key, value) {
if (key == 'errors') {
hasErrors = value;
//console.log('hasErrors set to '+value);
}
});
if (hasErrors == true) {
for (var i = 0; i < containers.length; i++) {
var processingContainer = containers[i];
dojo.forEach(processingContainer.getChildren(), function (wid) {
var widgetName = wid.attr('id');
$.each(formErrors, function (key, value) {
if (key == widgetName && value.length > 0) {
var myWidget = dijit.byId(widgetName);
//var wdgName = dijit.byId(widgetName).attr("id");
var myWidgetValue = value;
myWidget.validator = function () {
//console.log('Attribute Name is :' + wdgName + ' Error Value is : ' + myWidgetValue);
//console.log(wdgName + " : "+myWidgetValue);
this.set("invalidMessage", myWidgetValue);
};
myWidget._hasBeenBlurred = true;
myWidget.validate();
}
});
});
}
}
console.log(hasErrors);
def.resolve(hasErrors);
},
error: function(err){
console.log(err);
def.reject(err);
}
});
def.then(function(data){
console.log('In the then function');
//alert('In the def.then and the results is : ' + data);
if(data == true){
return false;
}else{return true;}
},function(err){
return false;
alert('In the def.error and there has been an error ' + err);
});
//return the value of hasErrors here
};
Devdar, you are making heavy wether out of something quite simple. In particular, you don't need to loop through an object to access one of its properties, and the variable hasErrors is not really necessary.
Your code should simplify to something like this :
function PostInformation() {
var $containers = $("#container1, #container2");
var Employee = {
//data
};
return dojo.xhrPost({
url: 'hello',
content: Employee
}).then(function(data) {
data = JSON.parse(data);
var formErrors = data.formErrors;
if(formErrors.errors) {
$containers.each(function(i, c) {
$(c).children().each(function(wid) {
var val = formErrors[wid.id],
myWidget;
if(val) {
myWidget = dijit.byId(wid.id);
myWidget.validator = function() {
this.set("invalidMessage", val);
};
myWidget._hasBeenBlurred = true;
myWidget.validate();
}
});
});
//Send an enhanced error object down the "error" route
throw $.extend(formErrors, {
'message': 'PostInformation(): validation failure'
});
}
//Send the data object down the "success" route
return data;
});
};
PostInformation().then(function(data) {
console.log('PostInformation(): everything went OK');
//access/process `data` here if necessary
//and/or just display a nice "success" message to the user
}, function(err) {
console.error(err.message);
});
Barring mistakes on my part, this code should do everything you want and more. As with your own code, it processes the server's JSON response and returns a Promise, but that's where the similarity stops.
In your code, you seek to return a Promise which is eventually resolved with a boolean to indicate whether or not errors were detected. Whilst this will (if correctly written) meet your immediate needs, it is not the best Promise logic.
In my code, the Promise is resolved only if validation succeeds and rejected if validation fails for whatever reason. Not only is this logically correct behaviour for a Promise (success goes down the success route, and errors go down the error route) but as a bonus should (see note below) also allow you to pass more information to whetever function(s) eventually handle errors. I choose to pass the whole formErrors object enhanced with an error message, thus providing a great deal of freedom in the error handler to display/log/etc as much or as little as is appropriate, and with virtually no assumption inside PostInformation() as to what will happen subsequently. You currently believe that you will only read and act on the boolean formErrors.errors but it could be beneficial to pass as much error data as possible thus allowing yourself the freedom to change your mind at a later date without needing to change anything in PostInformation().
In this regard you can think of PostInformation() as an agent of the server-side service; and like that service, it can be written with incomplete knowledge (or maybe no knowledge at all) of how the (promise of) data/errors it delivers will be used by "consumer code".
NOTE: I have to admit that I'm not 100% familiar with Dojo's Promises, so I'm not sure that a JS plain object can be thrown in the way I indicate. I have found evidence but not proof that it can. For that reason, I am cautious above in saying "your code should simplify to something like this" Anyway, that issue aside, the principle of sending success down the success route and errors down the error route should still apply.
I'd suggest this where you create your own Deferred() object, return it from your PostInformation() function and then register .then() handlers on it so you can pick up the resolve or reject on your own Deferred object that happens inside the PostInformation() function.
The way you had it you were creating your own Deferred() object, but then immediately overwriting it with the xhrPost return result which meant def is now something else and you weren't returning your Deferred from PostInformation() so it can be used outside that function to track the progress.
function PostInformation() {
var hasErrors = false;
var containers = [dijit.byId("container1"), dijit.byId("container2")];
var Employee = {
//data
};
var def = new dojo.Deferred();
dojo.xhrPost({
url: 'hello',
content: Employee,
load: function (data) {
formErrors = {
"errors": true,
"fName": "123",
"surname": "456",
"oNames": "789",
"bSurname": "784585"
};
//formErrors = (JSON.parse(data)).formErrors;
$.each(formErrors, function (key, value) {
if (key == 'errors') {
hasErrors = value;
//console.log('hasErrors set to '+value);
}
});
if (hasErrors == true) {
for (var i = 0; i < containers.length; i++) {
var processingContainer = containers[i];
dojo.forEach(processingContainer.getChildren(), function (wid) {
var widgetName = wid.attr('id');
$.each(formErrors, function (key, value) {
if (key == widgetName && value.length > 0) {
var myWidget = dijit.byId(widgetName);
//var wdgName = dijit.byId(widgetName).attr("id");
var myWidgetValue = value;
myWidget.validator = function () {
//console.log('Attribute Name is :' + wdgName + ' Error Value is : ' + myWidgetValue);
//console.log(wdgName + " : "+myWidgetValue);
this.set("invalidMessage", myWidgetValue);
};
myWidget._hasBeenBlurred = true;
myWidget.validate();
}
});
});
}
}
console.log(hasErrors);
def.resolve(hasErrors);
},
error: function (err) {
console.log(err);
def.reject(err);
}
});
return def.promise;
};
PostInformation().then(function (data) {
console.log('In the then function');
// process data value here which will contain the value you resolved with
}, function(err)
// process an error in the ajax result here
});
I think this is more of an issue with design of the function then.
Since the xHR call is asynchronous, the postInformation shouldn't really return anything unless it's the Deferred object itself. An alternative option is to have postInformation do some sort of event publishing (dojo/topic), that other functions will subscribe to and know how to handle said events.

Return an object from a function that has nested ajax calls

I would like to write a javascript function that returns informations from youtube videos; to be more specific I would like to get the ID and the length of videos got by a search, in a json object. So I took a look at the youtube API and I came out with this solution:
function getYoutubeDurationMap( query ){
var youtubeSearchReq = "https://gdata.youtube.com/feeds/api/videos?q="+ query +
"&max-results=20&duration=long&category=film&alt=json&v=2";
var youtubeMap = [];
$.getJSON(youtubeSearchReq, function(youtubeResult){
var youtubeVideoDetailReq = "https://gdata.youtube.com/feeds/api/videos/";
for(var i =0;i<youtubeResult.feed.entry.length;i++){
var youtubeVideoId = youtubeResult.feed.entry[i].id.$t.substring(27);
$.getJSON(youtubeVideoDetailReq + youtubeVideoId + "?alt=json&v=2",function(videoDetails){
youtubeMap.push({id: videoDetails.entry.id.$t.substring(27),runtime: videoDetails.entry.media$group.media$content[0].duration});
});
}
});
return youtubeMap;
}
The logic is ok, but as many of you have already understood because of ajax when I call this function I get an empty array. Is there anyway to get the complete object? Should I use a Deferred object? Thanks for your answers.
Yes, you should use deferred objects.
The simplest approach here is to create an array into which you can store the jqXHR result of your inner $.getJSON() calls.
var def = [];
for (var i = 0; ...) {
def[i] = $.getJSON(...).done(function(videoDetails) {
... // extract and store in youtubeMap
});
}
and then at the end of the whole function, use $.when to create a new promise that will be resolved only when all of the inner calls have finished:
return $.when.apply($, def).then(function() {
return youtubeMap;
});
and then use .done to handle the result from your function:
getYoutubeDurationMap(query).done(function(map) {
// map contains your results
});
See http://jsfiddle.net/alnitak/8XQ4H/ for a demonstration using this YouTube API of how deferred objects allow you to completely separate the AJAX calls from the subsequent data processing for your "duration search".
The code is a little long, but reproduced here too. However whilst the code is longer than you might expect note that the generic functions herein are now reusable for any calls you might want to make to the YouTube API.
// generic search - some of the fields could be parameterised
function youtubeSearch(query) {
var url = 'https://gdata.youtube.com/feeds/api/videos';
return $.getJSON(url, {
q: query,
'max-results': 20,
duration: 'long', category: 'film', // parameters?
alt: 'json', v: 2
});
}
// get details for one YouTube vid
function youtubeDetails(id) {
var url = 'https://gdata.youtube.com/feeds/api/videos/' + id;
return $.getJSON(url, {
alt: 'json', v: 2
});
}
// get the details for *all* the vids returned by a search
function youtubeResultDetails(result) {
var details = [];
var def = result.feed.entry.map(function(entry, i) {
var id = entry.id.$t.substring(27);
return youtubeDetails(id).done(function(data) {
details[i] = data;
});
});
return $.when.apply($, def).then(function() {
return details;
});
}
// use deferred composition to do a search and then get all details
function youtubeSearchDetails(query) {
return youtubeSearch(query).then(youtubeResultDetails);
}
// this code (and _only_ this code) specific to your requirement to
// return an array of {id, duration}
function youtubeDetailsToDurationMap(details) {
return details.map(function(detail) {
return {
id: detail.entry.id.$t.substring(27),
duration: detail.entry.media$group.media$content[0].duration
}
});
}
// and calling it all together
youtubeSearchDetails("after earth").then(youtubeDetailsToDurationMap).done(function(map) {
// use map[i].id and .duration
});
As you have discovered, you can't return youtubeMap directly as it's not yet populated at the point of return. But you can return a Promise of a fully populated youtubeMap, which can be acted on with eg .done(), .fail() or .then().
function getYoutubeDurationMap(query) {
var youtubeSearchReq = "https://gdata.youtube.com/feeds/api/videos?q=" + query + "&max-results=20&duration=long&category=film&alt=json&v=2";
var youtubeVideoDetailReq = "https://gdata.youtube.com/feeds/api/videos/";
var youtubeMap = [];
var dfrd = $.Deferred();
var p = $.getJSON(youtubeSearchReq).done(function(youtubeResult) {
$.each(youtubeResult.feed.entry, function(i, entry) {
var youtubeVideoId = entry.id.$t.substring(27);
//Build a .then() chain to perform sequential queries
p = p.then(function() {
return $.getJSON(youtubeVideoDetailReq + youtubeVideoId + "?alt=json&v=2").done(function(videoDetails) {
youtubeMap.push({
id: videoDetails.entry.id.$t.substring(27),
runtime: videoDetails.entry.media$group.media$content[0].duration
});
});
});
});
//Add a terminal .then() to resolve dfrd when all video queries are complete.
p.then(function() {
dfrd.resolve(query, youtubeMap);
});
});
return dfrd.promise();
}
And the call to getYoutubeDurationMap() would be of the following form :
getYoutubeDurationMap("....").done(function(query, map) {
alert("Query: " + query + "\nYouTube videos found: " + map.length);
});
Notes:
In practice, you would probably loop through map and display the .id and .runtime data.
Sequential queries is preferable to parallel queries as sequential is kinder to both client and server, and more likely to succeed.
Another valid approach would be to return an array of separate promises (one per video) and to respond to completion with $.when.apply(..), however the required data would be more awkward to extract.

Categories

Resources