add onclick event with values from object - javascript

in the following code, why does postCodes[i].countryCode return the last value in the loop,
and not the current value in the loop?
and how can i return the current value in the loop?
for (var i = 0; i < postCodes.length; i++) {
for (var ind = 0; ind < salesSuite.Defaults.Countries.length; ind++) {
if (postCodes[i].countryCode == myNamespace.Countries[ind].code) {
$('<button/>')
.click(function () {
console.log(postCodes[i].countryCode);
})
.appendTo($modalContent);

Try adding a function that gets a handler which creates a local variable to hold that postCode. And of course the reason for this is the usage of shared variable i inside the handler which would have run out by the time the handler is invoked So ultimately in your handler you are trying to invoke postCodes[postCodes.length].countryCode which is same as undefined.countryCode and will throw an error..
$('<button/>')
.click(getHandler(postCodes[i]))
.appendTo($modalContent);
.....
.....
function getHandler(postCode){
// This function creates a closure variable postCode which will hold that particular postCode passed in for each invocation to be used in the function reference returned by this
return function () {
console.log(postCode.countryCode);
}
}
Demo
Instead of doing all this you can utilize jquery data api to hold the postCode.
$('<button/>')
.click(function () {
console.log($(this).data('postCode').countryCode);
})
.appendTo($modalContent).data('postCode', postCodes[i]);
Demo

Related

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");
}
}

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();
}

Understanding Scope of 'this' in jQuery on change event

I wrote a quick custom extension for jQuery for a project I am working on. I am having a hard time understanding the scope of 'this' in a custom onChange method I would like implement. If left out the middle of my code where I am calling the webservice but if you checkout the last two methods, you will see where my problem is. I want to call the updateDetails method with the selected value changes. However, when that method is called within the onchange event, I obviously lose the scope of "this" as this.materialListResponse comes back as undefined in this context. Any help on helping me understand this would be greatly appreciated.
$.fn.appendMaterials = function (options) {
this.options = options;
//Set Default Options
this.defaults = {
typeID: '66E1320D-51F9-4900-BE84-6D5B571F9B80'
};
this.options = $.extend({}, this.defaults, options);
//
Code here to call web service and generate response XML
//
this.materialListResponse = $.xml2json(
$.parseXML($(this.materialListWebservice()).find("GetMaterialTreeResponse").text())).Materials.Material;
this.appendOptionString = function () {
var i = 0;
this.optionString = '<option>'
for (i = 0; i < this.materialListResponse.length; i++) {
this.optionString += '<option>' + this.materialListResponse[i].MaterialCode + '</option>';
};
this.append(this.optionString);
return this;
};
this.appendOptionString();
this.updateDetails = function () {
for (i = 0; i < this.materialListResponse.length; i++) {
if (this.materialListResponse[i].MaterialCode === this.val()) {
$('#table1 #MaterialDescription').val(this.materialListResponse[i].Description);
}
}
}
this.change(this.updateDetails)
};
pass the object this as data to the event:
this.change({that: this}, this.updateDetails)
and then you can access that in the scope of the event callback
this.updateDetails = function(event) {
var that = event.data.that;
...
}
RESOURCES
http://api.jquery.com/event.data/
The event handler will be called later, when you have exited the extension. It's called in the scope of the element, so this will be the element that has changed.
Copy the reference to the list to a local variable, and use that in the event handler:
var list = this.materialListResponse;
this.updateDetails = function() {
for (i = 0; i < list.length; i++) {
if (list[i].MaterialCode === this.val()) {
$('#table1 #MaterialDescription').val(list[i].Description);
}
}
}
By using the local variable in the function, the variable will be part of the closure for the function, so it will survive the scope of the extension method where it is declared.
When a method is called in JavaScript as a callback it behaves as a function. In this case "this" refers to the owner of this function, usually the Window object in a Web browser.

Why couldn't I pass the value in javascript?

for (id = 50; id < 100; id++)
{
if($('#'+id).attr('class') == 'myField')
{
$('#'+id).bind('click', function() { install(id); } );
}
}
No idea why id can't reach 'install' in function(). I am trying to bind every button (id from 50 to 100) with a click event to trigger the install(id) function. But it seems the variable id cannot reach install function. While I hard code it:
for (id = 50; id < 100; id++)
{
if($('#'+id).attr('class') == 'myField')
{
$('#'+id).bind('click', function() { install( 56 ); });
}
}
it works! Please tell me why.
What you made is one of the most common mistakes when using Javascript closures.
By the way the very fact that this mistake is so common is IMO a proof that it's indeed a "bug" in the language itself.
Javascript supports read-write closures so when you capture a variable in a closure it's not the current value of the variable that is captured, but the variable itself.
This means that for example in
var arr = [];
for (var i=0; i<10; i++)
arr.push(function(){alert(i);});
each of the 10 functions in the array will contain a closure, but all of them will be referencing the same i variable used in the loop, not the value that this variable was having at the time the closure was created. So if you call any of them the output will be the same (for example 10 if you call them right after the loop).
Luckily enough the workaround is simple:
var arr = [];
for (var i=0; i<10; i++)
arr.push((function(i) {
return (function(){alert(i);});
})(i));
using this "wrapping" you are calling an anonymous function and inside that function the variable i is a different one from the loop and is actually a different variable for each invocation. Inside that function i is just a parameter and the closure returned is bound to that parameter.
In your case the solution is therefore:
for (id = 50; id < 100; id++)
{
if($('#'+id).attr('class') == 'myField')
{
$('#'+id).bind('click',
(function(id){
return (function() { install(id); });
})(id));
}
}
By not reaching the install(), I guess you mean you get all your install(id) behaves like install(100).
Reason why it doesn't work
This is caused by the javaSctipt closure. This line function() { install(id) } assign the id to the install() callback function. The id's value won't be resolved until install() is call when is far later after the loop is finished - the time when id has already reached 100.
The solution is create another closure the hold the current id value.
for (id = 50; id < 100; id++)
{
if($('#'+id).attr('class') == 'myField')
{
(function (id) {
$('#'+id).bind('click', function() { install(id); });
}) (id);
}
}
Here is a demonstration code:
var funcCollections = [];
for (id = 50; id < 100; id++)
{
if(true)
{
(function () {
var thatId = id;
funcCollections.push(function () {console.log(thatId,id)});
}) ();
}
}
// funcCollections[1]();
// 51 100
// undefined
// funcCollections[2]();
// 52 100
You can't pass a variable to the function you've bind. It loses the val. When you pass '56' it will be always 56, but when you pass a var, the JavaScript will not bind the value of the var in the loop.
When you loop over variables and you create anonymous functions(closure) that reference the loop variable they will reference the last value
also note that you don't limit scope the loop variable to the for loop(it's not declared with var) so that means that later modifications to that variable will be propagated to all closures.
take a look at this
It's down to variable scope.
The anonymous function you're binding to the click event of the $('#' + id) elements has no awareness of the id variable in the your sample code (assuming that your sample code is an excerpt from a function). Even if it did (e.g. you declared id outside of any function, giving it global scope), id would hold the value 100 when the click event was called, which isn't what you intend.
However, you could use $(this).attr('id') to get hold of the element's id value instead:
for (id = 50; id < 100; id++)
{
if($('#' + id).attr('class') == 'myField')
{
$('#' + id).bind('click', function()
{
install(parseInt($(this).attr('id')));
});
}
}
Check out the jQuery .bind() documentation, it shows how this can be used from within an event handler.

Jquery assignment fails when inside a for loop

I have a jquery which works correctly when I do this:
var slide = [];
slide[1] =
{
hide: function() {
$("#slide-1").hide();
},
show: function() {
$("#slide-1").show(2000);
}
};
slide[1].show(); <<< works fine
But if I try it in a loop in fails:
for (var i=1; i <= totalSlides; i++) {
slide[i] =
{
hide: function() {
$("#slide-" + i).hide();
},
show: function() {
$("#slide-" + i).show(2000);
}
};
};
slide[1].show(); << unassigned
any idea?
Well, you're saying that it is "unassigned" but I'm guessing that the function is just not doing what you want.
This is a common issue. All the functions you're creating in the loop are referencing the same i variable. This means that when the function runs, it is getting the value of i where it was left after the loop finished.
You need to scope the variable that your functions reference in a new variable environment to retain the value from the loop. To do that, you need to invoke a function, and have that function reference the current i value.
Like this:
function generate_functions( j ) {
// v----- DO NOT place the opening brace on the next line, after the
return { // return statement, or your code will break!!!
hide: function() {
$("#slide-" + j).hide();
},
show: function() {
$("#slide-" + j).show(2000);
}
};
}
var slide = [];
for (var i=1; i <= totalSlides; i++) {
slide[i] = generate_functions( i );
};
slide[1].show(); // should work
I created a function called generate_functions(), and invoked it in each iteration, passing i as an argument.
You'll notice that generate_functions() received the value as the j parameter. You could call it i as well, but changing the name makes it a little clearer IMO.
So now your functions are referencing the local j. Because a new variable environment is created with each invocation of generate_functions(), the functions inside that you create will be referencing the j value of that specific variable environment.
So the generate_functions() returns the object that contains the functions that were created in each new variable environment, and that object is assigned to slide[i].
Is the $("slide-1" + i).show(2000) a typo, or the error?
Add var slide = []; above the for loop.

Categories

Resources