AppendChild adds all new elements to last element in loop. Why? - javascript

I'm looping a set of div's conaining an id for YouTube videos to get and show their titles, descriptions, etc. The divs I loop through look like this:
<div class="tutorialVideoDataContainer" data-clipid="xxxxxxxxxxx"></div>
<div class="tutorialVideoDataContainer" data-clipid="xxxxxxxxxxx"></div>
<div class="tutorialVideoDataContainer" data-clipid="xxxxxxxxxxx"></div>
<div class="tutorialVideoDataContainer" data-clipid="xxxxxxxxxxx"></div>
<div class="tutorialVideoDataContainer" data-clipid="xxxxxxxxxxx"></div>
For some reason the loop adds all of the info to the last of these divs instead of applying the info to the current div inside the loop. Any idea why this is?
And also, as a sub question, why can't I reach i inside the ajax call? I have to set tutorial = tutorials[i] and append the children to tutorial instead of tutorials[i] but I can't use just tutorials[i] within the ajax part. I can't understand what the difference is.
(function () {
var tutorials = document.getElementsByClassName('tutorialVideoDataContainer');
for (var i = 0; i < tutorials.length; i++) {
var tutorial = tutorials[i];
console.log(i);
console.log(tutorial);
console.log(tutorials[i]);
var id = tutorial.getAttribute('data-clipid');
$.ajax({
url: "http://gdata.youtube.com/feeds/api/videos/" + id + "?v=2&alt=json",
dataType: "jsonp",
success: function (data) {
var title = data.entry.title.$t;
var description = data.entry.media$group.media$description.$t;
var duration = data.entry.media$group.media$content[0].duration;
var thumbnail = data.entry.media$group.media$thumbnail[3].url;
if (title.length > 0) {
var titleElement = document.createElement("h3");
titleElement.innerText = title;
tutorial.appendChild(titleElement);
}
if (description.length > 0) {
var descriptionElement = document.createElement("p");
descriptionElement.innerText = description;
tutorial.appendChild(descriptionElement);
}
var minutes = Math.floor(duration / 60);
var seconds = duration - minutes * 60;
var durationElement = document.createElement("p");
durationElement.innerText = minutes + ":" + seconds;
tutorial.appendChild(durationElement);
var clickLink = document.createElement("a");
clickLink.className = "showOverlayVideo";
var thumbnailElement = document.createElement("img");
thumbnailElement.src = thumbnail;
thumbnailElement.alt = title;
clickLink.appendChild(thumbnailElement);
tutorial.appendChild(clickLink);
}
});
}
})();

If you loop over array and crate closure where you access the index of the array you access reference to variable not copy of it so when all asynchrous code is finished you end up with reference to last element. To fix it you need to create another closure with new reference:
for (var i = 0; i < tutorials.length; i++) {
(function(tutorial) {
// you can use tutorial inside
})(tutorials[i]);
}

Because of the way the javascript event loop works, the loop finishes running before your ajax requests come back. So by the time they do the variable tutorial is set to the last one in the loop.
Does the data you get back from the youtube api include the clip ID, you could then use that to look up the correct dom element in the callback rather than inside the loop.

My guess is that since $.ajax() is not a blocking function, the loop is finishing before ajax is done doing it's thing. This would cause the execution path to look something like this:
Loop starts, i = 0
Ajax call is made
i = 1
i = 2
...
i = tutorials.length
Ajax call finishes
Hope that helps / makes sense.

Related

Retrieving a variable from within a function

I am attempting to pull information from the League of Legends API.
To simplify what I am doing, I am attempting to pull information about a user and their previous matches. The problem that I run into is that when I parse a JSON request, it returns a champion ID rather than their name (Ex: 412 rather than "Thresh").
The only solution I can see for this would be to make another JSON request and parse that data for the champion name. Currently what I have looks like this.
$.getJSON(championMasteryPHP, function (json) {
for (var i = 0; i < 20; i++) {
var champID = json[i].championId;
var championInfo = "http://example.com/champInfo.php?summonerid=" + champID;
$.getJSON(championInfo, function (json2) {
var champName = json2.name;
});
$('#champ').append("<li>"+champID+" - "+champName+"</li>")
}
});
I'm unable to access the champName variable due to it being nested within the second JSON function.
Is there a better way to do this?
$.getJSON(championMasteryPHP, function (json) {
for (var i = 0; i < 20; i++) {
var champID = json[i].championId;
var championInfo = "http://example.com/champInfo.php?summonerid=" + champID;
$.getJSON(championInfo, function (json2) {
var champName = json2.name;
$('#champ').append("<li>"+champID+" - "+champName+"</li>")
});
}
});
Just put it inside the second json request since you need to wait till that request is done anyway.
You should put the append statement in the callback because getJSON is an asynchronous method (does mean the Request is running in the background, and calls your function back when it got a response), so you should wait for the response first then you can append it to #champ :
$.getJSON(championMasteryPHP, function (json) {
for (var i = 0; i < 20; i++) {
var champID = json[i].championId;
var championInfo = "http://example.com/champInfo.php?summonerid=" + champID;
$.getJSON(championInfo, function (json2) {
var champName = json.name;
$('#champ').append("<li>"+champID+" - "+champName+"</li>")
});
}
});
Hope this helps.

How to trigger a second AJAX call upon clicking of hyperlink?

I've an API that I'm connecting to. I've an index.html page with two columns in it. The left column contains thumbnails of images (see Part A) and the right column will contain details of information about the images once it is clicked on. There are currently two AJAX calls (both calling different URLs) within the same JS script file.
Part A
$.ajax({
url: 'http://gateway.marvel.com:80/v1/public/characters?limit=5&offset=300&apikey=' + publickey + "&ts=" + ts + "&hash=" + hash,
method: 'get',
success: function (res) {
var characters = res.data.results;
var index = 0;
loadCharacters();
function loadCharacters() {
if (index > characters.length) {
index = 0;
return;
}
for (index = 0; index < characters.length; index++) {
var hero = characters[index];
var heroID = hero.id;
var heroName = hero.name;
var image = hero.thumbnail;
var image_url = image.path + '.' + image.extension;
var image_target = $("#comics")
$('<img>').attr({
src: image_url,
width: 80,
height: 80,
}).appendTo($('<a>').attr({
href: '#?' + heroID,
//}).click(dummyCall(hero.id)).appendTo(image_target)); //NOTE: to figure how to pass this hero.id to second ajax call..
}).appendTo(image_target));
image_target.append("<br/>" + heroName + "<br/>");
}
} // end first ajax
}
});
Part B
$.ajax({
url: "https://gateway.marvel.com:443/v1/public/characters/1009269/comics?limit=5&apikey=" + publickey + "&hash=" + hash + "&ts=" + ts,
method: 'get',
success: function (res) {
var comicDetails = res.data.results;
var index = 0;
loadComic();
function loadComic() {
if (index > comicDetails.length) {
index = 0;
return;
}
for (index = 0; index < comicDetails.length; index++) {
var comic = comicDetails[index];
var comicTitle = comic.title;
$("#comics").append("<br/>" + comicTitle + "<br/>");
}
}
}
});
Part B is still incomplete, but for the purpose of this question, it should suffice. I need to know how I can trigger Part B once the user clicks on the anchored image hyperlinks in the left sidebar. I don't want Part B to load immediately because Part B is dependent on the hero.id (from Part A) being clicked and passed to it.
You'll want to have a click handler for the images. You can do this in a couple ways.
Either directly
$imageElements.on('click', functionThatCallsAjaxB);
Or indirectly with a delegate
$("#comics").on('click', 'img', functionThatCallsAjaxB);
The difference being that the first puts a handler on each image and the image has to exist when you execute that line. The second is a single handler attached to the parent that reacts when one of this children has that event.
I also see in your ajax A that your wondering how to pass information to the second ajax call. One way you can do this is with data elements.
$element.data('heroId', 'myvalue'); <-- setter
$element.data('heroId') <-- getter
So you can set them on the images when you create them and reference them later.
An assumption on my part, lets assume that hero id is a unique value for each image. If so, in you loop,
$('<img>').attr({
src: image_url,
width: 80,
height: 80,
id: hero.id,
...
Because you are assigning a click handler, you don't need the '< a>'.
Elaborating on Taplar's answer a little more, $("#comics").on('click', 'img', functionThatCallsAjaxB); after the looping is done (you only have to do it once.
If you do it this one...
$("#comics").on('click', 'img', function(evtObj){ functionThatCallsAjaxB(evtObj);});
with function
functionThatCallsAjaxB(obj) {
// the image that was clicked will be obj.target.
// with the above assumption,
var heroID = obj.target.id;
// the rest of your part b ajax goes here.
}

Nested for loop with inner functions

In javascript For loop it's works fast and inner functions are not called. I am trying to store images in database using cordova. In my code for loop works fast and finished but I didn't get any base64 images.
for(var i = 0; i < pages.length; i++)
{
var cat_img = res.Catalogue[0].Catalogue_img;
var catalogue_image_id = cat_img[i].catalogue_image_id;
var catalogue_image = cat_img[i].catalogue_image;
getBase64FromImage(catalogue_image,function (baseData64) {
console.log("baseData64===="+baseData64);
insertPageData (catalogue_image, catalogue_image_id);
},function (error) {
console.log("error====="+error);
});
}
Thanks in advance!!
Your callback "function (baseData64)" will be executed only when the image is downloaded and converted to base64. At that point, the value of "calalogue_image" and "catalogue_image_id" will contain the value of the last element in the loop - with "i = pages.length"
That is, your for loop finishes probably before even the first image is downloaded.
Actually I think the valid behaviour is that you should see only the last image, with "i = pages.length" in the database.
Try this
var processImage = function (cImg,cImgId){
var catalogueImage = cImg;
var catalogueImageId = cImgId;
return function (baseData64) {
console.log("baseData64===="+baseData64);
insertPageData (catalogueImage, catalogueImageId);
}
}
for(var i = 0; i < pages.length; i++)
{
var cat_img = res.Catalogue[0].Catalogue_img;
var catalogue_image_id = cat_img[i].catalogue_image_id;
var catalogue_image = cat_img[i].catalogue_image;
getBase64FromImage(catalogue_image,
processImage(catalogue_image, catalogue_image_id) ,
function (error) {
console.log("error====="+error);
}
);
}

Stop recursion and start over

I have the following function, that is called once in my program. When there is no user interaction it starts over again and again by recursion when the array is printed out. But when a user clicks on a link the function shall be stopped first (no more output from the array) and then be started again with new parameters.
function getText(mode){
var para = mode;
var url = "getText.php?mode=" + para;
var arr = new Array();
//get array via ajax-request
var $div = $('#text_wrapper');
$.each(arr, function(index, value){
eachID = setTimeout(function(){
$div.html(value).stop(true, true).fadeIn().delay(5000).fadeOut();
if(index == (arr.length-1)){
clearTimeout(eachID);
getText(para);
}
}, 6000 * index);
});
}
My code doesn't really stop the function but calling it once more. And that eventuates in multiple outputs and overlaying each other. How can I ensure that the function stops and runs just one at a time?
$(document).ready(function() {
$("#div1, #div2").click(function(e){
e.preventDefault();
var select = $(this).attr("id");
clearTimeout(eachID);
getText(select);
});
});
You keep overwriting eachID with the latest setTimeout, so when you clearTimeout(eachID) you are only stopping the last one.
Personally, I like to do things like this:
id = 0;
timer = setInterval(function() {
// do stuff with arr[id]
id++;
if( id >= arr.length) clearInterval(timer);
},6000);
This way, you only have one timer running, and you can clear it at any time.

How to check table for new content, and then update browser tab title?

I am working on a way to flash a browser tab when a new message appears in a table. I have the flashing of the tab part working, my only problem is that I can't seem to get it to flash when a message is received (which is the whole point of my exercise :) )
The newMessage() function is working fine, I just can't seem to get the notification() function to work.
My code is as follows:
function newMessage()
{
var oldTitle = "Your Page";
var msg = "New Message";
var timeout = setInterval(function()
{
document.title = document.title == msg ? '' : msg;
}, 1000);
window.onmousemove = function() {
clearInterval(timeout);
document.title = oldTitle;
window.onmousemove = null;
};
}
function notification()
{
var index = 2;
var content = document.getElementById('refreshMessages').childNodes[index];
var content = document.getElementById('refreshMessages').getElementByTagName("tr")[1];
var knownContent = content.toString();
updater.start();
updater2.start();
var newContent = document.getElementById('refreshMessages').childNodes[index];
var newContent = document.getElementById('refreshMessages').getElementByTagName("tr")[1];
if(knownContent != newContent.toString())
{
newMessage();
knownContent = newContent;
}
else if(knownContent = newContent.toString())
{
alert("No need to flash title.");
}
}
notification();
In the notification() function, I am trying to call the newMessage() function by comparing the strings at the appropiate cell in the table.
I put the alert() into the else if just to see if it would be called, but it does not happen. update.start() and update2.start() are carried out however, as I can see the messages appearing in the table.
I would be happier to use JavaScript but I am open to jQuery also.
My JavaScript is very very rusty so excuse me if I have made any silly mistakes!
Thanks,
Chuck
You have several mistakes in function notification(), see my comments:
function notification()
{
var index = 2;
//Why are you assigning value to "content" for twice?
var content = document.getElementById('refreshMessages').childNodes[index];
/*
* function getElementByTagName is undefined, should be getElementsByTagName,
* 's' is missing. And [1] means the second one not the first one, make sure
* that's exactly what you want.
*/
var content = document.getElementById('refreshMessages').getElementByTagName("tr")[1];
/*
* content is a tr dom object, content.toString() is something like "[object]".
* If you want to get content inside a cell, you should use cell.innerHTML.
* e.g. A table:
* <table id="refreshMessages">
* <tr><td>Hello world</td></tr>
* </table>
* var table = document.getElementById('refreshMessages');
* var firstTr = table.getElementsByTagName("tr")[0];
* var firstTd = firstTr.getElementsByTagName("td")[0];
* alert(firstTd.innerHTML); //alerts "Hello world"
*/
var knownContent = content.toString();
//I doubt these functions really get invoked cuz there's javascript error above.
updater.start();
updater2.start();
//assigning twice, "getElementByTagName" is missing "s"
var newContent = document.getElementById('refreshMessages').childNodes[index];
var newContent = document.getElementById('refreshMessages').getElementByTagName("tr")[1];
//Remove toString(), use innerHTML i metioned above.
if(knownContent != newContent.toString())
{
newMessage();
knownContent = newContent;
}
//You miss an "=" here, to judge a equals b, you should use "=="
else if(knownContent = newContent.toString())
{
alert("No need to flash title.");
}
}

Categories