I'm creating a jQuery plugin to do paging and encountered the following problem.
When I click on a page link created by the plugin below, it will always give we the value of the last index passed into the value i at the last iterator of the code below. If there are 4 pages, I will always get 4 if I press link 1, 2, 3 or 4. It seems that the reference to the delegate onclick also keeps a reference to the value of i instead of just the value.
Any Ideas? It's the options.onclick(i) that's acting strange.
$.fn.pager = function(options) {
var defaults = {
resultSet: undefined,
onclick: function(page) { alert(page); return false; },
};
return this.each(function () {
var rnd = Math.floor(Math.random()*9999)
var result = '';
for(var i = 1; i <= options.resultSet.PageCount; i++)
{
if(i == options.resultSet.PageCount)
result += '' + i + '';
else
result += '' + i + '' + options.separator;
}
$(this).html(result);
for(var i = 1; i <= options.resultSet.PageCount; i++)
{
$('#' + rnd + '_pagerPage_' + i).click(function() { options.onclick(i) });
}
});
}
I reduced the above code to just the problem case. So some checks re missing ;)
It seems that the reference to the delegate onclick also keeps a reference to the value of i instead of just the value.
What you are experiencing is your first (unexpected) encounter with closures. It's not even a reference that is being passed, it's weirder than that. To understand what's going on (and it's critical that you do if you program in javascript, this is considered basic stuff these days) read my answers to the following related questions:
Please explain the use of JavaScript closures in loops
Hidden Features of JavaScript?
This is a classic problem: the value of i that gets used inside the option click event handler function is whatever value i has at the point at which the event fires (which will be 4, the final value it has in the loop), not the value it had at the point at which you assigned the event handler. The way round it is to create an extra function each time through the loop that has its own variable containing a copy of the value of i at the point it was called:
var createClickHandler = function(n) {
return function() { options.onclick(n); };
};
$('#' + rnd + '_pagerPage_' + i).click( createClickHandler(i) );
Related
So I came upon this code during a review:
var permissions = $("#" + me.map.permissionsGridHtmlId).data("kendoGrid").dataSource.data();
var data = form.serializeArray();
for (var i = 0; i < permissions.length; i++) {
var record = permissions[i].toJSON();
$.each(record, function (key, value) {
data.push({
// ReSharper disable once ClosureOnModifiedVariable
name: "Permissions[" + i + "]." + key,
value: value
});
});
}
and that '// ReSharper disable' comment gave me pause.
I tried to look into it, and found this - https://www.jetbrains.com/help/resharper/AccessToForEachVariableInClosure.html
However, I tried to remove the comment and then do as that link said, create a variable inside of the scope to store the value, but the warning didn't go away.
Furthermore, despite the warning, it seems to behave as designed - the value of 'i' changes properly, and at the end the 'data' variable stores the proper/expected values.
So, my question is... why is ReSharper warning about this? Is there an actual problem in the code, or a bug in ReSharper? If the former, how should I fix the code? If the latter, is this warning ever right (and so we should leave the disable comment) or should I change the inspection severity to never show this warning?
Update
The following change to the code made the warning go away:
$.each(permissions, function (i, permission) {
$.each(permission.toJSON(), function (key, value) {
data.push({
name: "Permissions[" + i + "]." + key,
value: value
});
});
});
I'd still like to know why the warning exists, though, when the two code snippets appear to act identically.
Access to modified closure is only a problem if a lambda (function parameter in your case) would be executed after variable modification. In your case, $.each should execute lambda instantly, so it shouldn't be a problem. But ReSharper doesn't know if a called function would execute a passed lambda instantly or would store it for later execution, especially in JavaScript with its dynamic typing. So it just gives the warning always.
Please note that the article you found refers to C#, and the fix it suggests is only valid for C#. You cannot fix a problem in JS by using var i1 = i, because variable i1 declared by var would have a function scope, and you need it to have a block scope. So, if you can use ECMAScript 2015, then you can use let or const to declare a variable with a block scope, for example:
var permissions = $("#" + me.map.permissionsGridHtmlId).data("kendoGrid").dataSource.data();
var data = form.serializeArray();
for (var i = 0; i < permissions.length; i++) {
var record = permissions[i].toJSON();
let i1 = i;
$.each(record, function (key, value) {
data.push({
name: "Permissions[" + i1 + "]." + key,
value: value
});
});
}
I would like to pass an argument to another page but it turns out that it's undefined.
getAccount() returns a list of json. Firstly, I display those json objects one by one on HTML and when the user clicks each, the accDetail[i].accNo is set as local storage and will be passed to next page.
var accDetail=getAccounts();
for(var i =0;i<accDetail.length;i++) {
document.getElementById("demo").innerHTML+=''+accDetail[i].accNo +''+' '+ accDetail[i].accType+' '+ accDetail[i].balance+'<br>';
}
This is the function to set the item as local storage.
function getAcc(item)
{
localStorage.setItem("accNo",item); }
It does not display the value I want, is the way I concatenate it wrong?
Your onclick handler is going to be literally getAcc(accDetail[i].accNo) (try viewing in Inspect Element), which won't work because accDetail is not defined in the event handler. You need to change your Javascript so that it writes getAcc(0), getAcc(1), etc where 0, 1 are the different accNo.
Here is a small example I wrote, hopefully you can extend it to solve your problem:
var accNo = [1, 2, 3, 4];
for(var i = 0;i < accNo.length; i++) {
var line = '' + accNo[i] + ' <br>';
console.log(line);
document.getElementById("demo").innerHTML += line;
}
First Make sure your function getting correct value
Try Using window object instead
function getAcc(item)
{
localStorage.setItem("accNo",item);
}
Replace with
function getAcc(item)
{
console.log(item); //Please check you get item correct not undefined
window.accNo = item;
}
And whereever you want just use window.accNo
This question already has answers here:
Calling functions with setTimeout()
(6 answers)
Closed 7 years ago.
First of all, I have been looking around previous answers and altering my code however none of the solutions I have previously read have worked for me. Basically I have all my questions and answers saved in a jStorage and I am trying to create a review page, where the question and the answer are posted, as well as a new button that links the user back to the page where the answered the question.
I use two functions, the createReviewPage() which creates the page for the user, showing the question and the answer. Also I have the reviewPageButton(int).
Basically I need to attach the reviewPageButton() function to newly created buttons and I have been trying to use on.('click') to find the button to the function when it is created, however the event is firing before I even click a button.
My first function (Note some of my previous attempts have been commented out):
function createReviewPage() {
var questions = [];
for (var index = 1; index < 5; index++) {
questions.push($.jStorage.get("question" + index));
}
for (var index = 0; index < questions.length; index++) {
console.log(questions[index].questionText);
$('#resultDisplay').append("<h2> You answered " + questions[index].chosenAnswer + " to the question " + questions[index].questionText + "</h2>");
$('#resultDisplay').append("<input type = 'button' id = 'qbutton" + (index + 1) + "'/>");
// $('.qbutton' + (index + 1)).on("click", '#qbutton' + (index+1), invokeButton(index));
// $('body').on('click', '.qbutton'+(index+1),reviewPageButton(index));
$('.qbutton'+(index+1)).on('click',reviewPageButton(index));
$('#resultDisplay').append("<br> <br> <br>");
}
}
And my reviewPageButton()
function reviewPageButton(number) {
var currentquestion = $.jStorage.get("question" + (number + 1));
currentquestion.reviewed = "Yes";
$.jStorage.set("question" + (number + 1), currentquestion);
window.open("../src/questionpage" + (number + 1) + ".php", "_self");
}
reviewPageButton(index) stands for function execution, try reviewPageButton.bind(null, index) instead.
$('.qbutton'+(index+1)).on('click', function(){
reviewPageButton(index));
};
Don't call function there, just set handler. Maybe like this?
Could you use jQuery's .bind() method?
try replacing
$('.qbutton'+(index+1)).on('click',reviewPageButton(index));
with
$('.qbutton'+(index+1)).bind('click',reviewPageButton(index));
Missing ) and added a comma(,) after click and you are setting qbutton as ID but you are using it as class
$('#qbutton' + (index + 1)).on('click', reviewPageButton(index));
I need to attach the reviewPageButton() function to newly created buttons
If you are creating dynamic buttons, you shouldn't bind events to the buttons, instead bind them to the parent and target the button. For instance:
$('body').on('click', '.custom-button-class', reviewPageButton.bind(null, index) );
When you create a button and add it to the page, create it with your custom class and when the click event is triggered somewhere in the body, it'll detect if it was an element with 'custom-button-class' and call the function. Note: you should avoid binding the even to the body, to keep performance in mind. Instead, you should bind it to your closest comment ancestor that makes sense (e.g., a div.container).
Doing this registers only one event with the browser, which saves time (improves performance) for the browser detection cycle, but also when creating dynamic elements, since each element doesn't have it's own instance of the same function.
At the very least, you need to fix your code so it is free of syntax errors and does not invoke the function, when you only need to supply a reference to the function:
function createReviewPage() {
var questions = [];
for (var index = 1; index < 5; index++) {
questions.push( $.jStorage.get("question" + index) );
}
for (var index = 0; index < questions.length; index++) {
console.log(questions[index].questionText);
$('#resultDisplay').append(
"<h2> You answered " +
questions[index].chosenAnswer +
" to the question " +
questions[index].questionText +
"</h2>"
);
$('#resultDisplay').append(
"<input type = 'button' id = 'qbutton" +
(index + 1) +
"'/>"
);
// --- Problematic Line Changes ---
// missing first ending parentheses
// extra comma
// function call, instead of pointer
$('.qbutton' + (index + 1)).click(reviewPageButton.bind(null,index));
$('#resultDisplay').append("<br> <br> <br>");
}
}
I am generating an HTML table dynamically using JavaScript. One of the table's columns contains an onmouseover event, and when the mouse cursor is over a particular cell, I wish to display cell specific information.
In my case, I wish to display the value of nPos via an alert() call as it was during the table construction.
Simplified example of my table creation:
for (var iLoop = 0 ; iLoop < nHitsPerPage ; iLoop++) {
var nPos = (nCurrentPageParam * SearchResults.nMaximumHitsPage) + iLoop;
//...
var cellInfo = tableResultsRow.insertCell(6);
//...
cellInfo.className = "noWrapCell";
cellInfo.innerHTML = "?";
cellInfo.style.cursor = "pointer";
// The line below is important - I need to store nPos value during the table consturction
cellInfo.onmouseover = function(){alert("nPos = " + nPos)};
cellInfo.onmousemove = function(){FileResort.LightboxShortInfo.update(event)};
cellInfo.onmouseout = function(){FileResort.LightboxShortInfo.hide(event)};
}
As you can see from the example above, I iteratively created a table within my for() loop. I want to store the value of the nPos within every row (record) which is different for each row (record).
My problem is that once I mouseover that particular cell, I get the same nPos value for every row (record), and that nPos is the current value of nPos at the particular application state.
I cannot find a way to record the nPos value as it was during the for() execution, which is important for me in identifying what record is stored in the particular row.
Is there a way to capture, or store the value of nPos for every row (record) during the table's initial construction?
You are yet another victim of the closure monster :)
Look at this:
cellInfo.onmouseover = function(){alert("nPos = " + nPos)};
the function here - let's call it lambda - will have to find the value of the variable nPos. Since nPos is not declared inside lambda, it will look for it in the upper level function, that is the function where nPos is created.
When the mouseover event will fire, the code that declares and sets nPos will already have run to completion, so nPos will have the value nHitsPerPage.
That is exactly what lambda will display. Unfortunately, that's not what you want :).
To get over that, you need to create a (lexical) closure, i.e. provide lambda with a value of nPos that suits your needs.
The way of doing it in Javascript is as follows:
cellInfo.onmouseover = function(p){
return function () { alert("nPos = " + p)};
} (nPos);
let's call nu the new function (the one that takes p as a parameter).
We changed lambda so that it now refers to p instead of nPos.
It means that when the mouseover event will fire, lambda will look for p in its upper function, which is now nu. And it will indeed find p there, as the parameter of nu.
p is a function parameter, so it recieves a copy of nPos at the time it is called.
It means lambda will refer to a different instance of nu's calling context for each instance of cellInfo.
Each instance of the mouseover event handler will now hold a copy of p that is set to the desired value of nPos.
Quick Psuedo Code:
var nPosArray = [];
var itemsArray = [];
for (var iLoop = 0 ; iLoop < nHitsPerPage ; iLoop++)
{
var nPos = (nCurrentPageParam * SearchResults.nMaximumHitsPage) + iLoop;
//...
var cellInfo = tableResultsRow.insertCell(6);
//...
cellInfo.className = "noWrapCell";
cellInfo.innerHTML = "?";
cellInfo.style.cursor = "pointer";
// The line below is important - I need to store nPos value during the table consturction
nPosArray.push(nPos);
cellInfo.addEventListener('mouseout', cellMouseOut(event));
cellInfo.onmousemove = function(){FileResort.LightboxShortInfo.update(event)};
cellInfo.onmouseout = function(){FileResort.LightboxShortInfo.hide(event)};
itemsArray.push(cellInfo);
}
function cellMouseOut(e)
{
for(iLoop = 0 ; iLoop < cellInfo.length; iLoop++)
{
if(e.target.id == cellInfo[iLoop].id)
{
alert('NPos: ' + nPosArray[iLoop]);
}
}
}
This script is giving me problems. I've re written it several times but I'm missing something.
'questions' array refers to my objects. These objects are different questions with 'a' always being the correct answer.
The script works fine up to this point. The idea is to increment 'n' from 0 to scroll through my array, and provide the next question when a correct answer is clicked. 'x' is always the correct answer (it always holds 'a').
So I can increment 'n' just fine on correct guess; however, when I call the function 'a()' again, (after my alert 'hi' part), it is causing problems. I want to increment n, and call a() so I get the next question. Then I want to place the correct guess (x) in a random place (ie position 0, 1 or 2.)
Grateful for any help.
var questions = [q0,q1,q2,q3,q4,q5,q6,q7];
var n = 0;
function a(){
var y;
var z;
var x = Math.floor((Math.random() * 3))
if(x == 0){y = 1; z = 2}else if(x == 1){y = 0; z = 2}else{y = 0; z = 1}
$("#question_holder").text(questions[n].q);
$(".answer_holder").eq(x).text(questions[n].a);
$(".answer_holder").eq(y).text(questions[n].b);
$(".answer_holder").eq(z).text(questions[n].c);
$(document).ready(function(){
$(".answer_holder").eq(x).click(function(){
alert("hi");
n++;
a();
/*this area needs to get the next question by incrementing
n, then generate a different value to x, to place it
in a different position. which it does. however,
in subsequent questions, you can click the wrong answer as if
it's correct, ie not 'a' or 'x'.*/
});
});
};
Your logic is a bit strange here.. what you are trying to do is register a new click event every time a() runs. I think you want to register one click event for all answer_holder elements, and in the event handler check which element this is and handle it accordingly
Notice the following:
$(document).ready(function(){ - the function defined in this handler is supposed to run once your page is loaded.. I don't think you want to use it inside a(). It is usually only used in global scope (outside all functions)
$(".answer_holder").eq(x).click(function(){ - this event handler registers your function depending on the value of x. I suggest you register an event handler for all $(".answer_holder"), without depending on x (in the global scope). And inside that event handler (in the function), you check which element triggered the event (using $(this) - it returns the element that was clicked)
You have the $(document).ready() in the wrong place. Try something like this (caveat: this is completely untested):
function setupQuestion(n) {
var x,y,z;
x = Math.floor((Math.random() * 3))
if(x == 0){y = 1; z = 2}else if(x == 1){y = 0; z = 2}else{y = 0; z = 1}
$("#question_holder").text(questions[n].q);
$(".answer_holder").eq(x).text(questions[n].a).data('answer', 'a');
$(".answer_holder").eq(y).text(questions[n].b).data('answer', 'b');
$(".answer_holder").eq(z).text(questions[n].c).data('answer', 'c');
}
$(document).ready(function() {
var n = 0;
$('.answer_holder').click(function() {
if ($(this).data('answer') === 'a') { // Or whatever is the correct answer
n++;
if (n < questions.length) {
setupQuestion(n);
} else {
// Do something else, the test is finished
}
}
return false;
});
setupQuestion(n);
});
Note that I am not comparing on the text() function but rather the data() function. This is because the text as displayed to the user might be decorated in some way, making comparisons difficult. Simply using the answer index 'a' 'b' or 'c' is clearer.