I'm creating a long list of select items and wanted to create a closure call to add each option, then queue the call (via a setTimer) so the browser would not hang.
My implementation works great, but here is what has me puzzled, the following code:
var mainList = $('#mainList');
for (var i=0;i < 100000; i++) {
var self = mainList, addOption = function() {
$(self).append('<option value=' + i + '>' + i + '</option>');
};
$.queue.add(addOption, this);
}
generates:
<option value='100000'>100000</option>
<option value='100000'>100000</option>
<option value='100000'>100000</option> etc...
Where I would like to have it generate the options:
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option> etc...
I'm struggling with how the closure is executed, it makes sense that the addOption() method is called when i == 100000, but I would like the call to reflect the value of i at the time it is queued up.
Is there an easy trick I am missing here?
Set addOption by IIFE which returns a function
var mainList = $('#mainList');
for (var i = 0; i < 100000; i++) {
var self = mainList,
addOption = (function(i) {
return function() {
$(self).append('<option value=' + i + '>' + i + '</option>');
}
})(i);
$.queue.add(addOption, this);
}
Prior to the introduction of Function.prototype.bind, it was certainly necessary to employ a closure specifically to trap one or more vars in situations like this.
Now, that approach is considered to be a bit clumsy.
You can define a basic addOption function, then spawn versions of it in the loop, using addOption.bind() to bind in both mainList as "thisArg", and i as the first formal variable :
var mainList = $('#mainList');
var addOption = function(i) {
this.append('<option value=' + i + '>' + i + '</option>');
};
for (var i=0; i<100000; i++) {
$.queue.add(addOption.bind(mainList, i), this);
}
Related
I'm looping over an Ajax result and populating the JSON in a select box, but not every JSON result is unique, some contain the same value.
I would like to check if there is already a value contained within the select box as the loop iterates, and if a value is the same, not to print it again, but for some reason my if check isn't working?
for (var i = 0; i < result.length; i++) {
var JsonResults = result[i];
var sourcename = JsonResults.Source.DataSourceName;
if ($('.SelectBox').find('option').text != sourcename) {
$('.SelectBox').append('<option>' + sourcename + '</option>');
}
}
The text() is a method, so it needs parentheses, and it returns text of all <option> concatenated. There are better ways to do this, but an approach similar to yours can be by using a variable to save all the added text, so we can check this variable instead of having to check in the <option> elements:
var result = ["first", "second", "first", "third", "second"];
var options = {};
for (var i = 0; i < result.length; i++) {
var JsonResults = result[i];
var sourcename = JsonResults; //JsonResults.Source.DataSourceName;
if (!options[sourcename]) {
$('.SelectBox').append('<option>' + sourcename + '</option>');
options[sourcename] = true;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<select class="SelectBox"></select>
Note: I only used var sourcename = JsonResults; for the demo. Use your original line instead.
.text is a function, so you have to call it to get back the text in the option
for (var i = 0; i < result.length; i++) {
var JsonResults = result[i];
var sourcename = JsonResults.Source.DataSourceName;
if ($('.SelectBox').find('option').text() != sourcename) {
$('.SelectBox').append('<option>' + sourcename + '</option>');
}
}
For one thing, the jQuery method is .text() - it's not a static property. For another, your .find will give you the combined text of every <option>, which isn't what you want.
Try deduping the object before populating the HTML:
const sourceNames = results.map(result => result.Source.DataSourceName);
const dedupedSourceNames = sourceNames.map((sourceName, i) => sourceNames.lastIndexOf(sourceName) === i);
dedupedSourceNames.forEach(sourceName => {
$('.SelectBox').append('<option>' + sourceName + '</option>');
});
Hacker News recently released an API that I am using to display what the current top ten items are on Hacker News. I am running into some problems.
When I run the code below, the order of the items on the frontpage are inaccurate, jumping from the second one in the frontpage to the fourth, to the first, to the fifth, to the third and so on. Running the code again results in a slightly different order again.
$.getJSON('https://hacker-news.firebaseio.com/v0/topstories.json', function(json) {
var convoText = '<ol>';
for (var i = 0; i < 10; i++) {
(function(i) {
$.getJSON('https://hacker-news.firebaseio.com/v0/item/' + json[i] + '.json', function(json2) {
convoText += '<li>' + json2.title + '</li>';
if (i === 9) {
convoText += '</ol>';
addConvo(convoText);
}
});
})(i);
}
});
I understand that this is an effect of Javascript's asynchronous nature. How can I fix it?
The knack is to create and append a <li><a></a></li> structure synchronously in the loop - thereby establishing the correct order - then populate it asynchronously with json2 data when it arrives.
$.getJSON('https://hacker-news.firebaseio.com/v0/topstories.json', function(json) {
var $ol = $('<ol/>').appendTo(...);//wherever
for (var i = 0; i < Math.min(json.length, 10); i++) {
(function(i) {
var $a = $('<li><a></a></li>').appendTo($ol).find('a');
$.getJSON('https://hacker-news.firebaseio.com/v0/item/' + json[i] + '.json', function(json2) {
$a.attr('href', json2.url).text(json2.title);
});
})(i);
}
});
You will have to complete the .appendTo(...) line. I don't know from the question where the <ol>...</ol> is appended.
You can use jQueries $.when for that:
$.getJSON('https://hacker-news.firebaseio.com/v0/topstories.json', function(json) {
var requests = [];
for (var i = 0; i < 10; i++) {
requests.push($.getJSON('https://hacker-news.firebaseio.com/v0/item/' + json[i] + '.json'));
}
$.when.apply($, requests).done(function() {
var results = [].slice.call(arguments);
var list = results.map(function(arr) {
return '<li>' + arr[0].title + '</li>';
});
var convoText = '<ol>' + list.join('') + '</ol>';
console.log(convoText);
});
});
There are a few ways to fix this. The easiest is, instead of appending to convoText, use an array, and set its index when you get data. Like data[i] = json2;. Then when all your data is fetched, join your array.
A more structural fix would be to rearchitect your loop as a collection of promises, and construct your HTML when they have all resolved (what #xat was alluding to above).
While trying out jQuery, I have a question that is probably a newbie mistake, but I cannot seem to find the solution. This is the code:
$.get("index.html", function() {
var i = 0;
for (; i < 3; i++)
{
var lDiv = document.createElement('div');
lDiv.id = 'body-' + i;
document.getElementById('body').appendChild(lDiv);
$.get('index.html', function(data) {
lDiv.innerHTML = "<p>Hello World " + i + "</p>";
});
}
});
The output seems to be
<div id='body-0'></div>
<div id='body-1'></div>
<div id='body-2'>
<p>Hello World 3</p>
</div>
I expected the lDiv.innerHTML= code to be executed for each i, but apparently it is only executed for the last i? What am I overlooking?
This happens because the loop completes (i is 2) before any of the callbacks are fired.
#thecodeparadox's solution works, but it serializes the HTTP requests. (Makes them fire one-at-a-time.) This allows the requests to execute in parallel, and thus quicker:
for (var i = 0; i < 3; i++)
{
var lDiv = document.createElement('div');
lDiv.id = 'body-' + i;
document.getElementById('body').appendChild(lDiv);
$.get('index.html', function(i,lDiv) { // the current iteration's `i` and `lDiv` are captured...
return function(data) {
lDiv.innerHTML = "<p>Hello World " + i + "</p>";
}
}(i,lDiv)); // ...by passing them as an argument to the self-executing function
}
As $.get() is asynchronous, so you need to execute your append and next call within $.get()'s success() callback function.
var i = 0;
function recursiveLoad() {
if(i == 3) return;
var lDiv = document.createElement('div');
lDiv.id = 'body-' + i;
document.getElementById('body').appendChild(lDiv);
$.get('index.html', function(data) {
lDiv.innerHTML = "<p>Hello World " + i + "</p>";
i++;
recursiveLoad();
});
}
// initial call
recursiveLoad();
I have an array of jQuery objects that I built and which have events attached to them.
var obs = [];
for(var i=0;i<100;i++) {
var j = jQuery('<div></div>');
j.click(function() { console.log('Clicked ' + i); });
obs.push(j);
}
My HTML is:
<div id="replaceme"></div>
How do I replace the div on the page with all the jQuery objects in my array? I don't want to concatenate their HTML because I want to preserve the event handlers.
You can replace an element with an array of jQuery elements by doing this:
$(theElement).replaceWith(theArryaOfjQueryElements);
In your case, this is:
$('#replaceme').replaceWith(objs);
See the jQuery.replaceWith() method.
BTW you have an error here:
j.click(function() { console.log('Clicked ' + i); });
This will always print Clicked 100. The reason is that your function closes a reference to i, not to it's value. So if i is modified before this function is called, the function will see the new value of i.
You should do this instead:
(function(i) {
j.click(function() { console.log('Clicked ' + i); });
}(i));
Use replaceWith jQuery method which replaces each element in the set of matched elements with the provided new content.
var $obj = jQuery();//This will create an empty jQuery object
for(var i=0;i<100;i++) {
var j = jQuery('<div></div>');
j.click(function() { console.log('Clicked ' + i); });
$obj.add(j);//Using add method to add the jQuery object into $obj
}
$("#replaceme").replaceWith($obj);
var obs = [];
for(var i=0;i<100;i++) {
var j = jQuery('<div></div>');
j.click(function() { console.log('Clicked ' + i); });
obs.push(j);
}
$('#replaceme').html(objs);
/* or */
$('#replaceme').replaceWith(objs);
I read, that creating all elements at once is much faster then creating one by one. With the click function, you could count all previous siblings to get the index:
var str = ''
for (var i = 0; i < 100; i++) {
str += '<div></div>';
}
var elem = jQuery(str);
elem.click(function() { console.log('clicked '+jQuery(this).prevAll().length); });
jQuery('#replaceme').replaceWith(elem);
I have this problem where I do an .each() on this group of selects, for each one it fires off a call to the server for some data to populate it with. However I couldn't figure out why it would only populate the bottomest one. Then I threw in some alerts() and realized it was only running the call back function on the last one multiple times. I realized that by the time the first JSON call was done, $(this) was something different... How can I get it to wait so all of them will be populated by the proper call?
HERE IS THE SCRIPT PART:
var thisbundle;
var testcount = 0;
//get bundle options first..
$("select.bundle").each(function() {
thisbundle = $(this);
testcount++;
var url = "/order/getpricing/" + thisbundle.attr("id");
//clear it out...
//thisbundle.children().remove();
var passbundle = thisbundle;
$.getJSON(url, function(data, passbundle) {
var options = '';
for (n = 0; n < data.length; n++) {
options += '<option value="' + data[n].volumeID + '">' + explainPricing(data, n) + '</option>';
}
passbundle.html(options);
});
});
AND HERE IS THE FORM PART:
<div id="bundles">
<table>
<%foreach (KODmvc.Models.Product prod in Model.products)
{%>
<%if (prod.NumberOfCourses > 1)
{ %>
<tr><td><img src="<%=prod.Icon %>" /></td><td><b><%=prod.Title%></b><br /><%=prod.Description%></td><td><select class="bundle" id="<%=prod.ProductID %>"><option value="-1">None</option>"</select></td></tr>
<%} %>
<%} %>
</table>
</div>
Enclose the ajax call in an anonymous function like this. This creates a new closure for every select element. Each of these closures will remember it's own value for passbundle.
$("select.bundle").each(function(){
thisbundle = $(this);
testcount++;
var url = "/order/getpricing/" + thisbundle.val();
alert(thisbundle.id);
//clear it out...
//thisbundle.children().remove();
(function(){
var passbundle = thisbundle;
$.getJSON(url, function(data, passbundle){
var options = '';
for(n = 0; n < data.length; n++){
options += '<option value="' + data[n].volumeID + '">' + explainPricing(data, n) + '</option>';
}
passbundle.html(options);
});
})();
});
Declare thisbundle in your function and not in the global scope:
$("select.bundle").each(function(){
var thisbundle = $(this);
// …
});
Otherwise the global object would be overwritten with each iteration that the callback function would then use.
You could also use async : false, although that might be the wrong direction to head if you are looping. But it is worth looking at.