Getting value on hover produces "undefined" - javascript

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>

Related

DOM does not update when element changes (VUE)

for a vue/mvc project i am making a page divided into html sections.
If the user clicks on a button a javascript function is called that changes the display properties of the sections so that only the clicked section is shown.
When the dom is created, it calls the function and correctly shows one section.
However when the button is clicked, the function is called again, but the dom does not change.
Here is the code for the created function:
created: function () {
var self = this;
var sectionElements = document.getElementsByTagName("section");
for (var i = 0; i < sectionElements.length; i++) {
self.sections.push({ isSelected: false, object: sectionElements[i] });
}
for (var i = 0; i < self.sections.length; i++) {
self.sections[i].isSelected = false;
}
this.showSelectedSection(0);
},
Here is the code of the javascript function.
showSelectedSection(index) {
for (var i = 0; i < this.sections.length; i++) {
if (i == index) {
this.sections[i].isSelected = true;
this.sections[i].object.style.display = "block";
}
else {
this.sections[i].isSelected = false;
this.sections[i].object.style.display = "none";
}
}
Does anyone know why this is happening and how i can fix it?
Any tips or help is greatly appreciated.
First of all, I don't totally get why you're using self = this in this example, seems like it's not necessary. Nevertheless that is not your problem. You're modifying an object inside an array, and you're doing this by accessing the index. Normally that would be ok, but vue is not aware of this change. Try either passing the direct reference to the object inside the array or add a deep watch to your array so vue can hear this changes and make the proper modifications to your DOM.

for loop in multiselect not called first time javascript/jquery

I have a multiselect dependency where I display cities and their areas respectively.
The problem is that, both for loops in the function getArea only get called upon the second select of the cities option element.
Note: In debugger it works fine. I think its a scope problem and I tried using the foreach function but to no avail.
I have indicated below the line where the issue occurs.
//Global Variables
var allAreas = new Array();
$(document).ready(function() {
getCities();
$("#sel-city").bind("change", getAreas);
});
//get Cities on reload
function getCities() {
$.getJSON("cities.json", function(json) {
var citySelect = $("#sel-city");
for (var i in json) {
$("#sel-city")
.append($('<option>', {value: json[i].id})
.text(json[i].name));
}
});
}
function getAreas() {
var parentID = $(this).val();
console.log(parentID);
if (allAreas.length == 0) {
$.getJSON("areas.json", function(json) {
for (var i in json) {
allAreas.push(json[i]);
}
});
}
$('option', $("#sel-area")).remove();
var areasByParentID = new Array();
//Loop here
for (var i in allAreas) {
if (allAreas[i].city_id == parentID) {
areasByParentID.push(allAreas[i]);
}
}
console.log(areasByParentID);
//Loop here
for (var k in areasByParentID) {
$("#sel-area")
.append($('<option>', {value: areasByParentID[k].id})
.text(areasByParentID[k].name));
}
}
With the information you gave us and without sample data everything looks fine. But you have some minor mistakes which could lead to bad results. I decided to have a look on it, make some corrections and get it workin correctly with some sample data.
What you should take care of?
use for loops instead of for-in for arrays
you should avoid the new Array() operation
you should cache jquery variables to reduce siteload
for the areas to be set correctly on siteload you should just define init functions
What here could cause an error?
var a = [];
a[5] = 5;
for (var x in a) {
// Shows only the explicitly set index of "5", and ignores 0-4
}
The use of the for-in statement is to enumerate over object properties and will even inherited properties. Depending on your data it could give wrong results as shown in my example.
here is a jsfiddle with sample data.

Array value as condition of if else- JS

I've been trying to understand why whenever value of the array I click, it always add the class "foo".
Example: I clicked on London (cities[1], right?) and it added the class foo.
var cities = [
document.getElementById('Paris'),
document.getElementById('London'),
document.getElementById('Berlin')
];
for (var i = 0; i < cities.length; i++) {
cities[i].onclick = test;
function test(){
if(cities[i] === cities[0]) {
el.classList.add("foo");
}
}
}
EDIT: my original answer was incorrect, this updated one is right. addEventListener returns nothing. Instead, you should use some kind of wrapper to add and remove your listeners, again so that you don't waste resources on listeners that you aren't using:
function on (element, eventName, callback) {
element.addEventListener(eventName, callback);
return function unregister () {
element.removeEventListener(callback);
}
}
function test (event) {
if (event.currentTarget===cities[0]) {
event.target.classList.add('foo');
}
}
var listenerRemovers = cities.map(function (city) {
return on(city, 'click', test);
});
Now you can remove any of these listeners by calling the corresponding function in your listenerRemovers array:
listenerRemovers.forEach(function (unRegisterFunc) { unRegisterFunc(); });
ORIGINAL WRONG ANSWER:
For what it's worth, you're probably better off using .map in a case like this, since best practice is to keep a reference to the event listeners so you can cancel them if needed.
function test (event) {
if (event.currentTarget===cities[0]) {
event.target.classList.add('foo');
}
}
var listenerHandlers = cities.map(function (city) {
return city.addEventListener('click', test);
});
This is happening because you are setting the event functions inside a loop. Each function is using the same value of i.
Try to use this instead of trying to cities[i] inside the function.
function test(){
if(this === cities[0]) {
el.classList.add("foo");
}
}
The easiest approach to achieve this functionality is to use jQuery, here is the idea:
In html tags, give those cities a common class, e.g. class="city"
$('.city').click(function(){$('.city').addClass('foo')});
jQuery saves you more time and coding efforts.
The problem is you are trying to assign a function to a DOM attribute. You are not registering a listener but modifying the DOM. If you wish to do it this way, you must assign the onclick as cities[i].onclick = 'test()'
Also, you should move the function test outside of the for loop to look like the following. The problem is the function test is being declared many times, each with a different 'i' value.
for (var i = 0; i < cities.length; i++) {
cities[i].onclick = 'test(this)';
}
function test(el){
if(cities[i] === cities[0]) {
el.classList.add("foo");
}
}

generating dynamic onclick events with javascript

I am dynamically generating a series of onclick events where an alert() is associated with loop number of the pretended content. My problem is that currently the alerts outputs the 'i' value of the last loop rather than the i'th loop associated with the pretended content. Any thoughts?
JavaScript:
for (i = 1; i < 4; i++) {
prepend_content = 'foo';
$('#dynamic_div').prepend(prepend_content);
}
Many thanks.
Try concatenating it like you do before:
for (i = 1; i < 4; i++) {
prepend_content = 'foo';
$('#dynamic_div').prepend(prepend_content);
}
You might want to declare i and prepend_content (with var) in case you already haven't, to make sure they don't leak into the global scope.
At the same time, I wouldn't suggest using or adding HTML with inline event handlers. Try creating the element like this:
prepend_content = $("<a>").attr({
href: "#",
id: "img1_link_" + i
}).text("foo").on("click", (function (i) {
return function () {
alert(i);
};
})(i));
DEMO: http://jsfiddle.net/ujv4y/
The extra use of the immediately invoked function for the click handler is to make a closure that captures the value of i in the loop.
You can create a function using currying for the alert (for more complex stuff):
function(i) {
return function(){alert(i);}
}

Javascript multiple dynamic addEventListener created in for loop - passing parameters not working

I want to use event listeners to prevent event bubbling on a div inside a div with onclick functions. This works, passing parameters how I intended:
<div onclick="doMouseClick(0, 'Dog', 'Cat');" id="button_id_0"></div>
<div onclick="doMouseClick(1, 'Dog', 'Cat');" id="button_id_1"></div>
<div onclick="doMouseClick(2, 'Dog', 'Cat');" id="button_id_2"></div>
<script>
function doMouseClick(peram1, peram2, peram3){
alert("doMouseClick() called AND peram1 = "+peram1+" AND peram2 = "+peram2+" AND peram3 = "+peram3);
}
</script>
However, I tried to create multiple event listeners in a loop with this:
<div id="button_id_0"></div>
<div id="button_id_1"></div>
<div id="button_id_2"></div>
<script>
function doMouseClick(peram1, peram2, peram3){
alert("doMouseClick() called AND peram1 = "+peram1+" AND peram2 = "+peram2+" AND peram3 = "+peram3);
}
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
for (var i=0; i<names.length; i++){
document.getElementById(names[i]).addEventListener("click", function(){
doMouseClick(i, "Dog", "Cat");
},false);
}
</script>
It correctly assigns the click function to each div, but the first parameter for each, peram1, is 3. I was expecting 3 different event handlers all passing different values of i for peram1.
Why is this happening? Are the event handlers not all separate?
Problem is closures, since JS doesn't have block scope (only function scope) i is not what you think because the event function creates another scope so by the time you use i it's already the latest value from the for loop. You need to keep the value of i.
Using an IIFE:
for (var i=0; i<names.length; i++) {
(function(i) {
// use i here
}(i));
}
Using forEach:
names.forEach(function( v,i ) {
// i can be used anywhere in this scope
});
2022 edit
As someone is still reading and upvoting this answer 9 years later, here is the modern way of doing it:
for (const [i, name] of names.entries()) {
document.getElementById(name).addEventListener("click", () => doMouseClick(i, "Dog", "Cat"), false);
}
Using const or let to define the variables gives them block-level scope and the value of i passed to the handler function is different for each iteration of the loop, as intended.
The old ways will still work but are no longer needed.
2013 answer
As pointed out already the problem is to do with closures and variable scope. One way to make sure the right value gets passed is to write another function that returns the desired function, holding the variables within the right scope. jsfiddle
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
function getClickFunction(a, b, c) {
return function () {
doMouseClick(a, b, c)
}
}
for (var i = 0; i < names.length; i++) {
document.getElementById(names[i]).addEventListener("click", getClickFunction(i, "Dog", "Cat"), false);
}
And to illustrate one way you could do this with an object instead:
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
function Button(id, number) {
var self = this;
this.number = number;
this.element = document.getElementById(id);
this.click = function() {
alert('My number is ' + self.number);
}
this.element.addEventListener('click', this.click, false);
}
for (var i = 0; i < names.length; i++) {
new Button(names[i], i);
}
or slightly differently:
function Button(id, number) {
var element = document.getElementById(id);
function click() {
alert('My number is ' + number);
}
element.addEventListener('click', click, false);
}
for (var i = 0; i < names.length; i++) {
new Button(names[i], i);
}
It's because of closures.
Check this out: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#Creating_closures_in_loops_A_common_mistake
The sample code and your code is essentially the same, it's a common mistake for those don't know "closure".
To put it simple, when your create a handler function, it does not just accesses the variable i from the outer environment, but it also "remembers" i.
So when the handler is called, it will use the i but the variable i is now, after the for-loop, 2.
I've been struggling with this problem myself for a few hours and now I've just now managed to solve it. Here's my solution, using the function constructor:
function doMouseClickConstructor(peram1, peram2, peram3){
return new Function('alert("doMouseClick() called AND peram1 = ' + peram1 + ' AND peram2 = ' + peram2 + ' AND peram3 = ' + peram3 + ');');
}
for (var i=0; i<names.length; i++){
document.getElementById(names[i]).addEventListener("click", doMouseClickConstructor(i,"dog","cat"));
};
Note: I havn't actually tested this code. I have however tested this codepen which does all the important stuff, so if the code above doesn't work I've probably just made some spelling error. The concept should still work.
Happy coding!
Everything is global in javascript. It is calling the variable i which is set to 3 after your loop...if you set i to 1000 after the loop, then you would see each method call produce 1000 for i.
If you want to maintain state, then you should use objects. Have the object have a callback method that you assign to the click method.
You mentioned doing this for event bubbling...for stopping event bublling, you really do not need that, as it is built into the language. If you do want to prevent event bubbling, then you should use the stopPropagation() method of the event object passed to the callback.
function doStuff(event) {
//Do things
//stop bubbling
event.stopPropagation();
}

Categories

Resources