I have this JavaScript code:
for (var idx in data) {
var row = $("<tr></tr>");
row.click(function() { alert(idx); });
table.append(row);
}
So I'm looking through an array, dynamically creating rows (the part where I create the cells is omitted as it's not important). Important is that I create a new function which encloses the idx variable.
However, idx is only a reference, so at the end of the loop, all rows have the same function and all alert the same value.
One way I solve this at the moment is by doing this:
function GetRowClickFunction(idx){
return function() { alert(idx); }
}
and in the calling code I call
row.click(GetRowClickFunction(idx));
This works, but is somewhat ugly. I wonder if there is a better way to just copy the current value of idx inside the loop?
While the problem itself is not jQuery specific (it's related to JavaScript closures/scope), I use jQuery and hence a jQuery-only solution is okay if it works.
You could put the function in your loop:
for (var idx in data) {
(function(idx) {
var row = $("<tr></tr>");
row.click(function() { alert(idx); });
table.append(row);
})(idx);
}
Now, the real advice I'd like to give you is to stop doing your event binding like that and to start using the jQuery "live" or "delegate" APIs. That way you can set up a single handler for all the rows in the table. Give each row the "idx" value as an "id" or a "class" element or something so that you can pull it out in the handler. (Or I guess you could stash it in the "data" expando.)
Check out jquery's data() method:
http://api.jquery.com/jQuery.data/
Description: Store arbitrary data associated with the specified element.
Related
I have a bit of HTML generated by PHP in the format of:
<div class=zoomButton>
<input type=hidden name=zoomURL value=*different per instance*>
</div>
I am trying to attach a listener (imageZoom(event, url)) to each of the class "zoomButton" elements, but call it with different arguments for each instance.
i.e.
var zoomButtonArray = document.getElementsByClassName('zoomButton');
for (i=0; i<zoomButtonArray.length; i++)
{
var zoomURL = zoomButtonArray[i].children[0].value;
zoomButtonArray[i].addEventListener("mousedown", function(){imageZoom(event,zoomURL);});
}
however it seems that zoomURL is always the value of the very last element. How can I change my code/approach so that the argument passed to the listener is the correct one, and not the last one in the "zoomButtonArray" array?
Thanks
You need to wrap the event listener in a closure:
function makeEventListenerForZoomURL(zoomURL) {
return function(event) {
imageZoom(event, zoomURL);
}
}
var zoomButtonArray = document.getElementsByClassName('zoomButton');
for (i=0; i<zoomButtonArray.length; i++)
{
zoomButtonArray[i].addEventListener(
"mousedown",
makeEventListenerForZoomURL(zoomButtonArray[i].children[0].value)
);
}
This can also be simplified using the ECMAScript5 forEach:
var zoomButtonArray = document.getElementsByClassName('zoomButton');
zoomButtonArray = Array.prototype.slice.call(zoomButtonArray, 0);
zoomButtonArray.forEach(function(node) {
node.addEventListener("mousedown", function(event) {
imageZoom(event node.children[0].value);
});
});
The reason is that each time the for loop executes a new function is created, this new scope references the variable i but i changes each time the loop iterates. So by the time the event listener runs it looks at the value of i only to find that it is the last value when the for loop ended. By using a closure described above the scope created is unique to each iteration of the loop so that when the event listener finally executes the value of the wrapped variable (zoomURL or node in the examples above) will not have changed.
Here is a good article explaining closures in for loops: http://trephine.org/t/index.php?title=JavaScript_loop_closures
I think you are missing quotes around attributes. I just added quotes and the tested at jsFiddle (Fiddle link in comments) and it's working see to console in developer tool. it is iterating through each element as desired. Console screen shot
I am having a problem with JQuery at the moment where I am trying to parse a integer variable in a for loop in a eq() function. The problem I am having is when I try append the class ".episode-l" (needs to be a class as there are multiple of these and are displayed with a getjson) in a certain location with the eq() function but it wont display with a variable, I need it as a variable as i need to increment it each loop. There are other posts like this but I have looked at them and none of them work. Here is the code:
for( var i = 0; i < filmnamevar.length; i++)
{
$.getJSON('http://api.themoviedb.org/3/search/movie?query='+ filmnamevar[i] +'&api_key=81c50c197b83129dd4fc387ca6c8c323',function(dataa){
$('.episode-l').eq(i).append('<div class="rating">'+ dataa.results['0'].vote_average +'</div>');
console.log(i);
});
}
It is a common problem associated with the usage of a closure in a loop.
Assuming filimnamevar is a array, since you are using jQuery you can use $.each()(instead of using an IIFE function as given in the below links but the principle behind the problem is the same)
$.each(filmnamevar, function (i, val) {
$.getJSON('http://api.themoviedb.org/3/search/movie?query=' + val + '&api_key=81c50c197b83129dd4fc387ca6c8c323', function (dataa) {
$('.episode-l').eq(i).append('<div class="rating">' + dataa.results['0'].vote_average + '</div>');
console.log(i);
});
})
Read
Creating closures in loops: A common mistake
Javascript closure inside loops - simple practical example
I have a map of messages
say:
var Mapping = {
"notnow": 2,
"expensive": 3,
"not_worth_it": 4
}
i have a bunch of html elements (lets say divs with the same name)
so
<div id="notnow"></div>
,etc
now i want to attach a click handler to each of them,
i run a loop as shown below
function setThemUp(){
for(var item in Mapping)
{
$("#" + item).bind('click', function () {
Apply(Mapping[item]); });
}
}
But for some reason all of them seem to get bound to "not_worth_it":4. Not to their respective values.
I'm using Jquery 1.5.
Can someone please explain why this might be happening?
My guess is that instead of the Mapping[item] being resolved to their values, it's being passed as a reference or something, that's why since the value of item eventually points to "not worth it" all of them call the function with that value itself. Any way in which i could overcome them.
Hard coding each of them as
$("#notnow").bind('click', function () {
Apply(Mapping["notnow"]); });
$("#expensive").bind('click', function () {
Apply(Mapping["expensive"]); });
$("#not_worth_it").bind('click', function () {
Apply(Mapping["not_worth_it"]); });
does work, but i would prefer an elegant solution with a loop.
Answer
i went with the closure solution
function setThemUp(){
for(var item in Mapping)
{
$("#" + item).bind('click', (function () {
return function(temp) {
Apply(Mapping[temp]); };
})(item));
}
}
Reasons being , this was more of a why the loop didn't work rather than about optimization of the jquery , since this was afterall a representative example, not my actual code, and this was an elegant solution to that problem.
This is a classic case of scope issues: you're referencing the variable item in each of your bound handlers. The variable item changes, though -> it's being assigned all properties of the Mapping object literal, the last one being not_worth_it.
Creating a closure might help, to preserve the state of item for each callback:
for(var item in Mapping)
{
$("#" + item).bind('click', (function(currentItem)
{//IIFE, pass item as argument---------/
return function ()
{//return function, \/ access to closure scope
Apply(Mapping[currentItem]);
};
}(item)););
}
But this seems to be somewhat overkill, why not simply delegate the event, and use Mapping[$(this).attr('id')]?
I would suggest moving to this form:
Add the class mapped to your mapping divs.
HTML
<div id="notnow" class="mapped"></div>
JS
function setThemUp(){
$('.mapped').bind('click', function () {
Apply(Mapping[this.id]);
});
}
The problem is you need to write:
for (var item in Mapping)
rather than foreach.
Just a quick question, can anyone tell me why this doesnt work and how to fix it? Essentially its taking a group of table rows in HTML and dynamically attaching click events to them.
for (var a=index; a<rows.length; a++) {
tr = rows[a];
tr.onclick = function() { DropDownManager.onItemClick(tr, a); };
tr.observe("click", function() { DropDownManager.onItemClick(tr, a); });
}
The problem with this code is that the values passed into DropDownManager.onItemClick are always the last items in the loop, this isn't what im after as i wanted them to be the current value in that stage of the loop. I realize I'm missing something quite simple but cant for the life of me work it out!
JavaScript has no block scope, so e.g. loops don't create a new scope. You can create one by using a function:
for (var a=index; a<rows.length; a++) {
(function(a) {
tr = rows[a];
tr.onclick = function() { DropDownManager.onItemClick(this, a); };
tr.observe("click", function() { DropDownManager.onItemClick(this, a); });
}(a));
}
Depending on what rows and tr are, you might not even need the loop index. Maybe you can get the elements index inside the event handler through another way. E.g. if tr is a HTMLTableRowElement [MDN], then you can get its position among the other rows via this.rowIndex.
Btw. why are you binding the same event handler twice?
Aside from storing the attributes on the DOM object in the loop, you can also use function closures to "freeze" a copy of the variables in the loop for a particular function call. You can do it like this:
for (var a=index; a<rows.length; a++) {
tr = rows[a];
tr.onclick = function(tr, a) {
return(function() {
DropDownManager.onItemClick(tr, a);
});
}(tr,a);
}
What this does is says to assign tr.onclick the results of executing an anonymous function call that takes two variables as parameters (named tr and a) and is passed the current values of tr and a as parameters (this passing of the current values "freezes" the current values of those variables inside this function closure.
The result of executing that function call is itself another anonymous function. Because this internal anonymous function is now assigned to tr.onclick, it creates a function closure that keeps alive all the state that is currently in that closure. That state includes the "frozen" values of tr and a, but they are kept internal to just this function closure so every time through the loop creates a new function closure with a new "frozen" and "stored" value of tr and a.
Partly because I'm pedantic and partly because it's possible, here is the same thing done simply.
Event.on('table_id', 'click', 'tr', function(event, tr) {
DropDownManager.onItemClick(tr, rows.indexOf(tr));
});
Notice the containing table is needed for events to bubble up to, it is identified by it's ID but you could pass the element directly as well.
for (var a=index; a<rows.length; a++) {
tr = rows[a];
tr.setAttribute('a', a);
tr.onclick = function() { DropDownManager.onItemClick(this, this.getAttribute('a')); };
tr.observe("click", function() { DropDownManager.onItemClick(this, this.getAttribute('a')); });
}
try this way.
It will pass tr because variable tr is set as a last row.
I am trying to add a function named rows to the jqGrid jQuery plugin, but I can't determine the syntax. Here are my non-working versions.
(function($) {
$.fn.jgrid.rows = function(data) {
// do something
};
});
(function($) {
$.fn.rows = function(data) {
// do something
};
});
$.jqgrid.fn.rows = function(data) {
// do something
};
$.fn.rows = function(data) {
// do something
};
What would be the proper syntax?
Thanks!
It seems the correct answer on your question depends a little from what should do the method rows which you want to implement. I try to guess a little and gives the implementation which correspond to my understanding of your question.
First of all jqGrid is jQuery plugin and if you write for example
$(myselector).jqGrid('setSelection',rowid);
it can be that $(myselector) selects more as one DOM element. For example
$('table').jqGrid('setSelection',rowid);
will try call jqGrid method 'setSelection' on all <table> elements on the page. So this element in the array of DOM elements (it should be <table> DOM elements) and not only one element.
Another general remark. There are jQuery methods which can be chained like
$("#list").jqGrid('setGridParam',{datatype:'json'}).trigger('reloadGrid');
In the case the 'setGridParam' do something and return this to support chaining. Other methods don't support chaining and return what the method need to return. For example getDataIDs returns the array of ids and one can't chain getDataIDs with another jQuery methods.
Now I return back to your question. I would better name the new method getRowsById. The method will return array with DOM elements which represent <tr> (table row). The method will have rowid as the parameter. Then one can extend jqGrid with the new method in the way:
$.jgrid.extend({
getRowsById: function (rowid){
var totalRows = [];
// enum all elements of the jQuery object
this.each(function(){
if (!this.grid) { return; }
// this is the DOM of the table
// we
var tr = this.rows.namedItem(rowid);
if (tr !== null) { // or if (tr !== null)
totalRows.push(tr);
}
});
return totalRows;
}
});
First of all I use in the example the method $.jgrid.extend defined here. It does mostly $.extend($.fn.jqGrid,methods);. Then, because the method which we implement can't be chained, we define totalRows variable which will be returned later as the result of the method. Now we have to enumerate all objects from the this (like elements of $(myselector) or $('table') in the examples above). We do this with respect of this.each(function(){/*do here*/}); construct. Then inside of the loop we do following
if (!this.grid) { return; }
With the statement we test whether the current DOM element has grid property. It is not a standard property of the table element, but jqGrid extend the DOM elements of the table with the property. With the test we could skip for example other table elements where the jqGrid are not applied (which are not a jqGrid). Then I use the fact that this must be DOM of the table element which has rows property (see here, and here) and I use its namedItem method. The native implemented method works better as $("#"+rowid), but do the same. After all we return the array totalRows. It will have no element if the row with the row id not in the grid and 1 if it is exist. If the current jQuery selector select more as one grid and we had an error and included in both grids rows with the same id the returned array will has length greater as 1. So we can use it so
var grid = $("#list");
var tr = grid.jqGrid('getRowById','1111');
alert(tr.length);
At the end I want to mention that the method $.jgrid.extend can be helpful not only if you want to introduce new jqGrid method. Sometime there are already some jqGrid method, but it does not exactly what you need. So you want that the modified method do something at the beginning or something at the end of the original jqGrid method. In the case we can do the following
var oldEditCell = $.fn.jqGrid.editCell;
$.jgrid.extend({
editCell: function (iRow,iCol, ed){
var ret;
// do someting before
ret = oldEditCell.call (this, iRow, iCol, ed);
// do something after
return ret; // return original or modified results
}
});
In the example we overwrite the original editCell method with will be called by jqGrid itself and do something before of something after the call.
try:
$.extend($.jgrid,{
rows: function() {
// do something
}
});