Link to home
Start Free TrialLog in
Avatar of Marco Gasi
Marco GasiFlag for Spain

asked on

jQuery promises and variable scope issue

This is a follow up to discussion in https://www.experts-exchange.com/questions/28923510/push-strange-behavior.html?anchorAnswerId=41448325#a41448325
Here the original code:
function getFolders2() {
	var foldersContext = {cFolders: []};
	var customFolders = {};
	getFolderCardsNumber('PC').success(function (result) {
		var pcContext = {pc_cards_number: result};
		jQuery.extend(foldersContext, pcContext);
	});
	getFolderCardsNumber('SP').success(function (result) {
		var spContext = {sp_cards_number: result};
		jQuery.extend(foldersContext, spContext);
	});
	getCustomFolders().success(function (data) {
		var response = JSON.parse(data);
		if (response.success) {
			var folders = response.result;
			var obj = [];
			if (folders.length > 0) {
				var i = 0, 
					l = folders.length;
				for (i;i < l;i++) {
					var id = folders[i].folder_id;
					var description = folders[i].description;
					getFolderCardsNumber(id).success(function (result) {
						jQuery.extend(customFolders, {
							fd_cards_number: result 
						});
					});
					jQuery.extend(customFolders, {
						folder_id: id, 
						folder_description: description
					});
					obj.push(customFolders);
					console.log('customFolders');
					console.log(customFolders);
					console.log('obj');
					console.log(obj);
				}
				jQuery.extend(foldersContext, {cFolders: obj});
				console.log('context:');
				console.log(foldersContext);
				var html = parseAndReturnHandlebarsRev2("folders_all", foldersContext);
				console.log(html);
				jQuery('.all-folders').html(html);
			}
		}
	});
};

Open in new window


In topic linked above Sergio and Alex suggested I could avoid the use of $.extend to fill my array 'obj'. But i have a problem doing so.
Look at lines 21-33 of the snippet above (within a for loop)
var id = folders[i].folder_id;
var description = folders[i].description;
getFolderCardsNumber(id).success(function (result) {
	jQuery.extend(customFolders, {
 		fd_cards_number: result 
	});
});
jQuery.extend(customFolders, {
	folder_id: id, 
       folder_description: description
});
obj.push(customFolders);

Open in new window

I use $.extend because variables id and description are not available within getFolderCardsNumber(id).success and variable fd_cards_number is not availbel outside of it, but I need to put in the obj array an object with all these three properties!
Any thought?

What I noticed is that if I follow Sergio suggesiton and create a new customFolders object every loop I get all elements in array obj but they are totally missing the fd_cards_number property; if I create customFolders object at the start of the function, property fd_cards_number is correctly retrieved for each folder but then $.extend overwrite the array so I have an array with many identical elements...
Avatar of Julian Hansen
Julian Hansen
Flag of South Africa image

As a matter of interest why not do this
customFolders.fd_cards_number = result;

Open in new window

Instead of this
jQuery.extend(customFolders, {
  fd_cards_number: result
}); 

Open in new window


As the other experts mentioned extend is really about merging objects.
Avatar of Marco Gasi

ASKER

Hi Julian. Evidently I didn't fully understand the point of Alex and Sergio :-(
But this doesn't solve the problem I mentioned here Doing this:
var customFolders = {};
var i = 0, 
l = folders.length;
for (i;i < l;i++) {
        //var customFolders = {};
	var id = folders[i].folder_id;
	var description = folders[i].description;
	getFolderCardsNumber(id).success(function (result) {
		customFolders.fd_cards_number = result;
	});
	customFolders.folder_id = id;
	customFolders.folder_description = description;
	console.log(customFolders);
	obj.push(customFolders);
//	customFolders = {};
}
//jQuery.extend(foldersContext, {cFolders: obj});
foldersContext.cFolders = obj;

Open in new window

the result is wrong and the type of error depends on where I declare customFolders object: if I declare it before the loop I get cards number but I get only last element's folder name and id, if I declare it within the loop (where in the snippet is commented) I get folder names and ids correctly but card number is missed.
Please, let me know if this explanation is clear enough to understand the issue otherwise I'll explain it better :-)
But this clarify that the "root of evil" is not $.extend: using it or not gives exactly the same result...
Marco I think you are being tripped up by your original problem.

I am assuming the .success function in your code is linked to a promise or return from an asynch operation?

If that is the case then it is going to execute potentially after the loop has finished.

Your loop starts
You create the object
You call the function to get the remote data and call the success method of the returned object from that call (when it is ready) - which happens some time in the future
While the call is happening in the background you go on to do the rest of the loop.

Now you loop around again - this time you do another asynch call - the first one probably has not finished yet - but you are already on to a new object.
And so on.
Your loop finishes - so what happens is you have a bunch of empty objects that don't have card numbers because they missed the return from the asynch call.

You need to put the assignment of the folder values in the success function - as one possible solution.

Working on your problem - will post code but in the meantime that is what I think your problem is.
Yes, is a promise.
And yes, I agree: I've already tried to assign id and description within success function but it looks like they are undefined... But this was an old trying: I'm going to redo it now
If I do this:
getFolderCardsNumber(id).success(function (result) {
	customFolders.fd_cards_number = result;
	customFolders.folder_id = id;
	customFolders.folder_description = description;
	obj.push(customFolders);
});

Open in new window

I get nothing. If I do this:
getFolderCardsNumber(id).success(function (result) {
	customFolders.fd_cards_number = result;
	customFolders.folder_id = id;
	customFolders.folder_description = description;
});
obj.push(customFolders);

Open in new window

I get cards number but always the same id and description...
Hi Marco,

This seems to work maybe you can adapt for your requirements
<script src="//code.jquery.com/jquery.js"></script>
<script>

var folders = [
  {
    folder_id: 1,
    description: 'Test1'
  },
  {
    folder_id: 2,
    description: 'Test2'
  },
]
var obj = [];
var defferds = []
var i = 0, 
l = folders.length;
for (i;i < l;i++) {
  var customFolders = {};
  customFolders.folder_id = folders[i].folder_id;
  customFolders.folder_description = folders[i].description;
  
  
  def = getFolderCardsNumber(customFolders);
  
  defferds.push(def);
  obj.push(customFolders);
}

$.when.apply($, defferds).done(function() {
  console.log(obj);
});

function getFolderCardsNumber(folder) 
{
  var def = $.ajax({
    url: 't1349.php',
    data: {id: folder.folder_id},
    type: 'POST',
  }).done(function(data) {
    folder.fd_cards_number = data
  });
  
  return def;
}
</script>

Open in new window

PHP script just does
echo rand(1,10);

Open in new window

One thig that has nothing to do with your problem but you need to be careful.
Don't forget the var in your look
// wrong
for (i;i < l;i++) {

// correct
for (var i = 0;i < l;i++) {

Open in new window

Without the var, you're declaring a variable i in the global scope, which might become a very nice problem to debug.

You have this on line 20 of your original question code.
@Julian: thank you: I'm going to refactor following your example and I'll let you know asap

#Alex: yes, I know: var i, l are declared in e previous part of the script. I forgot to include them in the first snippet but yuou can see them in the snippet here :-)
Now for the problem, you're doing the same mistake of adding pointers to the same object to the array instead of new objects.

Picking your original code on the question, wouldn't it work if you replace the loop with this one:
for (var i = 0; i < l; i++) {
  var id = folders[i].folder_id;
  var description = folders[i].description;
  getFolderCardsNumber(id).success(function(result) {
    obj.push({
      fd_cards_number: result,
      folder_id: id,
      folder_description: description
    });
  });

  console.log('customFolders');
  console.log(customFolders);
  console.log('obj');
  console.log(obj);
}

Open in new window

Let me know.
@Alex
This is the whole function:
function getFolders(requestingPage) {
	var foldersContext = {cFolders: []};
	getFolderCardsNumber('PC').success(function (result) {
		var pcContext = {pc_cards_number: result};
		jQuery.extend(foldersContext, pcContext);
//		foldersContext.pc_card_number = result;
	});
	getFolderCardsNumber('SP').success(function (result) {
		var spContext = {sp_cards_number: result};
		jQuery.extend(foldersContext, spContext);
//		foldersContext.sp_card_number = result;
	});
	getCustomFolders().success(function (data) {
		var response = JSON.parse(data);
		if (response.success) {
			var folders = response.result;
			var obj = [];
			if (folders.length > 0) {
				var i = 0,
					l = folders.length;
//				for (i;i < l;i++) {
//					var customFolders = {};
//					var id = folders[i].folder_id;
//					var description = folders[i].description;
//					getFolderCardsNumber(id).success(function (result) {
//						customFolders.fd_cards_number = result;
//					});
//					customFolders.folder_id = id;
//					customFolders.folder_description = description;
//					console.log(customFolders);
//					obj.push(customFolders);
//				}
				for (var i = 0;i < l;i++) {
					var id = folders[i].folder_id;
					var description = folders[i].description;
					console.log(id+' = '+description);
					getFolderCardsNumber(id).success(function (result) {
						obj.push({
							fd_cards_number: result,
							folder_id: id,
							folder_description: description
						});
					});
					console.log(obj);
				}
				foldersContext.cFolders = obj;
				console.log('context:');
				console.log(foldersContext);
				if (requestingPage === 'all-folders') {
					var html = parseAndReturnHandlebarsRev2("folders_all", foldersContext);
				} else if (requestingPage === 'dashboard') {
					var html = parseAndReturnHandlebarsRev2("folders_dash", foldersContext);
				}
				console.log(html);
				jQuery('.all-folders').html(html);
			}
		}
	});
}
;

Open in new window

A note about extend. if I replace the first 2 extend with the commented line, I can't get the card number, so I have left them there.
Your code returns an array with the same element replicated many times: it seems the same proble of overwritten object (or array). ?
Just in case it is useful here is a link to an online sample of the code I posted earlier.

Look in the console to see the result

http://www.marcorpsa.com/ee/t1349.html
@Julian
Here the refactored functions  based on your snippet: almost done!
function getFolders2(requestingPage) {
	var foldersContext = {cFolders: []};
	getFolderCardsNumber('PC').success(function (result) {
		var pcContext = {pc_cards_number: result};
		jQuery.extend(foldersContext, pcContext);
//		foldersContext.pc_card_number = result;
	});
	getFolderCardsNumber('SP').success(function (result) {
		var spContext = {sp_cards_number: result};
		jQuery.extend(foldersContext, spContext);
//		foldersContext.sp_card_number = result;
	});
	getCustomFolders().success(function (data) {
		var response = JSON.parse(data);
		if (response.success) {
			var folders = response.result;
			var obj = [];
			var defferds = [];
			if (folders.length > 0) {
				var i = 0,
								l = folders.length;
				for (i;i < l;i++) {
					var customFolders = {};
					customFolders.folder_id = folders[i].folder_id;
					customFolders.folder_description = folders[i].description;
					def = getFolderCardsNumber(folders[i].folder_id);
					defferds.push(def);
					customFolders.fd_cards_number = def.responseText;
					obj.push(customFolders);
				}
				jQuery.when.apply(jQuery, defferds).done(function () {
					console.log(obj);
				});
				foldersContext.cFolders = obj;
				console.log('context:');
				console.log(foldersContext);
				if (requestingPage === 'all-folders') {
					var html = parseAndReturnHandlebarsRev2("folders_all", foldersContext);
				} else if (requestingPage === 'dashboard') {
					var html = parseAndReturnHandlebarsRev2("folders_dash", foldersContext);
				}
				console.log(html);
				jQuery('.all-folders').html(html);
			}
		}
	});
}

function getFolderCardsNumber(folder) {
  var def = jQuery.ajax({
    url: 'get_folder_cards_number.php',
    data: {folder: folder},
    type: 'POST',
  }).done(function(data) {
    fd_cards_number = data;
  });
	console.log(def);
  return def;	
}

Open in new window

For first 2 calls to getCardsNumber() numbers are correctly returned, but for the ones within the loop the function returns an object (the ajax call). I've tried to write def.responeText (this I see in console.log) but it is undefined... Any idea?
I'm not understanding how that is still not working.
I also don't see where are you actually returning the value from the function; I think you still need a global promise that will act as the holder of the overall result.

This is a fairly complex piece of code to be tested with the naked eye; I would need to debug it.
Is it, by any chance, available online somewhere? :)
@Alex: I have to prepare a page for you because is a commercial project and my client obviously wnats to keep it secret for now. I'll let you know when it will be ready, even I think only a little step is missing to get it to work (read my last comment above).
@Julian: def is always an object (obviously) but in the first 2 calls (where I then call $.extend) the card number is returned in the next times the number is always in responseText but it is not retrieved by the code: maybe I have to look better the return of the query when folder is not one of the first 2 (which are special predefined folders)...
I'll come back soon.
Maybe post what we are aiming for - what do you want your structure to look like when you are done.
It's quite simple: a list of folder names and near the number of elements (cards) they hold. But your code works fine, Julian: in the console I see all ajac object returned by getCardsNumber() and they all have the property responseText which hold the expected value for card number. The only thing now is going wrong is the different output I get within getFolders2() function. The question is: why def is trated as a number in the first 2 calls and as object in the following ones (within the loop)?
Can you post your latest code?
Here is it:
function getFolders2(requestingPage) {
	var foldersContext = {cFolders: []};
	getFolderCardsNumber('PC').success(function (result) {
		var pcContext = {pc_cards_number: result};
		jQuery.extend(foldersContext, pcContext);
//		foldersContext.pc_card_number = result;
	});
	getFolderCardsNumber('SP').success(function (result) {
		var spContext = {sp_cards_number: result};
		jQuery.extend(foldersContext, spContext);
//		foldersContext.sp_card_number = result;
	});
	getCustomFolders().success(function (data) {
		var response = JSON.parse(data);
		if (response.success) {
			var folders = response.result;
			var obj = [];
			var defferds = [];
			if (folders.length > 0) {
				var i = 0,
								l = folders.length;
				for (i;i < l;i++) {
					var customFolders = {};
					customFolders.folder_id = folders[i].folder_id;
					customFolders.folder_description = folders[i].description;
					def = getFolderCardsNumber(folders[i].folder_id);
					defferds.push(def);
					customFolders.fd_cards_number = def.responseText;
					obj.push(customFolders);
				}
				jQuery.when.apply(jQuery, defferds).done(function () {
					console.log(obj);
				});
				foldersContext.cFolders = obj;
				console.log('context:');
				console.log(foldersContext);
				if (requestingPage === 'all-folders') {
					var html = parseAndReturnHandlebarsRev2("folders_all", foldersContext);
				} else if (requestingPage === 'dashboard') {
					var html = parseAndReturnHandlebarsRev2("folders_dash", foldersContext);
				}
				console.log(html);
				jQuery('.all-folders').html(html);
			}
		}
	});
}

function getFolderCardsNumber(folder) {
  var def = jQuery.ajax({
    url: 'get_folder_cards_number.php',
    data: {folder: folder},
    type: 'POST',
  }).done(function(data) {
    fd_cards_number = data;
  });
	console.log(def);
  return def;	
}

Open in new window

Hi guys. I confess that even preparing the testing/debugging page is driving me crazy :-)
This is what I get until now: I can't get e decent output!
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script>
	function getFolders2(requestingPage) {
		var foldersContext = {cFolders: []};
		getFolderCardsNumber('PC').success(function (result) {
			var pcContext = {pc_cards_number: result};
			$.extend(foldersContext, pcContext);
//		foldersContext.pc_card_number = result;
		});
		getFolderCardsNumber('SP').success(function (result) {
			var spContext = {sp_cards_number: result};
			$.extend(foldersContext, spContext);
//		foldersContext.sp_card_number = result;
		});
		getCustomFolders().success(function (data) {
			var response = JSON.parse(data);
			var folders = response;
			var obj = [];
			var defferds = [];
			if (folders.length > 0) {
				var i = 0,	l = folders.length;
				for (i;i < l;i++) {
					var customFolders = {};
					customFolders.folder_id = folders[i].folder_id;
					customFolders.folder_description = folders[i].description;
					var def = getFolderCardsNumber(folders[i].folder_id);
					defferds.push(def);
					customFolders.cards_number = def;
					console.log(def["responseText"]);
					obj.push(customFolders);
				}
				$.when.apply($, defferds).done(function () {
//					console.log('when obj:');
//					console.log(obj);
				});
				foldersContext.cFolders = obj;
				for (var key in foldersContext) {
					if (foldersContext.hasOwnProperty(key)) {
						if ($.type(key) === 'object'){
							for (var subkey in foldersContext[key]){
								$('ul').append('<li>' + foldersContext[key].folder_id + ' / ' + foldersContext[key].folder_description + ' / ' + foldersContext[key].cards_number + '</li>');
							}
						}else{
							$('ul').append('<li>' + foldersContext.cards_number + '</li>');
							
						}
						
//						$('ul').append(key + " -> " + foldersContext[key]);
					}
				}
//					return foldersContext;
			}
		});
	}
	;

	function getCustomFolders() {
		return $.ajax({
			type: "GET",
			url: "get_folders.php",
			data: {}
		});
	}

	function getFolderCardsNumber(folder) {
		var def = $.ajax({
			url: 'get_folder_cards_number.php',
			data: {folder: folder},
			type: 'POST',
		}).done(function (data) {
			fd_cards_number = data;
		});
//		console.log(def);
		return def;
	}
	;

	$(document).ready(function ()
	{
		getFolders2('all-folders');
	});

</script>

<ul></ul>

Open in new window


Here the 2 php scripts
get_folder_cards_number.php:
<?php
$folder = $_REQUEST['folder'];
if ($folder === 'PC'){
	$count = "10";
}
elseif($folder==="SP")
{
	$count = "5";
}
else
{
	$count = mt_rand(0, 200);
}
echo $count;

Open in new window


get_folders.php:
<?php
$arr = array(
	array(
		"folder_id" => "xxx",	
		"description" => "personal"	
	),
	array(
		"folder_id" => "yyy",	
		"description" => "public"	
	),
	array(
		"folder_id" => "56a8f1260b3b1",
		"description" =>"ciccino",
	),
	array(
		"folder_id" => "56a8f13deccc6",
		"description" => "piccino"			
	),
	array(
		"folder_id" => "56aa0ffc45085",
		"description" => "new folder"
	),
	array(
		"folder_id" => "56aa1250f2277",
		"description" => "another folder for me"
	),
	array(
		"folder_id" => "56aa16173f72f",
		"description" => "lunghissimofolderciccino"
	),
	array(
		"folder_id" => "56aa17a1195a4",
		"description" => "hghghghghghghghghhhghghghghggh"
	),
);
echo json_encode($arr);

Open in new window


the live page is here: http://www.webintenerife.com/testing_script/jq_promises/promises.php
Or do I have to open a new quesiton to get help in preparing the debugging page? :-)
Thank you so mutch for your efforts!
SOLUTION
Avatar of Robert Schutt
Robert Schutt
Flag of Netherlands image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Hi Robert, welcome to our little party: it's never too late, if you bring some drink :-) And I see you brought a lot! I'm drunk already: this probably will kill me.
Ok, I go to test it in the original environment and I'll let you know asap.
Great! Oh one more thing: have you noticed / are you aware that the order in which the asynchronous functions return is not guaranteed?
Thank you
Thank you Julian: your snippet had brought me terribly near the solution but I didn't be able to do the last step :-(
Robert, you arrived late, but with a really good drink! Your code solved the issue perfectly and now it works like a charms. I would like to ask something about what you said:

after the loop you cannot be sure that all callbacks have returned already so there is a danger there when you run the function to create the html based on the partially filled objects. Again, maybe I'm overlooking something in the original code or the proposed changes but it seems to me something that could easily go wrong. In the code below I have added a check on the length of the response array but of course that would mean if anything goes wrong, the check is never true so I also added an error function with the same check (after pushing an 'error' response on the array).
I'm wondering if I should follow some other design pattern or some other technology to avoid these problems: any thought?
Thank you again for having vanished this nightmare :-)
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Just a note on the accepted solution - if you are going to be working with promises best to use the full set of functionality that comes with them.

The correct way of determining if a deffered object (or group of objects) has resolved is to use the $.when - as per the code samples provided above.

You can pass multiple deffered objects to $.when which fires only when they have all resolved.
Well Marco, I'm glad you could use it. I'm not sure about a design pattern. If I had a simpler approach I would certainly have used it instead of working around the problems step by step like I posted ;-)

I see Julian explains the second part of handling the results after all functions have executed much better!

By first pushing the objects instead of doing that in the callback the ordering may have been solved as well?
Hi.
@Julian: I'll test your code tomorrow, thank you very mutch for your help
Hi guys. Sorry for the delay. I'm going to request attention in order to invert the points and the accepted solution.
After several tests, having to change the way data were used depending on the requestingPage, I tried to modify the checkIfDone function and I have then realized that this function was called as many times as the loop loops (can we say this way?) After some trying to adjust things to make it different, I tested the Julian's code: just moving the for part from $.when to a specific function, adding a parameter with the callback function I wish to call and writing the second callback for the new page made things work fine in any case, so I think it is better, for other who can read this thread to highlight Julian solution as the more flexible (even considering what Julian said about the use of all tools offered by promises.
Hope this wont offend you, Robert: your comments made me happy, really happy. This decision is only to advice future readers about the solution I actually implemented :-)
Cheers and thank you all again.
Yep, totally understand. No offense at all.
Thank you again, guys
Thanks Marco and you are welcome.