JavaScript Recursion with promises -- order of execution error - javascript

Building a method to create markup for a web app off canvas navigation. I am making async callback to another service that returns children of the parent menu node (see code below):
function GenerateMarkup(Terms, n) {
var termsEnum = Terms.getEnumerator();
var html = "<ul>";
// Process top level terms
while (termsEnum.moveNext()) {
var currentTerm = termsEnum.get_current();
html += "<li>"
if (currentTerm.get_termsCount() > 0) {
var childcall = function() {
var deferred = $.Deferred();
html += "" + currentTerm.get_name() + "<br><span>" + currentTerm.get_description() + "</span>";
SPTermStore.GetTermsFromTermSet(currentTerm).then(function(termSet) {
if (typeof termSet !== undefined) {
deferred.resolve(GenerateMarkup(termSet, n++));
}
else
deferred.reject("something bad happened");
});
return deferred.promise();
};
$.when(childcall()).done(function(markup) {
html += markup;
});
} // end if
else
html += "" + currentTerm.get_name() + "";
html += "</li>"
} // end while
html += "</ul>";
console.log("GenerateMarkup (" + n + "): " + html);
return html;
} // end function
The issue is the order the markup is generated is not right; in a normal synchronous the recursive call to GenerateMarkup would complete, but in this situation I am trying to wait for the returned promise (i.e. the call to GenerateMarkup to complete) so I can append the html. The idea is as it iterates through the while, top level nodes will have their child nodes processed etc.
If I look at the console.log output this is what I get; the problem is the first listed markup below is what is returned to the page and not the combination of the below.
GenerateMarkup (0): <ul><li>About<br><span>Our Company</span></li><li>Portfolio<br><span>Our Properties</span></li><li>Corporate Responsibility<br><span>Our Committment</span></li></ul>
GenerateMarkup (0): <ul><li>Careers</li><li>Core Values</li><li>Governance</li><li>History</li></ul>
GenerateMarkup (1): <ul><li>Core Market Strategy</li><li>Our Properties</li></ul>
GenerateMarkup (2): <ul><li>Community Involvement</li><li>CSR Report</li><li>Diversity</li><li>Sustainability</li></ul>
Any help would be appreciated.

Promises are asynchronous so they're not guaranteed to return in the order in which they're promised.
If the ordering is important, consider chaining the promises so that they execute in the sequence that you expect. Promise.then can be chained.

One possibility would be to loop through the termsenumerator, and load these into a deferred array. Then apply the $when, later. See sample below:
var deferreds = [];
for (var i = 0; i<termsenumerator; i++) {
deferreds.push(Grab data you want and push it into this array);
}
//Now that we have all the results... (deferred), process these.
$.when.apply($, deferreds).done(function () {
var resultdata = [];
for (var i = 0; i < arguments.length; i++) {
var daydata = arguments[i][2];
applymarkupWData(resultdata.responseJSON.d,i);
};
});

Related

Wait until multiple asynchronous functions complete before executing

I have a Javascript for loop which runs through an array of database records (that have been extracted already).
I want to know when all the subsequent asynchronous actions have completed but I can't seem to do it.
For each record, the code runs a number of functions which return promises and then resolve (which then triggers another function to get more information, etc). This all works ok, but I can't figure out how to gather up each "FOR" iteration and detect when all records have been processed. Basically, I want to use a "throbber" and have the throbber remain until all processing has been completed.
Code is below (I've removed some extraneous info)...
for (var i = 0; i < systemArray.length; i++) {
// ***** FOR EACH SYSTEM ***** //
var currRecord = systemArray[i];
// SECTION REMOVED //
// GET SYSTEM LINES
var thisSystem = AVMI_filterArray("8.9", currRecord);
var thisSystemName = thisSystem[1].value;
var thisSystemRID = thisSystem[0].value;
// GET CHILDREN RIDS
AVMI_getChildren(systemLinesTable, thisSystemRID, systemLinesFID).done(function(ridList, sysRID)
{
var thisDiv = "div#" + sysRID;
// GET RECORD INFO FOR EACH RID
AVMI_getMultipleRecordInfoFromArray(ridList, systemLinesTable).done(function(systemLinesArray)
{
if (systemLinesArray != "" && systemLinesArray != null) {
systemLinesArray = systemLinesArray.sort(propComparator("10"));
x = AVMI_tableCombiner("System Lines", systemLinesArray, systemLinesCLIST, "skip3Right hbars xsmallText");
$(thisDiv).append(x);
} else {
$(thisDiv).append("<p>No System Lines...</p>");
}
}
);
}
);
} // ***** FOR EACH SYSTEM ***** //
AVMI_throbberClose(); // THIS, OF COURSE, EXECUTES ALMOST IMMEDIATELY
Here is function 1
///////////////////////////////////////////////////////////////
// Get related records using master
///////////////////////////////////////////////////////////////
function AVMI_getChildren(AVMI_db, AVMI_rid, AVMI_fid, AVMI_recText) {
var AVMI_query = "{" + AVMI_fid + ". EX. " + AVMI_rid + "}";
var AVMI_ridList = [];
var dfd2 = $.Deferred();
$.get(AVMI_db, {
act: "API_DoQuery",
query: AVMI_query,
clist: "3",
includeRids: "1"
}).then(function(xml1) {
$(xml1).find('record').each(function(){
var AVMI_record = $(this);
var AVMI_childRID = AVMI_record.attr("rid");
AVMI_ridList.push(AVMI_childRID);
});
AVMI_throbberUpdate("Found " + AVMI_ridList.length + " " + AVMI_recText + "...");
dfd2.resolve(AVMI_ridList, AVMI_rid);
});
return dfd2.promise();
};
And function 2
///////////////////////////////////////////////////////////////
// Get record info for each array member
///////////////////////////////////////////////////////////////
function AVMI_getMultipleRecordInfoFromArray(ridList, AVMI_db, AVMI_recType) {
var promises = [];
var bigArray = [];
$.each(ridList, function (index,value) {
var def = new $.Deferred();
var thisArray = [];
$.get(AVMI_db, { //******* ITERATIVE AJAX CALL *******
act: 'API_GetRecordInfo',
rid: value
}).then(function(xml2) {
AVMI_throbberUpdate("Got " + AVMI_recType + " " + value + "...");
$(xml2).find('field').each(function() {
var $field = {};
$field.fid = $(this).find('fid').text();
$field.name = $(this).find('name').text();
$field.value = $(this).find('value').text();
thisArray.push($field);
});
thisArray = thisArray.sort(AVMI_ArrayComparator);
bigArray.push(thisArray);
def.resolve(bigArray);
});
promises.push(def);
});
return $.when.apply(undefined, promises).promise();
};
Any ideas of how to structure this? I've tried all sorts of things with $.Deferred but I can't quite figure it out...
You do exactly the same thing you did in AVMI_getMultipleRecordInfoFromArray: Collect the promises in an array and use $.when (or Promise.all) to wait until they are resolved.
You can simply use .map in here which also takes care of the "function in a loop" problem:
var promises = systemArray.map(function(currRecord) {
// ...
return AVMI_getChildren(...).done(...);
});
$.when.apply(undefined, promises).done(function() {
AVMI_throbberClose();
});
You should have to disable the async property of ajax. By default it is set to true. It means that your doesn't wait for your ajax response. That why it is returning you undefined value and you have to set it to false. So your code will wait your request to complete.
So all you have to do is.
$.ajax({
url: '',
type: '',
async: false,
success: function(data){
}
});

Next iteration of $.each when received AJAX-content

The question has been asked before, but it is almost four years ago and maybe there is a better solution.
I have a $.each-loop where sometimes additional data is being fetched via ajax.
I am bulding an object with the fetched data, after the loop there is a function that generates HTML from the object. The problem is that the loop finishes before the ajax data arrives. If I place an alert in the HTML-generating-function the content is loading properly.
I am searching for a solution that calls the HTML-generator-function only when the loop and all ajax calls are finished. Maybe it is a solution to count the started Ajax requests and wait if all of them are finished?
I believe jQuery deferred is the right solution for me but I do find only examples where everything stays inside the loop. Can someone help?
I have stripped down my code to the most important things:
//goes through each testplace -->main loop
$.each(jsobject, function(key, value)
{
//build object together...
for (var i = 0, numComputer = jenkinsComputer.contents.computer.length; i < numComputer; i++)
{
//If the testplace is in both objects then fire AJAX request
if (jenkinsComputer.contents.computer[i].displayName == key) //<<<This can happen only once per $.each loop, but it does not happen every time
{
//next $.each-iteration should only happen when received the JSON
var testplaceurl = jenkinsComputer.contents.computer[i].executors[0].currentExecutable.url;
$.when($.getJSON("php/ba-simple-proxy.php?url=" + encodeURI(testplaceurl) + "api/json?depth=1&pretty=1")).done(function(jenkinsUser)
{
//build object together...
});
}
}
}); //End of main Loop ($.each)
generateHTML(builtObject);
It would be great if someone could give me an advice how to do it.
I would do something like this:
var thingstodo = $(jsobject).length;
var notfired = true;
$.each(jsobject, function(key, value)
{
//build object together...
for (var i = 0, numComputer = jenkinsComputer.contents.computer.length; i < numComputer; i++)
{
//If the testplace is in both objects then fire AJAX request
if (jenkinsComputer.contents.computer[i].displayName == key) //<<<This can happen only once per $.each loop, but it does not happen every time
{
//next $.each-iteration should only happen when received the JSON
var testplaceurl = jenkinsComputer.contents.computer[i].executors[0].currentExecutable.url;
$.when($.getJSON("php/ba-simple-proxy.php?url=" + encodeURI(testplaceurl) + "api/json?depth=1&pretty=1")).done(function(jenkinsUser)
{
//build object together...
thingstodo--;
if(thingstodo === 0 && notfired){
notfired = false;
generateHTML(buildObject);
}
});
}else{
thingstodo--;
}
}
}); //End of main Loop ($.each)
if(thingstodo === 0 && notfired){
generateHTML(buildObject);
}
This is short untested example about the solution. I hope this to give you idea.
// I guess that jsobject is array ..
// if it is not object you can use something like:
// var keys = Object.getOwnPropertyNames(jsobject)
(function () {
var dfd = $.Deferred();
function is_not_finished() {
return jsobject.length > 0 && jenkinsComputer.contents.computer.length > 0;
}
(function _handleObject() {
var key = jsobject.shift();
var displayName = jenkinsComputer.contents.computer.shift().displayName;
if (displayName == key) //<<<This can happen only once per $.each loop, but it does not happen every time
{
//next $.each-iteration should only happen when received the JSON
var testplaceurl = jenkinsComputer.contents.computer[i].executors[0].currentExecutable.url;
$.getJSON("php/ba-simple-proxy.php?url=" + encodeURI(testplaceurl) + "api/json?depth=1&pretty=1").done(function(jenkinsUser)
{
//build object together...
if(is_not_finished()) {
setTimeout(_handleObject,0);
} else {
dfd.resolve();
}
});
} else if (is_not_finished()) {
setTimeout(_handleObject,0);
} else {
dfd.resolve();
}
}());
return dfd.promise();
}()).done(function () {
generateHTML(builtObject);
});

JS, multiple JSON requests and callback functions

I need some help. I'm quite new to Javascript, I'm currently trying to do something using two differents api: SongKick and Deezer. The idea is simple, on the page you can type your city, 1) I do a first request to the Songkick Api to get the ID of this city, 2) then with the ID, I do another request to get a list of concerts and I only take the name of the artist (20 maximum), 3) then I with the list of names I use the deezer Api to get the picture of the artist and a mp3 preview.
I've tried many ways but I can't access the data everywhere of course, and I don't know how to use callback cause there is too many things, if you can take a look that would be awesome.
Thanks!
artistsArray = [];
artistsArray2 = [];
arr = [artistsArray,[],[]];
var dispName;
var areaId;
function search(){
area = document.getElementById('band').value;
function songKickArea(callback){
$.getJSON('http://api.songkick.com/api/3.0/search/locations.json?query=' + area + '&apikey=tIhpFoFn0dWpQ72A',function(data){
var areaId = data['resultsPage']['results']['location'][0].metroArea.id;
callback(areaId);
});
console.log("1 is done");
}
function findAreaId(callback){
songKickArea(function(callback){
console.log(callback);
});
$.getJSON("http://api.songkick.com/api/3.0/metro_areas/" + areaId + "/calendar.json?apikey=tIhpFoFn0dWpQ72A",function(data){
for (var i=0; i<20 ; i++)
{
artistsArray.push(data['resultsPage']['results']['event'][i].performance[0].displayName);
}
callback(artistsArray);
});
console.log("2 is done");
}
function addInfos(callback){
for (var i=0; i<20 ; i++)
{
DZ.api('/search?q=artist:' + '"'+ artistsArray[i]+'"' +'?limit=1', function(json){
if(json.data[0]){
artistsArray2.push({preview:json.data[0].preview, picture: json.data[0].artist.picture})
}
});
}
console.log("3 is done");
callback();
}
function runSearchInOrder(callback) {
songKickArea(function() {
findAreaId(function() {
addInfos(function() {
console.log(areaId);
});
});
});
}
runSearchInOrder(function(){console.log('finished')});
}
EDIT 09/17/2015
Thanks Vittore, I took a look at promises in JS and it's very interesting and perfect in my case. So now I'm here :
function songKickArea(areaId){
area = document.getElementById('band').value;
return $.getJSON('http://api.songkick.com/api/3.0/search/locations.json?query=' + area + '&apikey=XXXXXXX',function(data){
});
}
function findAreaId(data){
var areaId = data['resultsPage']['results']['location'][0].metroArea.id;
return $.getJSON("http://api.songkick.com/api/3.0/metro_areas/" + areaId + "/calendar.json?apikey=XXXXXXX",function(data){
});
}
function addInfos(data){
for (var i=0; i<20 ; i++)
{
artistsArray.push(data['resultsPage']['results']['event'][i].performance[0].displayName);
DZ.api('/search?q=artist:' + '"'+ artistsArray[i]+'"' +'?limit=1', function(json){
if(json.data[0]){
artistsArray2.push({preview:json.data[0].preview, picture: json.data[0].artist.picture})
}
});
}
}
And I use this onClick:
songKickArea().then(findAreaId).then(addInfos).then(createList);
So everything is working fine, in addInfos my array artistsArray2 get all the infos I need from deezer (preview and picture). But now the next step is to create a list to display these artists (or tracks) so the next function is like this.
function createList(json){
var html = '<ul>';
for (var i=0; i<17; i++)
{
html+= '<li>';
html += '<div class="picture">' + '<div class="player"><img src="svg/play43.svg" ></div>'+ '<a href=' + artistsArray2[i].preview + '>' + '<img src=' + artistsArray2[i].picture + '>' + '</a>' +'</div>';
html+= '<div class="arrow"><img src="css/svg/arrow487.svg"></div>';
html+= '</li>';
}
html+= '</ul>';
$('#results').append(html);
}
But here I have no idea how to pass the value of a full array from the last function to this one, could you help me ? Thanks a lot !
UPDATE: Little clarification on multiple calls to the services and array of results.
You original code has addInfos method that iterates ( for loop ) through array and calling web service in that loop. What you want to do is to get results of each of those calls all together. While there are many ways of doing that, what I am showing you is using array.map to "map" each element of an array with data from step X to the promise returned by AJAX call. Let me give an example:
Say you have an array:
var artistIds = [6664009,6664010,6664011]
Now you can map it to promises:
var artistCalls = artistIds.map(function(id) {
return $.getJson('~ get artists data service url ~' + id)
}
Which will give you array artistCalls each element of which will eventually have resolved promise with the data you need. While you can do all the crazy stuff with it, the easiest way of getting data from ALL calls is to use $.when helper method:
$.when(artistCalls).then(function(artists) {
// here artists will array where each element is data returned by each AJAX call
})
Now if you want to render html to show all artists on the page you might have code like that:
function renderArtistHtml(artist) {
return '<li>'
+= '<div class="picture"><div class="player"><img src="svg/play43.svg" ></div><img src="' + artistsArray2[i].picture + '"></div>'
+= '<div class="arrow"><img src="css/svg/arrow487.svg"></div>'
+= '</div></li>';
}
And a function that renders entire list:
function renderAllArtistsHtml(artists) {
return '<ul>' + artists.map(renderArtistHtml) + '</ul>'
}
Now that you have it you can create whole chain of your functions together:
$(... my button selector ...).on('click',function(e) {
var area = ... get area
songKickArea(area)
.then(findAreaId) // this thing returns promise that returns array
.then(addInfos) // this thing returns $.when(arr.map(...))
.then(renderAllArtistsHtml) // this thing converts array of data from all calls from previous step to html layout
.then(function(html) { // this part just adds it to DOM
$('#results').append(html);
});
})
Just answered similar question here.
Basically every ajax method in jquery returns promise ( and if your api does not return promise (like DZ.api ) you can wrap it in a $.deferred )
Once you return promises from your functions you can chain them:
function myajax1() {
return $.getJson(...)
}
function myajax2(data) {
return $.getJson(...)
}
myajax1().then(myajax2)
This will call myajax2 with data returned by myajax1 ajax call
You can chain it as many times as you want.
In case you need to wait for several you can use $.when:
$.when([myajax11(), myajax12()]).then(myajax2)
So closer to your actual code, you have 3 api calls:
songkick locations
songkick metro_areas
DZ.api
last one will require wrapping up in a promise, see example here: https://learn.jquery.com/code-organization/deferreds/examples/
Declare 3 functions:
function getLocations(area) {
return $.getJson(....) // location query
}
function getMetroArea(data) {
var areaId = data['resultsPage']['results']['location'][0].metroArea.id
return $.getJson(...) // metro query
}
function getArtists(data) {
var artist = data['resultsPage']['results']['event'][i].performance[0].displayName
return DZAPIWraper(...)
}
And chain them:
getLocations(...).then(getMetroArea).then(getArtists)
In case you really need to do several calls for several artists in the last step, your code will look similar to:
function getArtists(data) {
var artists = getArtistsArrayFromMetro(data)
var artistsCallbacks = artists.map(function(a) {
return DZAPIWrapper(...)
})
return $.when(artistCallbacks)
}
and that full chain is:
getLocations(...).then(getMetroArea).then(getArtists).then(function(artists) {
// here artists going to be all artists data from all api calls to DZ.api
})

Variable scope or return issue (not sure which)

Using the script below I'm attempting to create an object called temptagarray which gets populated with all the tags on a Tumblr weblog and their frequency. So it should end up looking like this:
{'performance': 10, 'installation': 5}
I know the object is being created and it looks correct (I can print it out in each loop) but I can't figure out how to use it after/outside the function i.e. at the bottom of the script where I attempt to document.write() it out. Is this a global/local variable issue, a return issue or do I need to address it in some way?
<script type="text/javascript">
var temptagarray = {};
var tags;
var tag;
function loadPosts () {
var key = "api_key=9I4rZAYQCbU1o5TSMZuyrlvXiQsNxKBicCJxNK5OKZ6G9pgdim";
var api = "https://api.tumblr.com/v2/blog/garrettlynch.tumblr.com/";
var retrieve_more = function (offset) {
$.getJSON(api + "posts?callback=?&filter=image&limit=20&offset=" + offset + "&" + key,function(data) {
//for each item (post) in the response
$.each(data.response.posts, function(i, item) {
//pull out the posts tags
tags = item['tags'];
//loop through the tags
for (i = 0; i < tags.length; i++)
{
tag = tags[i];
//if the tag already exists in the tag array
if (temptagarray[tag])
{
temptagarray[tag] = temptagarray[tag] + 1;
}
else
{
temptagarray[tag] = 1;
}
}
});
if (data.response.posts.length == 20) {
retrieve_more(offset + 20);
}
});
};
retrieve_more(0);
}
loadPosts();
document.write(JSON.stringify(temptagarray));
</script>
Thanks in advance
Garrett
Replace this:
if (data.response.posts.length == 20) {
retrieve_more(offset + 20);
}
...with this:
if (data.response.posts.length == 20) {
retrieve_more(offset + 20);
} else {
document.write(JSON.stringify(temptagarray));
}
The problem you're having is that, despite your document.write(...) command being located below the ajax call in your code, the ajax call is asynchronous and thus the callback will be invoked asynchronously as well. Basically, document.write(...) is being invoked long before you've had a chance to interact with the temptagarray variable in the ajax callback.
First things first - AJAX is Async Asynchronous.
So the code block does not wait for the previous instruction to be completed before it executes the next line.
So your document.writeline would have already been executed by the time the response comes back.
Try printing that info in the success call back after the if block and you would indeed see the response.
thanks for the replies. Below is what I have now as a workable solution as the result is going to call another function anyway. Reading a little bit more I'm wondering if I should be using a callback - is it better?
<script type="text/javascript">
//load posts from a Tumblr weblog
function loadPosts () {
//api key and weblog address
var key = "api_key=9I4rZAYQCbU1o5TSMZuyrlvXiQsNxKBicCJxNK5OKZ6G9pgdim";
var api = "https://api.tumblr.com/v2/blog/garrettlynch.tumblr.com/";
//tags object
var temptagarray = {};
//all tags and each tag
var tags;
var tag;
//looping function to keep retrieving posts until all are retrieved
var retrieve_more = function (offset) {
$.getJSON(api + "posts?callback=?&filter=image&limit=20&offset=" + offset + "&" + key,function(data) {
//for each item (post) in the response
$.each(data.response.posts, function(i, item) {
//pull out the posts tags
tags = item['tags'];
//loop through the tags
for (i = 0; i < tags.length; i++)
{
//pull out each tag
tag = tags[i];
//if the tag already exists in the tag array
if (temptagarray[tag])
{
//add 1 to its count
temptagarray[tag] = temptagarray[tag] + 1;
}
else
{
//set its count to 1
temptagarray[tag] = 1;
}
}
//to test object as it gets added to
//$("#Posts ul").append('<li>' + JSON.stringify(item, ['tags']) + '</li>')
});
//if the number of posts is more than 20
if (data.response.posts.length == 20)
{
//retrieve the next 20
retrieve_more(offset + 20);
}
else
{
//call the show result function
showresult(temptagarray);
}
});
};
//stop retrieving posts
retrieve_more(0);
}
loadPosts();
function showresult(tagarray)
{
$("#Posts ul").append('<li>' + JSON.stringify(tagarray) + '</li>');
//document.write(JSON.stringify(tagarray));
}
</script>

Async.js - Deferring execution while still accessing the correct array index

I'm trying to us Async.js to process an array of items. Is there some cute way to get this to work properly? If you're smarter than I, you'd except that because of the deferred execution, http://3 gets printed three times.
jsFiddle link
var a_servers = ['http://1', 'http://2', 'http://3'];
var a_actions = [];
for (var i = 0; i < a_servers.length; i += 1)
{
var server = a_servers[i];
a_actions.push(function(callback)
{
document.write(server + '<br/>');
callback(false, server );
});
}
async.series(a_actions, function(err, a_servers)
{
document.write('processed ' + a_servers.length + ' servers<br>');
console.info(a_servers);
});​
You have a classic closure scope issue. You need to pass the server variable from the outer scope to the inner scope to get the desired behavior. You can use an IIFE for this.
for (var i = 0; i < a_servers.length; i += 1)
{
var server = a_servers[i];
a_actions.push((function(server) {
return function(callback) {
document.write(server + '<br/>');
callback(false, server );
})(server));
}

Categories

Resources