How to assign event callbacks iterating an array in javascript (jQuery) - javascript

I'm generating an unordered list through javascript (using jQuery). Each listitem must receive its own event listener for the 'click'-event. However, I'm having trouble getting the right callback attached to the right item. A (stripped) code sample might clear things up a bit:
for(class_id in classes) {
callback = function() { this.selectClass(class_id) };
li_item = jQuery('<li></li>')
.click(callback);
}
Actually, more is going on in this iteration, but I didn't think it was very relevant to the question. In any case, what's happening is that the callback function seems to be referenced rather than stored (& copied). End result? When a user clicks any of the list items, it will always execute the action for the last class_id in the classes array, as it uses the function stored in callback at that specific point.
I found dirty workarounds (such as parsing the href attribute in an enclosed a element), but I was wondering whether there is a way to achieve my goals in a 'clean' way. If my approach is horrifying, please say so, as long as you tell me why :-) Thanks!

This is a classic "you need a closure" problem. Here's how it usually plays out.
Iterate over some values
Define/assign a function in that iteration that uses iterated variables
You learn that every function uses only values from the last iteration.
WTF?
Again, when you see this pattern, it should immediately make you think "closure"
Extending your example, here's how you'd put in a closure
for ( class_id in classes )
{
callback = function( cid )
{
return function()
{
$(this).selectClass( cid );
}
}( class_id );
li_item = jQuery('<li></li>').click(callback);
}
However, in this specific instance of jQuery, you shouldn't need a closure - but I have to ask about the nature of your variable classes - is that an object? Because you iterate over with a for-in loop, which suggest object. And for me it begs the question, why aren't you storing this in an array? Because if you were, your code could just be this.
jQuery('<li></li>').click(function()
{
$(this).addClass( classes.join( ' ' ) );
});

Your code:
for(class_id in classes) {
callback = function() { this.selectClass(class_id) };
li_item = jQuery('<li></li>')
.click(callback);
}
This is mostly ok, just one problem. The variable callback is global; so every time you loop, you are overwriting it. Put the var keyword in front of it to scope it locally and you should be fine.
EDIT for comments: It might not be global as you say, but it's outside the scope of the for-loop. So the variable is the same reference each time round the loop. Putting var in the loop scopes it to the loop, making a new reference each time.

This is a better cleaner way of doing what you want.
Add the class_id info onto the element using .data().
Then use .live() to add a click handler to all the new elements, this avoids having x * click functions.
for(class_id in classes) {
li_item = jQuery('<li></li>').data('class_id', class_id).addClass('someClass');
}
//setup click handler on new li's
$('li.someClass').live('click', myFunction )
function myFunction(){
//get class_id
var classId = $(this).data('class_id');
//do something
}

My javascript fu is pretty weak but as I understand it closures reference local variables on the stack (and that stack frame is passed around with the function, again, very sketchy). Your example indeed doesn't work because each function keeps a reference to the same variable. Try instead creating a different function that creates the closure i.e.:
function createClosure(class_id) {
callback = function() { this.selectClass(class_id) };
return callback;
}
and then:
for(class_id in classes) {
callback = createClosure(class_id);
li_item = jQuery('<li></li>').click(callback);
}
It's a bit of a kludge of course, there's probably better ways.

why can't you generate them all and then call something like
$(".li_class").click(function(){ this.whatever() };
EDIT:
If you need to add more classes, just create a string in your loop with all the class names and use that as your selector.
$(".li_class1, .li_class2, etc").click(function(){ this.whatever() };

Or you can attach the class_id to the .data() of those list items.
$("<li />").data("class_id", class_id).click(function(){
alert("This item has class_id "+$(this).data("class_id"));
});
Be careful, though: You're creating the callback function anew for every $("<li />") call. I'm not sure about JavaScript implementation details, but this might be memory expensive.
Instead, you could do
function listItemCallback(){
alert("This item has class_id "+$(this).data("class_id"));
}
$("<li />").data("class_id", class_id).click(listItemCallback);

Related

JQuery: What is a proper way to keep the scope of an object when different functions are called?

I have a general question as the title states. I am just not sure if what I am doing is the proper way or perhaps there is an easier way to keep the scope of an object.
I have here an example just to illustrate what I am working with. You can see I am passing the "this" object to the function via a parameter however is this the only way to do it? Is there a way I don't need to pass the parameter.
Please note that the criteria is there is no identifying class/ids on the forms.
http://jsfiddle.net/TmaHs/
Thanks.
As per your example, there are easier ways :
$("form select").on('change', changeText);
function changeText(e) { //the event is still available
$("input", e.target.form).val("Changed!"); //so is the target and the form
}
will do the same. FIDDLE
As for more complex functions, passing an element to the function is usually not a problem, and in my opinion it's better than using globals.
You can use the standard bind function to explicitly set the context
https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
Or the similar equivalent in jquery if it has one (I'm not sure about this)
That's a valid way of doing it - an alternative is to declare the variable in a higher scope, assign it in the first function, and reference it without the need to use arguments in the second function.
See this edit to your JSFiddle for example.
You could work with the window object, by declaring the variable out of score, then setting the window-object of it.
$("form select").change(function() {
window.thisObj = $(this).parent("form");
changeText();
});
function changeText() {
window.thisObj.find("input").val("Changed!");
}
​
But the way you did it is the proper one, working with a window-object should always be avoided.
I don't see anything wrong in that approach... Its perfectly fine :)
BUT, if you still want to know the other way, then there are many...
I will post couple of them over here...
1) Retrieve all the values that you need in your selector function, and pass them to the function.
$("form select").change(function() {
var val1 = $("selector1").val();
var val1 = $("selector2").val();
var val1 = $("selector3").val();
changeText(val1,val2,val3);
});
function changeText(val1,val2,val3) {
// your operation
}
2) Add the required values to an array, and pass that array to your target function.
var targetArray = new Array();
$("form select").change(function() {
targetArray[0]=//your val1
targetArray[1]=//your val2
targetArray[2]=//your val3
changeText(targetArray);
});
function changeText(arr) {
// your operation
}
​

Is calling $find a relatively expensive operation?

I am just wondering what the pros/cons would be between these two styles of coding:
var foo;
$(document).ready( function() {
foo = $find(fooID);
});
function OnBarClicked()
{
foo.doStuff();
foo.doMoreStuff();
}
compared to
function OnBarClicked()
{
$find(fooID).doStuff();
$find(fooID).doMoreStuff();
}
I feel like there's probably a gotcha in the former, but I'm not aware of why there would be a gotcha. In addition, if there's any seek time for $find().. is it more efficient to find all variables guaranteed to be 'found' 2+ times at the start, and then use the one instance?
EDIT: $find description and usage
var radListBox1ID = "<%= RadListBox1.ClientID %>";
var radListBox = $find(radListBox1ID);
alert(radListBox.get_id()); //Alerts RadListBox1.ClientID
Assuming you're asking if querying the DOM once vs multiple (one per statement) with $()...
Yes, it is more efficient to use a single $() and store the result in a variable, rather than invoke jQuery at every statement if you are using the element multiple times within the same scope...
This is particularly useful in event handlers where you are referencing this as a jQuery object multiple times.
$('#element').click(function(e) {
var $clicked = $(this);
$clicked.doStuff();
$clicked.doMoreStuff();
});
First of all, you felt that there was a gotcha in the first example, and indeed, there is: You declare the foo variable in a local context that your onBarClicked function doesn't have access to. You'll get an undefined error if you try to use it. To fix that problem, declare foo in the global scope:
var foo;
$(document).ready(function () {
foo = $(fooID);
});
When it comes to your actual question, the short answer is: Yes. Running the search once and storing the result will be faster.
But if you're only searching for an element by its ID, then in all honesty it probably won't make much of a difference. The times when it really matters are when you have a complicated selector that involves attribute selectors or pseudo-elements or descendant combinators or stuff. You'd definitely want to save that result instead of running it again and again and again.
The first one doesn't work as your would expect since foo won't be defined in OnBarClicked. foo gets set in the callback function once the document is ready.
I recommend this aproach:
$( function () {
// get all the references (to the DOM elements) ASAP
var header = $( '#header' )[0],
nav = $( '#navigation' )[0],
toolbar = $( '#toolbar' )[0];
// etc.
// now whenever you need to work with a referenced DOM element
// just wrap it inside a jQuery object
$( nav ).click( function () { ... });
});

Clean way to pass parameters between JavaScript events?

I have values I would like to keep in memory when certain events fire, like when a user is dragging and I want to save the element index. The value is needed when another event might fire. I don't know if it will ever fire.
I am using global variables to keep these values around. It seems there are better ways to do this like putting these values in an object with a namespace. Benefits? Any other suggestions?
My alltime favorite is currying variables into event handlers. Curry is a function prototype and when called on a function it will return a version of the function with preset arguments:
Function.prototype.curry = function curry() {
var fn = this, args = Array.prototype.slice.call(arguments);
return function curryed() {
return fn.apply(this, args.concat(Array.prototype.slice.call(arguments)));
};
};
node.addEventListener('mousedown', handler.curry(var1,var2,etc));
You could bind the data to a parent element that is a relevant container for each value. So for example, say you have a #dragAndDropContainer, in which there are many drag and droppable items, then as soon as a drag event fires (i.e. a drag begins), you could (in jQuery), execute:
$('#dragAndDropContainer').data('lastDragged', $(this).attr('id'));
And then just query $('#dragAndDropContainer').data('lastDragged') every time you need to.
I like davin's solution, but here is an alternative if that doesn't suit you. You can create a closure around the event handler like so:
var data = { x: 1, y: 2 };
var handler = function() {
console.log(data);
}
if (node.addEventListener) {
node.addEventListener('mousedown', handler, false);
} else {
node.attachEvent('onmousedown', handler);
}
An alternative to using Function.bind (sometimes known as curry): You can control the scope of your shared variable. Here's some jQuery pseudo-code demonstrating it. Shared is accessible to those two handlers and nobody else.
$(function(){
var shared = {a:1, b:2};
$('#id-1').click(function() {
alert(shared.a);
});
$('#id-2').click(function() {
alert(shared.b);
});
});
If you're writing jQuery procedural code, the closure approach is much simpler. Since most everything I write is an object, I'd rather not get into too many levels of inner closures, so I prefer to setup a handler with binding/currying (like Martin's example) if a handler needs access to shared variables.

Event.observe in a loop and variables

To put things in context, I'm loading a list of items via Ajax, creating a div with main info for each one and want to display details on page when clicking on it. So I have that code in my onSuccess :
items = transport.responseText.evalJSON(); // my list of objects that contains all the details I'll need for that page
for (var itemID in items)
{
newDiv = ... // Creating my div with main infos
$('myDiv').appendChild(newDiv);
// More code to make everything look pretty and that works fine
Event.observe(newDiv, 'click', function() { loadItem(itemID); });
}
loadItem is my function that will display all the item details. And my problem is that itemID isn't replace by its value when creating the observe event, so it always returns the same ID for all items.
Any idea how I can fix that ? I checked bind on prototype doc, that seemed to be made for those cases, but probably didn't get it, since it wouldn't work for me.
For a minimal-impact fix, replace your Event.observe line with this:
Event.observe(newDiv, 'click', loadItem.curry(itemID));
Explanation:
In your original code, the event handler functions you're creating close over (have an enduring reference to) the itemID variable, and so will use the value of that variable at of when the event handler is called, not as of when you assign it to the event. That value will be the last value that itemID has in the loop — for all of the handler functions. More about closures here.
With the minimal-impact revised code, we use Prototype's curry function, which will create a function for you that, when called, will call the underlying function with the arguments you gave curry. (The name is from mathematics; Haskell Curry came up with the technique, though there are arguments he wasn't the first to do so.) We could do the same thing ourselves:
items = transport.responseText.evalJSON(); // my list of objects that contains all the details I'll need for that page
for (var itemID in items)
{
newDiv = ... // Creating my div with main infos
$('myDiv').appendChild(newDiv);
// More code to make everything look pretty and that works fine
Event.observe(newDiv, 'click', prepLoadItem(itemID));
}
function prepLoadItem(id) {
return function() {
loadItem(id);
};
}
...but because Prototype has a general-purpose function for it, we don't have to.
Off-topic: Is items an array? If not, ignore this off-topic comment. If so, don't use for..in to loop through it, or at least, not unless you take some precautions the code above doesn't to do it properly. Details here, but for..in is not for looping through the indexes of an array; it's for looping through the properties of an object. Array objects may well have properties other than array indexes (and in fact, if you're using Prototype, they do.)

How to pass a variable by value to an anonymous javascript function?

The Objective
I want to dynamically assign event handlers to some divs on pages throughout a site.
My Method
Im using jQuery to bind anonymous functions as handlers for selected div events.
The Problem
The code iterates an array of div names and associated urls. The div name is used to set the binding target i.e. attach this event handler to this div event.
While the event handlers are successfully bound to each of the div events, the actions triggered by those event handlers only ever target the last item in the array.
So the idea is that if the user mouses over a given div, it should run a slide-out animation for that div. But instead, mousing over div1 (rangeTabAll) triggers a slide-out animation for div4 (rangeTabThm). The same is true for divs 2, 3, etc. The order is unimportant. Change the array elements around and events will always target the last element in the array, div4.
My Code - (Uses jQuery)
var curTab, curDiv;
var inlineRangeNavUrls=[['rangeTabAll','range_all.html'],['rangeTabRem','range_remedial.html'],
['rangeTabGym','range_gym.html'],['rangeTabThm','range_thermal.html']];
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
curTab=(inlineRangeNavUrls[i][0]).toString();
curDiv='#' + curTab;
if ($(curDiv).length)
{
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
$(curDiv).bind("mouseout", function(){showHideRangeSlidingTabs(curTab, false);} );
}
}
My Theory
I'm either not seeing a blindingly obvious syntax error or its a pass by reference problem.
Initially i had the following statement to set the value of curTab:
curTab=inlineRangeNavUrls[i][0];
So when the problem occured i figured that as i changed (via for loop iteration) the reference to curTab, i was in fact changing the reference for all previous anonymous function event handlers to the new curTab value as well.... which is why event handlers always targeted the last div.
So what i really needed to do was pass the curTab value to the anonymous function event handlers not the curTab object reference.
So i thought:
curTab=(inlineRangeNavUrls[i][0]).toString();
would fix the problem, but it doesn't. Same deal. So clearly im missing some key, and probably very basic, knowledge regarding the problem. Thanks.
You need to create a new variable on each pass through the loop, so that it'll get captured in the closures you're creating for the event handlers.
However, merely moving the variable declaration into the loop won't accomplish this, because JavaScript doesn't introduce a new scope for arbitrary blocks.
One easy way to force the introduction of a new scope is to use another anonymous function:
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
curDiv='#' + inlineRangeNavUrls[i][1];
if ($(curDiv).length)
{
(function(curTab)
{
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
$(curDiv).bind("mouseout", function(){showHideRangeSlidingTabs(curTab, false);} );
})(inlineRangeNavUrls[i][0]); // pass as argument to anonymous function - this will introduce a new scope
}
}
As Jason suggests, you can actually clean this up quite a bit using jQuery's built-in hover() function:
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
(function(curTab) // introduce a new scope
{
$('#' + inlineRangeNavUrls[i][1])
.hover(
function(){showHideRangeSlidingTabs(curTab, true);},
function(){showHideRangeSlidingTabs(curTab, false);}
);
// establish per-loop variable by passsing as argument to anonymous function
})(inlineRangeNavUrls[i][0]);
}
what's going on here is that your anonmymous functions are forming a closure, and taking their outer scope with them. That means that when you reference curTab inside your anomymous function, when the event handler runs that function, it's going to look up the current value of curTab in your outer scope. That will be whatever you last assigned to curTab. (not what was assigned at the time you binded the function)
what you need to do is change this:
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
to this:
$(curDiv).bind("mouseover",
(function (mylocalvariable) {
return function(){
showHideRangeSlidingTabs(mylocalvariable, true);
}
})(curTab)
);
this will copy the value of curTab into the scope of the outer function, which the inner function will take with it. This copying happens at the same time that you're binding the inner function to the event handler, so "mylocalvariable" reflects the value of curTab at that time. Then next time around the loop, a new outer function, with a new scope will be created, and the next value of curTab copied into it.
shog9's answer accomplishes basically the same thing, but his code is a little more austere.
it's kinda complicated, but it makes sense if you think about it. Closures are weird.
edit: oops, forgot to return the inner function. Fixed.
I think you're making this more complicated than it needs to be. If all you're doing is assigning a sliding effect on mouseover/out then try the hover effect with jquery.
$("#mytab").hover(function(){
$(this).next("div").slideDown("fast");},
function(){
$(this).next("div").slideUp("fast");
});
If you posted your full HTML I could tell you exactly how to do it :)
You can put your variable's value into a non existing tag, and later you can read them from there. This snippet is part of a loop body:
s = introduction.introductions[page * 6 + i][0]; //The variables content
$('#intro_img_'+i).attr('tag' , s); //Store them in a tag named tag
$('#intro_img_'+i).click( function() {introduction.selectTemplate(this, $(this).attr('tag'));} ); //retrieve the stored data

Categories