.each() loop wait for another loop to finish before starting - javascript

I have two main loops for posts and comments. However the comments don't display, presumably because the post ID is not on the DOM yet (?).
$.getJSON('fresh_posts.php',function(data){
//posts
$.each(data.freshposts, function(id, post) {
// set variables and append divs to document
var id = post.id;
...
});
// comments attached to each post
$.each(data.freshcomments, function(id, commentList) {
$.each(commentList, function(index, c) {
// set variables and append comments to each post div
var postid = c.postid; // this is the same as post.id (linked)
...
var full = "<div> ... </div>";
$('#comment-block'+postid).append(full); // comment-block+postid is attached with each post div, so it tells the comment which div it should be appended to.
})
});
});
Does not display comments ^
If I wrap the $.each loop for the comments in a setTimeOut(function(){},1), the comments are able to be displayed - I suppose it needs to wait 1 millisecond before the loop can commence? However this doesn't seem like a good/fool-proof way to ensure this.
setTimeOut(function(){
$.each(data.freshcomments, function(id, commentList) {
...
})
},1)
Displays comments ^

I managed to solve this after a few hours of work.
I made two functions, getPosts() and appendComments(). I used the promise method:
function getPosts(){
var deferred = new $.Deferred(); // new deferred
$.getJSON('fresh_posts.php',function(data){
global_save_json = data.freshcomments;
var howManyPosts = Object.keys(data.freshposts).length; // how many posts there are (object length)
var arrayCount = 0;
$.each(data.freshposts, function(id, post) {
// closing tags for above
return deferred.promise();
}
I had an async method (just learned what that was) to check if the image was designed for retina displays within getPosts.
if (retina === "true") {
var img = new Image();
img.onload = function() {
var retina_width = this.width/2;
var post = '...';
$('.main').append(post);
arrayCount++; // for each post add +1 to arrayCount
if (arrayCount == howManyPosts) { // if last post
deferred.resolve();
}
}
img.src = image;
} else {
...
I then did
getPosts().then(appendComments);
after closing tag of the function getPosts. So basically even though the function finishes, it still waits for the async method to also finish within that function before the function getComments is called. That way the post id's will exist on the document for the comments to be appended to.
appendComments looks like this:
function appendComments(){
if (global_save_json != null){
$.each(global_save_json, function(id, commentList) {
$.each(commentList, function(index, c) {

Related

Make code wait for popup to open, then scrape the popup

I am writing a JS script that automates some browser actions.
With get_baskets_onclicks, I am collecting onclick functions from certain DOM elements and returning them in an array. Each onclick looks something like this:
onclick="PrimeFaces.ab({s:"j_id_32:GenerationTable:0:j_id_1e_2_3p",u:"#widgetVar(GenerationCodingDialog)",onco:function(xhr,status,args){PF('GenerationCodingDialog').show();}});return false;"
and opens a pop-up from which I need to collect some data with get_MAP_data.
Also, each of these functions is called from within get_MAP_data.
The problem is I cannot make the code wait for the popup to be opened, so the data returned by get_MAP_data is empty.
Besides the below document.readyState === 'complete', I have also tried window.onload = function(){}, to no avail.
Is there any way to make the browser (Chrome) wait? I guess I cannot use jQuery, because this is not my webpage.
function get_baskets_onclicks() {
// returns array of functions that launch MAP dialogs
var baskets = Array.from(document.getElementsByClassName("ui-commandlink ui-widget margin-right-5px"));
var baskets_onclicks = baskets.map(basket => basket.onclick);
return baskets_onclicks;
};
function get_MAP_data(basket_onclick) {
basket_onclick()
if (document.readyState === 'complete') {
console.log("PAGE LOADED");
// wait here for the dialog to open
// dt = detail table
var MAP_data = {} // container for transaction details
var labels_to_get = ['Description', 'Category', 'Department', 'Justification', 'My Shop Voucher', 'My Shop Coding'];
var all_dts = document.getElementsByClassName('summary-details-grid');
var dt = Array.from(all_dts).filter(table => table.parentElement.id == "paymentGenerationmyShopCodingForm")[0];
var dt_body = dt.children[0];
var dt_trs = Array.from(dt_body.children) ;
dt_trs.forEach(function(tr) {
tds = Array.from(tr.children);
tds.forEach(function(td) {
var label = td.textContent;
if (labels_to_get.includes(label)) {
var value_for_label = tds[1].textContent;
MAP_data[label] = value_for_label;
console.log(label, value_for_label);
};
});
});
// console.log(MAP_data);
return MAP_data;
};
};
var first_onclick = get_baskets_onclicks()[0];
get_MAP_data(first_onclick);
A small, hacky fix would be to make your code poll for the existence of the elements you are checking.
let interval = setInterval(() => {
var all_dts = document.getElementsByClassName('summary-details-grid');
if (all_dts.length !== 0) {
// Found some elements, now lets run the code
clearInterval(interval);
get_MAP_data(first_onclick);
}
}, 100);
This would check for summary-details-grid classes ever 10th of a second, and when it finds them, run your code.
https://developer.mozilla.org/en-US/docs/Talk:DOM/window.setTimeout
http://mdn.beonex.com/en/DOM/window.setInterval.html
combination of retries, setTimeout, setInterval, while loop, I'm not sure the best way for your specific modal, but one of those options should be fit for polling the DOM results until something is there or it's tried to many times.

Run function after another function completes JavaScript and JQuery

I need a little help. I'm trying to run my second function "likeLinks();" but only after my first function "getLikeURLs();" is finished. This is because my 2nd function relies on the links Array to execute. It seems like they are trying to run at the same time.
Any help would be appreciated.
var links = [];
var url = '/' + window.location.pathname.split('/')[1] + '/' + window.location.pathname.split('/')[2] + '/'
getLikeURLs();
likeLinks();
function getLikeURLs() {
for (i = 1; i < parseInt(document.getElementsByClassName('PageNav')[0].getAttribute('data-last')) + 2; i++) {
var link = $.get(url + 'page-' + i, function(data) {
//gets the like links from current page
$(data).find('a[class="LikeLink item control like"]').each(function() {
links.push($(this).attr('href')); // Puts the links in the Array
});
});
}
}
function likeLinks() {
for (t = 0; t <= links.length; t++) {
var token = document.getElementsByName('_xfToken')[0].getAttribute('value')
$.post(links[t], {
_xfToken: token,
_xfNoRedirect: 1,
_xfResponseType: 'json'
}, function(data) {});
}
}
The link variables are actually jQuery deferred objects - store them in an array and then you can use $.when() to create a mew deferred object that only resolves when all of the previous $.get() operations have completed:
function getLikeURLs(url) { // NB: parameter, not global
var defs = [], links = []; // NB: links no longer global
for (...) {
var link = $.get(...);
defs.push(link);
}
// wait for previous `$.get` to finish, and when they have create a new
// deferred object that will return the entire array of links
return $.when.apply($, defs).then(function() { return links; });
}
Then, to start the chain of functions:
getLikeURLs(url).then(likeLinks);
Note that likeLinks will now be passed the array of links instead of accessing it from the global state. That function should also be rewritten to allow you to wait for its $.post calls to complete, too:
function likeLinks(links) {
// loop invariant - take it outside the loop
var token = document.getElementsByName('_xfToken')[0].getAttribute('value');
// create array of deferreds, one for each link
var defs = links.map(function(link) {
return $.post(link, {
_xfToken: token,
_xfNoRedirect: 1,
_xfResponseType: 'json'
});
});
// and another for when they're all done
return $.when.apply($, defs);
}
p.s. don't put that (relatively) expensive parseInt(document.getAttribute(...)) expression within the for statement - it'll cause it to be evaluated every iteration. Calculate it once outside the loop and store it in a variable. There's a few other places where you're repeating calls unnecessarily, e.g. window.location.pathname.split()
EDIT: My answer discusses the issue but see Alnitak answer for a much better solution.
The get in getLikeURLs and the put in likeLinks are both asynchronous. The calls to both of these function return immediately. When data is returned from the called server at some indeterminate time later, the callback functions are then called. The puts could return before the gets which would be a problem in your case. Also note that JavaScript is NOT multi-threaded so the two methods, getLikeURLs and likeLinks will never run at the same time. The callback functions, on the other hand, might be called at anytime later with no guarantee as to the call back order. For example, the 3rd get/put might return before the 1st get/put in your loops.
You could use $.ajax to specify that the gets and puts are synchronous but this is ill advised because the browser will hang if ANY get/put doesn't return in a reasonable amount of time (e.g. server is offline). Plus you don't have the "multi-tasking" benefit of sending out a lot of requests and having the various servers working at the same time. They would do so serially.
The trick is to simply call likeLinks form the callback function in getLikeURL. Your case is a little tricky because of the for loop but this should work:
var links = [];
var url = '/' + window.location.pathname.split('/')[1] + '/' + window.location.pathname.split('/')[2] + '/'
getLikeURLs();
//likeLinks(); // Don't call yet. Wait for gets to all return.
function getLikeURLs() {
var returnCount = 0; // Initialize a callback counter.
var count = parseInt(document.getElementsByClassName('PageNav')[0].getAttribute('data-last')) + 1;
for (i = 0; i < count; i++) {
var link = $.get(url + 'page-' + (i + 1), function(data) {
//gets the like links from current page
$(data).find('a[class="LikeLink item control like"]').each(function() {
links.push($(this).attr('href')); // Puts the links in the Array
});
// If all gets have returned, call likeLinks.
returnCount++;
if (returnCount === count) {
likeLinks();
}
});
}
}
function likeLinks() {
for (t = 0; t <= links.length; t++) {
var token = document.getElementsByName('_xfToken')[0].getAttribute('value')
$.post(links[t], {
_xfToken: token,
_xfNoRedirect: 1,
_xfResponseType: 'json'
}, function(data) {});
}
}

FirefoxOS: return array using device storage api

I am just getting started with coding for FirefoxOS and am trying to get a list of files in a directory.
The idea is to find the name of each file and add it to the array (which works), but I want to return the populated array and this is where I come unstuck. It seems that the array gets populated during the function (as I can get it to spit out file names from it) but when I want to return it to another function it appears to be empty?
Here is the function in question:
function getImageFromDevice (){
var imageHolder = new Array();
var pics = navigator.getDeviceStorage('pictures');
// Let's browse all the images available
var cursor = pics.enumerate();
var imageList = new Array();
var count = 0;
cursor.onsuccess = function () {
var file = this.result;
console.log("File found: " + file.name);
count = count +1;
// Once we found a file we check if there are other results
if (!this.done) {
imageHolder[count] = file.name;
// Then we move to the next result, which call the cursor
// success with the next file as result.
this.continue();
}
console.log("file in array: "+ imageHolder[count]);
// this shows the filename
}
cursor.onerror = function () {
console.warn("No file found: " + this.error);
}
return imageHolder;
}
Thanks for your help!
Enumerating over pictures is an asynchronous call. Essentially what is happening in your code is this:
You are initiating an empty array
You are are telling firefox os to look for pictures on the device
Then in cursor.onsuccess you are telling firefox os to append to the array you have created WHEN it gets back the file. The important thing here is that this does not happen right away, it happens at some point in the future.
Then you are returning the empty array you have created. It's empty because the onsuccess function hasn't actually happened.
After some point in time the onsuccess function will be called. One way to wait until the array is full populated would be to add in a check after:
if (!this.done) {
imageHolder[count] = file.name;
this.continue();
}
else {
//do something with the fully populated array
}
But then of course your code has to go inside the getImageFromDevice function. You can also pass a callback function into the getImageFromDevice function.
See Getting a better understanding of callback functions in JavaScript
The problem is with the aSynchronous nature of the calls you are using.
You are returning (and probably using) the value of imageHolder when it's still empty - as calls to the "onsuccess" function are deferred calls, they happen later in time, whereas your function returns immediately, with the (yet empty) imageHolder value.
You should be doing in this case something along those lines:
function getImageFromDevice (callback){
...
cursor.onsuccess = function () {
...
if (!this.done) {
// next picture
imageHolder[count] = file.name;
this.continue();
} else {
// no more pictures, return with the results
console.log("operation finished:");
callback(imageHolder);
}
}
}
Or use Promises in your code to accomplish the same.
Use the above by e.g.:
getImageFromDevice(function(result) {
console.log(result.length+" pictures found!");
});

Concept - Designing a collapsible queue for asynchronous resources

I've noticed that the size of a file requested will effect how long the response takes for ajax calls. So if I fire 3 ajax GET requests for files of varying size, they may arrive in any order. What I want to do is guarantee the ordering when I append the files to the DOM.
How can I set up a queue system so that when I fire A1->A2->A3. I can guarantee that they are appeneded as A1->A2->A3 in that order.
For example, suppose A2 arrives before A1. I would want the action to wait upon the arrival and loading of A1.
One idea is to create a status checker using a timed callback as such
// pseudo-code
function check(ready, fund) {
// check ready some how
if (ready) {
func();
} else {
setTimeout(function () {
check(ready, fund);
}, 1); // check every msec
}
}
but this seems like a resource heavy way, as I fire the same function every 1msec, until the resources is loaded.
Is this the right path to complete this problem?
status checker using a 1msec-timed callback - but this seems like a resource heavy way; Is this the right path to complete this problem?
No. You should have a look at Promises. That way, you can easily formulate it like this:
var a1 = getPromiseForAjaxResult(ressource1url);
var a2 = getPromiseForAjaxResult(ressource2url);
var a3 = getPromiseForAjaxResult(ressource3url);
a1.then(function(res) {
append(res);
return a2;
}).then(function(res) {
append(res);
return a3;
}).then(append);
For example, jQuery's .ajax function implements this.
You can try something like this:
var resourceData = {};
var resourcesLoaded = 0;
function loadResource(resource, callback) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var state = this.readyState;
var responseCode = request.status;
if(state == this.DONE && responseCode == 200) {
callback(resource, this.responseText);
}
};
xhr.open("get", resource, true);
xhr.send();
}
//Assuming that resources is an array of path names
function loadResources(resources) {
for(var i = 0; i < resources.length; i++) {
loadResource(resources[i], function(resource, responseText) {
//Store the data of the resource in to the resourceData map,
//using the resource name as the key. Then increment the
//resource counter.
resourceData[resource] = responseText;
resourcesLoaded++;
//If the number of resources that we have loaded is equal
//to the total number of resources, it means that we have
//all our resources.
if(resourcesLoaded === resources.length) {
//Manipulate the data in the order that you desire.
//Everything you need is inside resourceData, keyed
//by the resource url.
...
...
}
});
}
}
If certain components must be loaded and executed before (like certain JS files) others, you can queue up your AJAX requests like so:
function loadResource(resource, callback) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var state = this.readyState;
var responseCode = request.status;
if(state == this.DONE && responseCode == 200) {
//Do whatever you need to do with this.responseText
...
...
callback();
}
};
xhr.open("get", resource, true);
xhr.send();
}
function run() {
var resources = [
"path/to/some/resource.html",
"path/to/some/other/resource.html",
...
"http://example.org/path/to/remote/resource.html"
];
//Function that sequentially loads the resources, so that the next resource
//will not be loaded until first one has finished loading. I accomplish
//this by calling the function itself in the callback to the loadResource
//function. This function is not truly recursive since the callback
//invocation (even though it is the function itself) is an independent call
//and therefore will not be part of the original callstack.
function load(i) {
if (i < resources.length) {
loadResource(resources[i], function () {
load(++i);
});
}
}
load(0);
}
This way, the next file will not be loaded until the previous one has finished loading.
If you cannot use any third-party libraries, you can use my solution. However, your life will probably be much easier if you do what Bergi suggested and use Promises.
There's no need to call check() every millisecond, just run it in the xhr's onreadystatechange. If you provide a bit more of your code, I can explain further.
I would have a queue of functions to execute and each of them checks the previous result has completed before executing.
var remoteResults[]
function requestRemoteResouse(index, fetchFunction) {
// the argument fetchFunction is a function that fetches the remote content
// once the content is ready it call the passed in function with the result.
fetchFunction(
function(result) {
// add the remote result to the list of results
remoteResults[index] = result
// write as many results as ready.
writeResultsWhenReady(index);
});
}
function writeResults(index) {
var i;
// Execute all functions at least once
for(i = 0; i < remoteResults.length; i++) {
if(!remoteResults[i]) {
return;
}
// Call the function that is the ith result
// This will modify the dom.
remoteResults[i]();
// Blank the result to ensure we don't double execute
// Store a function so we can do a simple boolean check.
remoteResults[i] = function(){};
}
}
requestRemoteResouse(0, [Function to fetch the first resouse]);
requestRemoteResouse(1, [Function to fetch the second resouse]);
requestRemoteResouse(2, [Function to fetch the thrid resouse]);
Please note that this is currently O(n^2) for simplicity, it would get faster but more complex if you stored an object at every index of remoteResults, which had a hasRendered property. Then you would only scan back until you found a result that had not yet occurred or one that has been rendered.

Variable scope. Use a closure?

I'm trying to grab all the URLs of my Facebook photos.
I first load the "albums" array with the album id's.
Then I loop through the albums and load the "pictures" array with the photos URLs.
(I see this in Chrome's JS debugger).
But when the code gets to the last statement ("return pictures"), "pictures" is empty.
How should I fix this?
I sense that I should use a closure, but not entirely sure how to best do that.
Thanks.
function getMyPhotos() {
FB.api('/me/albums', function(response) {
var data = response.data;
var albums = [];
var link;
var pictures = [];
// get selected albums id's
$.each(data, function(key, value) {
if ((value.name == 'Wall Photos')) {
albums.push(value.id);
}
});
console.log('albums');
console.log(albums);
// get the photos from those albums
$.each(albums, function(key, value) {
FB.api('/' + value + '/photos', function(resp) {
$.each(resp.data, function(k, val) {
link = val.images[3].source;
pictures.push(link);
});
});
});
console.log('pictures');
console.log(pictures);
return pictures;
});
}
You're thinking about your problem procedurally. However, this logic fails anytime you work with asynchronous requests. I expect what you originally tried to do looked something like this:
var pictures = getMyPhotos();
for (var i = 0; i < pictures.length; i++) {
// do something with each picture
}
But, that doesn't work since the value of 'pictures' is actually undefined (which is the default return type of any function without an actual return defined -- which is what your getMyPhotos does)
Instead, you want to do something like this:
function getMyPhotos(callback) {
FB.api('/me/albums', function (response) {
// process respose data to get a list of pictures, as you have already
// shown in your example
// instead of 'returning' pictures,
// we just call the method that should handle the result
callback(pictures);
});
}
// This is the function that actually does the work with your pictures
function oncePhotosReceived(pictures){
for (var i = 0; i < pictures.length; i++) {
// do something with each picture
}
};
// Request the picture data, and give it oncePhotosReceived as a callback.
// This basically lets you say 'hey, once I get my data back, call this function'
getMyPhotos(oncePhotosReceived);
I highly recommend you scrounge around SO for more questions/answers about AJAX callbacks and asynchronous JavaScript programming.
EDIT:
If you want to keep the result of the FB api call handy for other code to use, you can set the return value onto a 'global' variable in the window:
function getMyPhotos(callback) {
FB.api('/me/albums', function (response) {
// process respose data to get a list of pictures, as you have already
// shown in your example
// instead of 'returning' pictures,
// we just call the method that should handle the result
window.pictures = pictures;
});
}
You can now use the global variable 'pictures' (or, explicitly using window.pictures) anywhere you want. The catch, of course, being that you have to call getMyPhotos first, and wait for the response to complete before they are available. No need for localStorage.
As mentioned in the comments, asynchronous code is like Hotel California - you can check any time you like but you can never leave.
Have you noticed how the FB.api does not return a value
//This is NOT how it works:
var result = FB.api('me/albums')
but instead receives a continuation function and passes its results on to it?
FB.api('me/albums', function(result){
Turns out you need to have a similar arrangement for your getMyPhotos function:
function getMyPhotos(onPhotos){
//fetches the photos and calls onPhotos with the
// result when done
FB.api('my/pictures', function(response){
var pictures = //yada yada
onPhotos(pictures);
});
}
Of course, the continuation-passing style is contagious so you now need to call
getMyPhotos(function(pictures){
instead of
var pictures = getMyPhotos();

Categories