I'm working with jQuery (version 3.2.1), and I'm finding that sometimes, for reasons that I cannot discern, jQuery is unable to locate jQuery-created DOM elements. I would say that this issue occurs about 1 out of every 10 times I refresh the page. In those instances, the element is undefined.
It's a bit of a long and involved script, so I'll attempt to distill it to its critical parts. First of all, the scripts are introduced in the index.html like so:
<body>
...
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="app.js"></script>
</body>
Pretty much what you'd expect. Here's the relevant (and abbreviated) code from app.js - the problem occurs in the loadItems() function:
function getQueryParamCat(queryParam) {
return $('.category-item[data-query_name=' + '\'' + queryParam + '\'' + ']');
}
function loadItems(queryParam) {
$.post('./get_items.php', {}, () => {
const queryParamCat = getQueryParamCat(queryParam);
if (queryParamCat[0]) {
// Leaving out categoryClick() - it triggers a click on the relevant DOM element
categoryClick(queryParamCat);
} else {
categoryClick($('category').first());
}
});
}
function loadCategories(callBack) {
$.post('./get_categories.php', {}, (data) => {
const categories = $.parseJSON(data);
$.each(categories, (i, value) => {
const cat = $('<category>').appendTo($('left')).html(value.name);
cat.attr('class', 'category-item');
cat.attr('data-query_name', value.name.toLowerCase());
cat.mousedown(function () {
categoryClick($(this));
});
});
return callBack;
});
}
$(document).ready(() => {
// Leaving out getParameterByName() - just gets a string from the url
const queryParam = getParameterByName();
loadCategories(loadItems(queryParam));
});
In brief summary:
the page loads and loadCategories() is called.
the client makes an AJAX request to get_categories.php, and the returned data is used to create a set of <category> DOM elements.
loadItems(queryParam) is then called as a callback, which then makes an additional AJAX request to get more data.
In the callback following that request, we ultimately want to call the categoryClick() function, passing in a <category> DOM element as the argument (the element to be 'clicked'). THIS IS WHERE THE PROBLEM OCCURS.
About 1 out of 10 times, the result of getQueryParamCat() comes back as r.fn.init [prevObject: r.fn.init(1)], which makes the value of queryParamCat[0] in the conditional in loadItems() evaluate to undefined. However, in those situations, $('category') also evaluates to r.fn.init [prevObject: r.fn.init(1)], meaning that $('category').first() is also undefined.
This problem only seems to affect elements that are created by jQuery - anything that was hard-coded in the HTML can be accessed, no problem. Why is it that jQuery is unable to consistently find these elements? Is it trying to find those elements before they've been successfully appended? I could understand if it failed all the time, but the inconsistency is confusing to me. Can anyone offer any suggestions as to how to make this code perform reliably?
Odd syntax; loadCategories expects a callback as an argument, but loadItems doesn't return anything, so loadCategories(loadItems(queryParam)); turns into loadCategories(undefined);.
Also, return callBack; doesn't do anything inside of a $.post function; it's not only not returning the value to the outer function's caller, it's also running async.
Maybe did you mean to do something like this?
loadCategories(() => {
loadItems(queryParam)
});
function loadCategories(callBack) {
// ...
$.each(categories, (i, value) => {
// ...
});
callBack();
That ensures the callback is called after the meat of loadCategories is done.
Related
I'm using jQuery to load text from files into divs using load().
There is a JavaScript function I need to call only when that text has loaded. I know I can use the load() callback to do this once one of the text files has loaded, but how to I do this only once all the files have loaded?
This is my attempt using $.when():
$.when($("#testdiv").load("text/textures.txt")).done(function () { var msg = document.getElementById('testdiv').innerHTML; alert(msg); });
There is a comment below that points to why it is wrong. I need to dig deeper to understand it.
var loaded = 0;
function callback() {
loaded++;
if (loaded === totalNeeded) {
alldone();
}
}
function alldone() {
//do stuff here;
}
You can extend jQuery to add the functionality you want. Here's an example of adding a jQuery method called loadPromise which takes either a string url like .load does, or a callback which is called for each matching element, which returns the argument to pass to .load for that element. It returns a single promise which resolves to an array with one .load result per element loaded.
// You can pass a single url to be used on all matching elements
// or you can pass a callback, which is called for each
// element in the selector, which returns the url to use
// for that element
$.fn.loadPromise = function(urlOrCallback) {
var promises = [];
this.each(function(i, element) {
var url,
defer;
if (typeof urlOrCallback === 'function')
url = urlOrCallback.call(element, i, element, this);
else
url = urlOrCallback;
if (url) {
defer = $.Deferred();
$(element).load(url, function(responseText, textStatus, xhr) {
defer.resolve({
element: element,
responseText: responseText,
textStatus: textStatus,
xhr: xhr
});
});
promises.push(defer.promise());
}
});
return $.when.apply($, promises).then(function(/*...*/) {
return Array.prototype.slice.call(arguments);
});
};
// Simple example:
$('.single-example')
.loadPromise('example-single')
.then(function(result) {
console.log('done load: ' + result[0]);
});
// Example with multiple
$('.multi-example').loadPromise(function() {
return $(this).attr('data-url');
}).then(function(multi) {
console.log('multi done', multi);
});
Here's a live demo on jsfiddle. Note that since jsfiddle has gone to great lengths to block this sort of thing, it gets 404 errors (of course, the example names I pass aren't valid URLs), but if you check the console, you'll see it is working as intended. On your own site, it will have no trouble loading if your existing .load works.
How it works:
JQuery extension functions get the selector passed in this. It uses each to iterate through each matching element, calling .load for that element with the passed url or the result of calling the callback, and creates a promise that is resolved for each one and adding it to the promises array.
Once it has its array of promises, it takes that and uses apply to pass multiple parameters to $.when, one for each element. $.when returns a promise which is resolved when all of the promises are resolved. The .then for the promise $.when returns receives multiple parameters, one per promise. The .then handler there takes all of the parameters and makes one array from them all, and returns it, resolving the promise returned from loadPromise with a single parameter, an array of completions.
Run the fiddle and have a look at the debugger console. You'll see that the promise loadPromise returns is resolved with an array, click into them and have a look at what it returns. You probably don't need that much detailed information, just the promise being resolved at all tells you that they all finished loading.
Side note
Most jQuery APIs that can work on multiple elements have an overload where you can pass a function, and give it a value to use for an argument for each element, as I did in my extension. For example, .text can work on a selector that matches multiple elements, getting the text for each element by calling your callback. Apparently, they forgot to do this for .load. Additionally, jQuery has a mechanism for waiting for multiple completions, which is used for waiting for multiple animations. They seem to have forgotten about that with .load as well.
I've stumbled upon (or created myself of course) an error that I cannot model in my head. I'm iteratively calling an URL using the webdriverio client with different IDs and parsing the resulting HTML. However, the html variable gets overwritten with the last element in the loop, which results in the array containing multiple duplicates of the last html variable value:
async.forEach(test, function (id, callback) {
self.url('https://<api-page>?id=' + id).getHTML('table tbody', true).then(function(html) {
//Parse HTML
parser.write(html);
parser.end();
//Add course to person, proceed to next.
callback();
});
}, function (err) {
self.end().finally();
res.json(person);
});
Parsing is done using the htmlparser2 NPM library. The html variable always returns the last element, even though I can see it going through the different API ids with different data. I would think the error lies at when I get HTML and return it, but I cannot say why nor have any of my fixes worked.
Hopefully someone more skilled than me can see the error.
Thanks in advance,
Chris
UPDATE/Solution - See solution below
I am not sure if I understood quite well the context but the html variable is not overridden, it is just the last chunk that you 've retrieved from the self.url function call. If you want to have the whole result saved in a variable, you should keep append on every loop the result. Probably, you need something like that:
var html = '';
async.forEach(test, function (id, callback) {
self.url('https://<api-page>?id=' + id).getHTML('table tbody', true).then(function (tmpHtml) {
//Parse HTML
parser.write(tmpHtml);
parser.end();
html += tmpHtml;
//Add course to person, proceed to next.
callback();
});
}, function (err) {
self.end().finally();
res.json(person);
});
I finally figured it out and I missed that async.forEach executes the function in parallel, whereas the function I needed was async.timesSeries, which executes the functions in a loop, waiting for each function to finish before starting the next! I've attached the working code below:
async.timesSeries(3, function(n, next) {
self.url('<api-page>?id=' + n').then(function() {
console.log("URL Opened");
}).getHTML('table tbody', true).then(function(html) {
console.log("getHTML");
parser.write(html);
parser.end();
next();
});
}, function(err, results) {
//Add to person object!
self.end().finally();
res.json(person);
});
community. I have encountered a very strange d3.js behavior.
In the following html code, the console.log print out the array "projects" and its length in my firefox console. Strangely, the content (A, B, C) is there, but the length part is 0!!
Any help is appreciated, thanks!!
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>(function() {
var event = d3.dispatch("data");
var projects = [];
d3.tsv("example.tsv", type, function(input) {
event.data(input);
input.forEach(function (d) {
if (projects.indexOf(d.project) == -1) {
projects.push(d.project);
}
})
});
graph(event);
function graph(event) {
event.on("data.graph", function(input) {
console.log(projects, projects.length);
});
}
function type(d, i) {
d.id = i;
d.project = d.project;
return d;
}
})()</script>
Here are the example.tsv
project
A
B
C
Edit
OK, big thanks to Lars. I googled d3.tsv and async, found a page in O'Reilly:
http://chimera.labs.oreilly.com/books/1230000000345/ch05.html#_data
Note that d3.csv() is an asynchronous method, meaning that the rest of your code is executed even while JavaScript is simultaneously waiting for the file to finish downloading into the browser. (The same is true of D3’s other functions that load external resources, such as d3.json().)
This can potentially be very confusing, because you—as a reasonable human person—might assume that the CSV file’s data is available, when in fact it hasn’t finished loading yet. A common mistake is to include references to the external data outside of the callback function. Save yourself some headaches and make sure to reference your data only from within the callback function (or from within other functions that you call within the callback function).
Personally, I like to declare a global variable first, then call d3.csv() to load the data. Within the callback function, I copy the data into my global variable (so it’s available to all of my subsequent functions), and finally I call any functions that rely on that data being present.
I admit, this contradicts my personal understanding of program paradigm. But the console.log still confuses me. As Lars said, console.log is not async but it evals the two variables in another order?? Isn't that the definition of async??
This is because d3.csv is an asynchronous call. That is, the code after the d3.csv block can be run before the call returns and the data is available. This is what happens in Chrome, while Firefox loads the data faster (maybe because of caching?).
The proper way to handle the data retrieved in an asynchronous call is in the callback function entirely, i.e.
d3.tsv("example.tsv", type, function(input) {
event.data(input);
input.forEach(function (d) {
if (projects.indexOf(d.project) == -1) {
projects.push(d.project);
}
});
graph(event);
});
In my jQuery scripts, when the user closes a menu with an animation, I have to call a function after the closing animation is finished. I want to assign this function dynamically by calling a function openStrip() with a parameter. My code looks like:
var FUNCTION_JUST_AFTER_MENU_CLOSE = function(){};
function openStrip(stripId){
FUNCTION_JUST_AFTER_MENU_CLOSE = function(){
createStrip(stripId);
});
}
if I call openStrip("aStripId"), I expect FUNCTION_JUST_AFTER_MENU_CLOSE to be:
// #1
function(){
createStrip("aStripId");
}
whereas my current code gives:
//#2
function(){
createStrip(stripId);
}
i.e, the parameter passed to the function openStrip() is lost while assigning the function() to the variable FUNCTION_JUST_AFTER_MENU_CLOSE.
How can I avoid this.
EDIT: I discovered that my code is actually working. The problem was elsewhere. I got confused because when I looked at Chrome's debugger, it was showing me the function definition as is (#2 in above). But when it actually went down executing that function later in the code, it did evaluate the values of the passed argument, and endedup executing #1.
Thanks for the answer though. I am marking it correct because that is perhaps a better way of assigning the function.
The best way is to return a function, from openStrip like this
function openStrip(stripId) {
return function() {
createStrip(stripId);
};
}
For example,
function openStrip(stripId) {
return function() {
console.log(stripId);
};
}
openStrip("aStripId")();
# aStripId
openStrip("bStripId")();
# bStripId
You can even assign the function objects returned to different variables and use them later on
var aStrip = openStrip("aStripId");
aStrip();
# aStripId
aStrip();
# aStripId
In my app I have the following:
client.on('test', function(req, fn) {
var returnArr = [];
redis.hkeys(req, function (err, replies) {
replies.forEach(function(reply, i) {
if (reply.indexOf('list.') > -1) {
redis.hgetall(reply.substring(5), function(err, r) {
returnArr.push({name:r['name'],index:i});
console.log(returnArr);
});
}
});
console.log(returnArr);
});
console.log(returnArr);
});
For some reason, the second and third logs contain a blank array even though the array is declared once at the beginnning of the event. Any ideas?
EDIT: Sorry, I changed the variable name when I posted it here without thinking. This happens when it's named anything.
Those redis calls are asynchronous. That's why you provide them with callbacks. The code won't work even if you fix the variable name for that reason.
To elaborate: the code in the callback to "hkeys" will be invoked when the data is available. The call will return immediately, however, so your array will have nothing in it at that point.
You cannot wrap asynchronous calls in a function and expect to return a value. It simply won't work.
Instead, the general pattern is to do exactly what the redis API (and virtually everything else in the node.js world; that's kind-of the whole point in fact): give your own function a callback argument to be invoked when appropriate. In your case, it'll be inside the "hgetall" callback that's the last one to be invoked. It should figure out that your results array has as many values in it as there are keys, and so it's time to call the callback passed in to your function.
(I should note that it's not clear what you're trying to do, given that the overall function appears to be a callback to something.)
Another approach would be to use some sort of "promise" pattern, though that's really just a restructuring of the same idea.
edit — the general pattern for an API with a callback would be something like this:
function yourAPI( param1, param2, callback ) {
// ...
some.asynchronousFunction( whatever, function( result ) {
callback( result );
}
}
Now in your case you're making multiple asynchronous service requests, and you'd need to figure out when it's time to invoke the callback. I think you'd probably want to iterate through the "replies" from the call to get the keys and extract the list of ones you want to fetch:
redis.hkeys(req, function (err, replies) {
var keys = [];
replies.forEach(function(reply, i) {
if (reply.indexOf('list.') > -1) {
keys.push( reply.substring(5) );
}
});
keys.forEach( function( key ) {
redis.hgetall(key, function(err, r) {
returnArr.push({name:r['name'],index:i});
if (returnArr.length === keys.length) {
// all values ready
callback( returnArr );
}
});
});
You cannot call your variable return
It is one of a few reserved words that you cannot use in your code as variables.
As Neal suggests don't use javascript reserved words for your variables, here is the list :
https://developer.mozilla.org/en/JavaScript/Reference/Reserved_Words
#Pointy answered this tersely already, but let me explain it a bit more clearly: Those nested functions are not being run in the order you think they are.
Node.js is non-blocking, and uses Javascript's implicit event loop to execute them when ready. Here's your code with line numbers:
/*01*/ client.on('test', function(req, fn) {
/*02*/ var returnArr = [];
/*03*/ redis.hkeys(req, function (err, replies) {
/*04*/ replies.forEach(function(reply, i) {
/*05*/ if (reply.indexOf('list.') > -1) {
/*06*/ redis.hgetall(reply.substring(5), function(err, r) {
/*07*/ returnArr.push({name:r['name'],index:i});
/*08*/ console.log(returnArr);
/*09*/ });
/*10*/ }
/*11*/ });
/*12*/ console.log(returnArr);
/*13*/ });
/*14*/ console.log(returnArr);
/*15*/ });
/*16*/ //Any other code you have after this.
So, what's the order of execution of this thing?
Line 1: Register the event handler for the 'test' event.
Line 16: Start running any other code to be run during this pass through the event loop
Line 2: A 'test' event has been received at some point by the event loop and is now being handled, so returnArr is initialized
Line 3: A non-blocking IO request is performed, and a callback function is registered to execute when the proper event is queued into the event loop.
Line 14-15: The last console.log is executed and this function is finished running, which should end the current event being processed.
Line 4: The request event returns and the callback is executed. The forEach method is one of the few blocking Node.js methods with a callback, so every callback is executed on every reply.
Line 5: The if statement is executed and either ends (goes to line 10) or enters the block (goes to line 6)
Line 6: A non-blocking IO request is performed, adding a new event to the event loop and a new callback to be run when the event comes back.
Line 9: Finishes the registration of the callback.
Line 10: Finishes the if statement
Line 11: Finishes the `forEach callbacks.
Line 12: Executes the second console.log request, which still has nothing in the returnArr
Line 7: One of the events returns and fires the event handler. The returnArr is given the new data.
Line 8: The first console.log is executed. Depending on which event this is, the length of the array will be different. Also the order of the array elements DOES NOT have to match the order of the replies listed in the replies array.
Essentially, you can look at the more deeply nested functions as executing after the entirety of the less-deeply nested functions (because that's what's happening, essentially), regardless of whether the method contains statements after nested non-blocking callback or not.
If this is confusing to you, you can write your callback code in a Continuation Passing Style so it's obvious that everything in the outer function is executed before the inner function, or you can use this nice async library to make your code look more imperative.
This, I think, answers your real question, rather than the one you've entered.