addEventListener for array elements - javascript

With this code I'm trying to iterate over a Array which is derived from a json Array.
There is a SVG inside the page.
When I click on a country, the name should be submitted to a URL.
Unfortunately I get the following error at the line indicated with -->error
Uncaught TypeError: Cannot call method 'addEventListener' of null
How can I overcome this error?? And why is it occurring?
The array is like this:
{"Countries":["india","switzerland","usa","alaska","germany","austria","netherlands","france","italy","spain","poland","hungary","czech","romania","russia","china","mexico","brazil","britain","thailand","sweden","uae","new_zealand_north_island","new_zealand_south_island","egypt"]}
var mySVG = document.getElementById("VectorMap");
var svgDoc;
mySVG.addEventListener("load", function () {
svgDoc = mySVG.contentDocument;
$.getJSON("http://www.someurl.com/jsonArray",
function (data) {
$.each(data, function (index, item) {
var i=0;
for (tot=item.length; i < tot; i++) {
var someElement = svgDoc.getElementById(item[i]);
//--->error
someElement.addEventListener("mousedown", function () {
$("#info").html(ajax_load).load("http://www.someurl.com/returnData"+"?country="+text);
}, false); //add behaviour
}
});
});
}, false);

You're not checking whether the element exists before attempting to attach the event handler; if the element doesn't exist, getElementById() would return null. This code would have that check.
var someElement;
for (var i = 0, tot = item.length; i < tot; i++) {
someElement = svgDoc.getElementById(item[i]);
if (someElement) {
someElement.addEventListener("mousedown", function () {
$("#info")
.html(ajax_load)
.load("http://www.someurl.com/returnData"+"?country="+text);
}, false); //add behaviour
}
}

The exception is happening because in the page there is no element with the id given. In such case, getElementById returns null: you can check by your web console / debugger in the browser, what is this id that doesn't exists, and if it is supposed to be there – and therefore you have an error or typo.
Anyway, you could take advantages of jQuery. Because in jQuery you can pass a list of ids, and the listener will be attached only to those elements that are actually in the page, without throwing exception. So in your case, instead of:
var i=0;
for (tot=item.length; i < tot; i++) {
var someElement = svgDoc.getElementById(item[i]);
//--->error
someElement.addEventListener("mousedown", function () {
$("#info").html(ajax_load).load("http://www.someurl.com/returnData"+"?country="+text);
}, false); //add behaviour
}
You can simply have:
$('#' + item.join(', #')).on('mousedown', function() {
$("#info").html(ajax_load).load("http://www.someurl.com/returnData"+"?country="+text);
});
So basically, assuming item is an array (and therefore I would call it items) with plain ids, like ['a', 'b', 'c'], with '#' + item.join(', #') you will obtain "#a, #b, #c" as string to pass to jQuery: if any on those elements doesn't exists in the page, the mousedown listener simply won't be attached, without raise any error.
Note: not sure where this text variable came from, I just put there because in your original example.

Related

Referring to the calling object in JavaScript using addEventListener

I'm new to JavaScript/HTML/CSS and I can't spot the mistake I'm doing in this JavaScript function. Our teacher told us to use the addEventListener method cause it has some notable advantages.
This is my entire script with the problematic function
var espandi = function (e) {
var toHide = document.getElementsByClassName("optional");
for (var index = 0; index < toHide.length; index++)
toHide[index].style.display = "none";
var toShow = e.target.getElementsByClassName("optional");
for (index = 0; index < toShow.length; index++)
toShow[index].style.display = "block";
}
var expansibleObjects = document.getElementsByClassName("singleresult");
for (var index = 0; index < expansibleObjects.length; index++)
expansibleObjects[index].AddEventListener("click",espandi);
The fact is the line e.target.getElementsByClassName gets me an error of this type
Uncaught TypeError: Cannot read properties of undefined (reading 'getElementsByClassName').
On the contrary, if I set the function with the property "onclick" directly on the element the function works perfectly. So I think the problem it's about referring to the calling object using e.target
Update 1
First of all, I want to say that this is a project for university and I cannot publish the whole code, it would be risky for my exam.
Then there are more issues apparently. First of all with the method getElementsByClassName() applied on document it seems that he can get the Collection of Elements but then if I try to print the single Element it gives me undefined on the log. Here's the code:
var list = document.getElementsByClassName("prova");
if (list[0]) {
console.log("Assigning using \"list[0]\" as check");
list[0].onclick = espandi();
list[0].addEventListener("click",espandi);
console.log("finished adding listeners");
}
else if(list.item(0)){
console.log("Assigning using \"list.item(0)\" as check");
list.item(0).onclick = espandi();
list.item(0).addEventListener("click",espandi);
console.log("finished adding listeners");
}
else console.log("failed assignment");
console.log("printing list");
console.log(list);
console.log("printing list[0]");
console.log(list[0]);
console.log("printing list.item(0)");
console.log(list.item(0));
and here's the log output:
log output
Apparently the only way I succesfully make my script work is editing the function this way:
function espandi (caller) {
var toHide = document.getElementsByClassName("optional");
for (var index = 0; index < toHide.length; index++)
toHide[index].style.display = "none";
var toShow = caller.getElementsByClassName("optional");
for (index = 0; index < toShow.length; index++)
toShow[index].style.display = "block";
}
and then assigning it to elements directly using the "onclick" HTML attribute, like this:
<table class="prova" onclick="espandi(this)">
so that the parameter "caller" refers to the element who actually triggered the function espandi. The problem is I really want to know 2 things:
how to refer to the caller using an EventHandler function (in the case of method .addEventListener()) and a normal function (in the case of the attribute .onclick of the desired element) in JS.
how to manage the method getElementByClassName() and the collection returned by itself.
In the end, just to sum up, now I have problems with assigning the event listeners and also with referring to the caller without using a parameter like this in the HTML code I showed you.
You are calling espandi instead of assign it to onclick handler.
So you need to remove the () and do expansibleObjects[index].onclick = espandi;
Anyway in your question I don't see any e.target.addEventListener() so when you say The fact is the line e.target.addEventListener() gets me an error I don't understand what you mean, maybe you have to add more code.

Getting value on hover produces "undefined"

This jsfiddle demonstrates a basic mockup of what I'm trying to achieve. After clicking the link, I should be able to hover over the list elements and text should be appearing on the page, but they don't. When I print the values of the strings that should be appearing, they are "undefined." Why is this so?
Here's the js below, but I recommend looking at the fiddle.
$('#link1').click(function () {
var foolist = ["foo1", "foo2", "foo3"];
for (var i = 0; i < foolist.length; i++) {
var li = document.createElement('li');
li.innerHTML = "This is a link.";
$(li).hover(function () {
console.log(foolist[i]);
$('#p1').append(foolist[i]);
},
function () {});
$('#ul1').append(li);
}
});
You've discovered the need for something called closures! Congratulations - not many people get to see how cool they are. This is pulled from a great MSDN article - I highly recommend reading the rest of it.
The problem is that i variable no longer has the value it did when you called .hover - if you log it you'll see that i===3. Why would that be? The function that you're passing to .hover is a closure - which means it consists of the function itself and a sort of "snapshot" of the .click function's scope. You create a closure with each iteration of the loop, but they all share the same "snapshot". By the time you try to get access to i via the click event, the loop has already completed.
So how can you solve it? More closures!
function showText(i) {
$('#p1').append(foolist[i]);
}
function makeTextCallback(i) {
return function() {
showText(i);
};
}
for (var i = 0; i < foolist.length; i++) {
$(li).hover(makeTextCallback.call(this,i));
}
This is called a "function factory". You send i to makeTextCallback which captures i within the closure that it returns.
https://jsfiddle.net/aLofhaxp/28/
The simple rule is to avoid any closures within loops.
Another good way is to define such things in data attributes:
$('#link1').click(function () {
var foolist = ["foo1", "foo2", "foo3"];
foolist.forEach(function(x) {
$("<li/>")
.data('list', x)
.text("This is a link");
.hover(fooListItemOnHover, fooListItemOnHoverOff)
.appendTo('#ul1');
}
});
function fooListItemOnHover() {
var data = $(this).data('list');
console.log(data);
$('#p1').append(data);
}
function fooListItemOnHoverOff() {
}
This will produce elements with additional data-list attribute, which will store your custom data:
<li data-list='foo1'>This it a link</li>
Then, your script will read this data from it using jQuery data().
Use .data() method of jQuery to attach data of any type to DOM elements.
$('#link1').click(function () {
var foolist = ["foo1", "foo2", "foo3"];
for (var i = 0; i < foolist.length; i++) {
var $li = $("<li/>");
$li.data("VAL", foolist[i]);
$li.html("This is a link.");
$($li).hover(function () {
var value = $(this).data("VAL");
console.log(value);
$('#p1').append(value);
},
function () {});
$('#ul1').append($li);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<a id="link1" href="javascript:;">Click to show list</a>
<ul id="ul1"></ul>
<p id="p1"></p>

Calling a method of a global object from a function

I found out how to solve the problem but I could not understand what the problem was although, I know why it is working now.
Here is the code that works:
function onReceive(json) {
for (var i = 0; i < json.length; i++) {
var m = $("<p/>", {
"class": "message",
html: json[i].message
});
$("#messages").append(m);
}
}
I quote from here:
var carName = " Volvo";
// code here can use carName
function myFunction() {
// code here can use carName
}
My question is, why wouldn't this work?
messages = $("#messages");
function onReceive(json) {
for (var i = 0; i < json.length; i++) {
var m = $("<p/>", {
"class": "message",
html: json[i].message
});
messages.append(m);
}
}
I guess you are using messages = $("#messages"); before DOM ready. So at that time your javaScript not able to get $("#messages") and you are using onReceive() function after DOM load so it's working inside your function.
You need to get element when it is part of page already.
You could wrap $ call with document ready callback, like this:
$(document).ready(function () {
messages = $("#messages");
});
Otherwise selector return empty collection (no element with id messages found).
Such wrapper is not required in event handlers, because event callbacks are always called after document is ready.

Why doesn't calling a function from an object work in this for loop?

Edit: Problem solved, I forgot to put the output for the riteg object before.
Okay, so I'm quite sure how to describe this issue (which is why the title is bad), but this for loop:
function(){
var out = 0;
for(var i = 0;i<gameLists.listAllGen.length;i++){
out += window[gameLists.listAllGen[i]].output();
}
return out/10000;
},
doesn't work at all. In the console, it says: TypeError: window[gameLists.listAllGen[i]].output is not a function
When I replace the [i] with, for example, [0], it works:
function(){
var out = 0;
for(var i = 0;i<gameLists.listAllGen.length;i++){
out += window[gameLists.listAllGen[0]].output();
}
return out/10000;
},
returns out as expected and no errors.
The variables if needed:
var gameLists = {
listAllGen:['solarGen','riteg']
}
and
var solarGen = {
name:"Solar Generator",
count:0,
genRate:0.25,
price:410,
output:
function(){
return this.count*this.genRate*10000;
},
gameLists.listAllGen[0] returns -> solarGen
and you have solarGen in your script, thus window[gameLists.listAllGen[0]].output() referes to the output key and () calls the function assosiated with the key solarGen
But gameLists.listAllGen[1] return -> riteg
and no output property is assosiated with the riteg, thus it, and window[gameLists.listAllGen[1]] it expects. that there is riteg with output as a key aand a function assosiated with it, but it is not able to find that.
Thus you are getting the error
TypeError: window[gameLists.listAllGen[i]].output is not a function
because there is no function by that name.

jQuery - why can't I bind events to elements in a loop?

Here is my code:
var b = $(slipStream.conf.mainVis).find('p#prev');
b.click(function() {
slipStream.slideLeft();
return false;
});
b = $(slipStream.conf.mainVis).find('p#next');
b.click(function() {
slipStream.slideRight();
return false;
});
b = $(slipStream.conf.controls).find('li img');
console.log(b);
for (var l in b) {
l.click(function() {
var visIndex = l.index();
console.log(visIndex);
});
};
The first two bindings go through, no problem. But I can't loop through a collection and bind something to each member? (the console is telling me that "l.click is not a function.") Is this a limitation of jQuery or is my code off? This seems like it would be the way to do it, though...
When you enumerate over a jQuery object, the values being enumerated are actual DOM nodes and not jQuery wrappers. Therefore, they don't have a click method but you can wrap them again to get all the usual functionality.
Of course this is not necessary because you can simply attach a wrapper directly from your initial jQuery instance:
$(slipStream.conf.controls).find('li img').click(function() {
var visIndex = $(this).index();
console.log(visIndex);
});
This is the classic "loop variables don't work properly in callbacks" bug.
Your variable l no longer has the originally supplied value by the time the callback is invoked - it has whatever final value was assigned in the last pass through the loop.
[FWIW, l isn't actually a jQuery object, so you have to wrap it - $(l) to use it with jQuery]
The usual fix to the loop bug is to create an additional closure that returns a function bound to the current value:
for (var l in b) { // NB: don't use `for ... in ...` on array-like objects!
var make_cb = function(n) {
return function() {
var visIndex = $(n).index();
console.log(visIndex);
}
}
$(l).click(make_cb(l));
};
Fortunately, you don't need a loop at all - you can have jQuery automatically add the callback to every element by itself:
b = $(slipStream.conf.controls).find('li img');
b.click(function() {
var visIndex = $(this).index();
console.log(visIndex);
});
Could it be that the problem is the forloop. .click is part of the jQuery, so you must be sure that it's called on element that is wrapper with jQuery.
$.each(b, function (index, element) {
$(element).click(function() {
});
};
With each() you can iterate through a set of jQuery objects:
$(slipStream.conf.controls).find('li img').each(function(){
$(this).click(function() {
var visIndex = $(this).index();
console.log(visIndex);
});
});
$(this) will match the currently indexed object from the collection.

Categories

Resources