JavaScript closure issue on dynamic element creation in a loop - javascript

During the execution of the page, a string gets composed containing the code that would need to be executed as the handler for the click event. The string could look like:
var handler = '"myFunction1(12, 20);myOtherFunction();"';
OR
var handler = '"myFunction1(12, 20);"'
When I create the buttons dynamically, and try to attach the events in a loop, it gets attached to the last button only. I can sense a closure issue but what am I missing?
Here is the code.
var buttons = [],
arg1 = 12,
arg2 = 20;
var butt1 = { Text: 'Bye', onClick: "anotherFunction();" },
butt = { Text: 'Hello', onClick: "myNewFunction();" },
butt2 = { Text: 'Bye333' };
buttons.push(butt1);
buttons.push(butt);
buttons.push(butt2);
function myNewFunction() {
alert('my New Function');
};
function myCloseFunction(arg1, arg2) {
alert('close: ' + arg1 + ' other: ' + arg2)
}
function anotherFunction() {
alert('Say Goodbye');
}
window.onload = function () {
var buttonContainer = document.getElementById('controlDiv'),
closeOnClick = "myCloseFunction(" + arg1 + ", " + arg2 + ")",
button;
for (var i = 0; i < buttons.length; i++) {
buttons[i].onClick = (buttons[i].onClick == null) ? closeOnClick : (closeOnClick + ';\n' + buttons[i].onClick);
button = document.createElement("INPUT");
button.type = 'button';
button.onclick = new Function(buttons[i].onClick);
if (i > 0)
buttonContainer.innerHTML += '&nbsp';
buttonContainer.appendChild(button);
}
};
The HTML page is simple:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
<script src="Script.js" type="text/javascript"></script>
</head>
<body>
<h1>hello</h1>
<div id="controlDiv"></div>
</body>
</html>
Sensing a closure issue I have tried various options to close over any variables but was not successful. e.g.
button.onclick = (function (action, i) {
var name1 = 'hello' + i,
a123 = new Function('action', 'return function ' + name1 + '(){action();}')(action);
return a123;
} (new Function(buttons[i].onClick), i));

buttonContainer.innerHTML += '&nbsp'; - This line (or rather re-setting the innerHTML) is the problem. I don't think that the innerHTML property contains any events. Works as expected here.

I'd do it like this:
var buttons = [],
container = document.getElementById('controlDiv');
buttons.push(new Button('Bye', 'anotherFunction()'));
function Button(text, clickEvent){
var obj = document.createElement('input'),
self = this;
this.text = text;
this.clickEvent = clickEvent;
obj.type = 'button';
container.appendChild(obj);
obj.addEventListener('click', handleClick);
function handleClick(e){
if(self.clickEvent){
closeOnClick();
window[self.clickEvent];
} else {
closeOnClick();
}
};
}

Related

How scope works in a for loop?

When I created 5 buttons in a loop and click the value of i is always 6
function createButtons() {
for (var i = 1; i <= 5; i++) {
var body = document.getElementsByTagName("BODY")[0];
var button = document.createElement("BUTTON");
button.innerHTML = 'Button ' + i;
(function(num) {
button.onclick = function() {
alert('This is button ' + num);
}
})(i);
body.appendChild(button);
}
}
but when I change the scope of i to block scope(using IIFE or let keyword) it gives the right value of i. How does it working under the hood of javascript?
I have seperated your functions into incorrect and correct one.
The incorrect version is what you're asking. The correct version is what you've already figured out but don't know why it work.
In the incorrect version, the value of i will always be updated to the most recent value because i belongs to the createButtons function and is shared with all onclick handler, and it is changing with the loop.
In the correct version, the value of i is given to the IIFE as num, and num belongs to the IIFE and not to createButtons.
Because of that, num is fixed because a new num is created for every loop thus is not shared with the other onclick handler.
Why? It is how closure works in JavaScript.
Read this for deeper understanding on JavaScript closure.
function createButtons_incorrect() {
for (var i = 1; i <= 5; i++) {
var body = document.getElementsByTagName("BODY")[0];
var button = document.createElement("BUTTON");
button.innerHTML = 'Bad ' + i;
button.onclick = function() {
alert('This is button ' + i);
}
body.appendChild(button);
}
}
function createButtons_correct() {
for (var i = 1; i <= 5; i++) {
var body = document.getElementsByTagName("BODY")[0];
var button = document.createElement("BUTTON");
button.innerHTML = 'Good ' + i;
(function(num){
button.onclick = function() {
alert('This is button ' + num);
}
})(i);
body.appendChild(button);
}
}
createButtons_incorrect();
document.body.appendChild(document.createElement('br'));
createButtons_correct();

Add onfocus listener instead of onload

I have such script in js file wich I'm calling from jsp, and I need to add listener instead of "onload".
What important for me:
1) It must be pure js without jQuery or anything
2) Input tags would be created dynamically(Maybe this is important)
3) It must be external js file(<script src="<c:url value="/js/focus.js" />"></script>), but not the tag <script>function.....</script> inside jsp page
onload = function () {
var allInput = document.getElementsByTagName("input");
for (i = 0; i < allInput.length; i++) {
allInput[i].onfocus = showHint
}
};
function showHint() {
var hint = document.getElementById("hint");
hint.innerHTML = this.name + " " + this.value;
hint.style.display = "block";
};
The page can get focus before is't loaded. And if the page is not loaded your inputs don't exist so window.onfocus can't set allInput[i].onfocus. When the page is refreshed with the focus on the devtools the page gets a chance to create inputs before window.onfocus call.
Put your window.onfocus inside window.onload so that it is always called after the page is loaded. If you don't want to override window.onload use addEventListener instead:
addEventListener('load', function () {
addEventListener('focus', function() {
var allInput = document.getElementsByTagName("input")
for (i = 0; i < allInput.length; i++) {
allInput[i].addEventListener('focus', showHint)
}
}
})
function showHint() {
var hint = document.getElementById("hint")
hint.innerHTML = this.name + " " + this.value
hint.style.display = "block"
}
replace onload with onfocus
onfocus = function () {
var allInput = document.getElementsByTagName("input");
for (i = 0; i < allInput.length; i++) {
allInput[i].onfocus = showHint
}
};
That works for me
window.addEventListener('load', function () {
var allInput = document.getElementsByTagName("input");
for (i = 0; i < allInput.length; i++) {
allInput[i].onfocus = showHint
}
});
function showHint() {
var hint = document.getElementById("hint");
hint.innerHTML = this.name + " " + this.value;
hint.style.display = "block";
};

onkeyup event on dynamic array

Good Evening,
I am having trouble setting up the onkeyup event. I am trying to get it to fire an objects method when a user enters text into the text field. It does not seem to find the object.
I have cut down on the code and have made the following sample:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
<script>
var ReportModule = new function () {
function ReportObj(id, title) {
this.id = id;
this.title = title;
this.result = "";
this.empno = "";
this.UpdateEmpno = function (empNo, resultBoxID) {
this.empno = empNo;
$(resultBoxID).update("Result: " + empNo);
};
};
var ReportObjArray = new Array();
var test1 = new ReportObj("box1", "First object");
var test2 = new ReportObj("box2", "Second object");
ReportObjArray.push(test1);
ReportObjArray.push(test2);
this.Initialize = function () {
for (i = 0; i < ReportObjArray.length; i++) {
var container = document.createElement("div");
container.id = ReportObjArray[i].id;
container.textContent = ReportObjArray[i].title;
$('#Container').append(container);
var empnoInput = document.createElement("input");
empnoInput.type = "text";
empnoInput.id = ReportObjArray[i].id + "_Empno";
empnoInput.onkeyup = function (event) {
// Update Report Objects empno field
ReportObjArray[i].UpdateEmpno(empnoInput.value,empnoInput.id); // <-------- Undefined here
};
$('#' + ReportObjArray[i].id).append(empnoInput);
var container2 = document.createElement("div");
container2.id = ReportObjArray[i].id + "_result";
container2.style.border = "1px solid black";
container2.style.width = "100px";
container2.textContent = "Result:";
$('#' + container.id).append(container2);
};
};
}
</script>
</head>
<body onload="ReportModule.Initialize()">
<div id="Container"></div>
</body>
</html>
Update: It works when searching for the object in the ReportObjArray and matching the correct object. However, I was wondering if there was a more efficient way instead of having to look through the array each time.
empnoInput.onkeyup = function (event) {
// Update Report Objects empno field
var target_id = document.getElementById(event.target.id).id;
for (j = 0; j < ReportObjArray.length; j++) {
if (target_id = ReportObjArray[j].id) {
ReportObjArray[j].UpdateEmpno(document.getElementById(event.target.id).value,empnoInput.id);
break;
}
}
};
Wrap your for loop code in a closure:
for (i = 0; i < ReportObjArray.length; i++) {
(function(i) {
// code
})(i);
}
Working JS Fiddle:
http://jsfiddle.net/23vkS/

How to change the value of the variable in this function without making a new function

function SlideShow(area) {
var SlideImg = new Array('img1', 'img2');
var SlideArea = document.getElementById(area);
for (i = 0; i < SlideImg.length; i++) {
if (SlideImg[i] == SlideImg[0]) {
var classname = 'active';
} else {
var classname = 'not-active';
}
var html = '<img src="images/room/' + SlideImg[i] + '.jpg" id="' + SlideImg[i] + '" class="' + classname + '" />';
SlideArea.innerHTML += html;
}
var a = 1;
function RunSlide() {
var before = a - 1;
if (a > SlideImg.length - 1) {
a = 0;
}
ImgBefore = document.getElementById(SlideImg[before]);
ImgBefore.className = 'not-active';
ImgNext = document.getElementById(SlideImg[a]);
ImgNext.className = 'active';
a++;
}
var run = setInterval(RunSlide, 5000);
}
How to change the a value? Because the a variable is not in the Global scope, can I access it from another function?
I want to access it, but don't want to declare the a variable as a global.
Yes, the variable is local to the function, so you have to expose a function outside to be able to access it.
You can for example put this last in the function to make it return a function that can set the variable:
function SlideShow(area) {
...
return function(value){ a = value; };
}
Usage:
var setA = SlideShow(something);
Then later you can use the function:
setA(42);
Simple, declare var a; outside of SlideShow and then it will be globally accessible.
Remember that it will still require resetting within the functin with a = 1;
If you can make the scope of the a variable global, then you should be able to. Instead of declaring in your function, just reference variable and declare in the "< head >" of your page instead. See my example and this I tested and clicking diff links changes value of variable and displays on page.
<html>
<head><title>Test</title>
<script language="javascript">
var a = -1;
function changeVar(value) {
a = value;
document.getElementById("test1").innerHTML = a;
}
</script>
</head>
<body>
<h1>Test page</h1>
Click me (2) <br />
Click me (5) <br />
Click me (9) <br />
<hr>
<h1>Value for a is: <div id="test1"></div></h1>
<!-- really test by including function later on page -->
<script language="javascript">
function SlideShow(area) {
var SlideImg = new Array('img1', 'img2');
var SlideArea = document.getElementById(area);
for (i = 0; i < SlideImg.length; i++) {
if (SlideImg[i] == SlideImg[0]) {
var classname = 'active';
} else {
var classname = 'not-active';
}
var html = '<img src="images/room/' + SlideImg[i] + '.jpg" id="' + SlideImg[i] + '" class="' + classname + '" />';
SlideArea.innerHTML += html;
}
a = 1; // removed declaration of a and just using a from prior declaration
function RunSlide() {
var before = a - 1;
if (a > SlideImg.length - 1) {
a = 0;
}
ImgBefore = document.getElementById(SlideImg[before]);
ImgBefore.className = 'not-active';
ImgNext = document.getElementById(SlideImg[a]);
ImgNext.className = 'active';
a++;
}
var run = setInterval(RunSlide, 5000);
}
</script>
</html>
You cannot access the variable itself if declared in the function because it's scope is LOCAL. See: http://www.w3schools.com/js/js_variables.asp
However, there is a way you could tweak your function to set a value of another element on the page, then others can reference that value. Using same code snippet for your script, when a is updated you can set the value of a hidden field on your page, and then others can reference that value. Any other way I think you have to declare it outside the function.
ALTERNATE 1:
<!-- really test by including function later on page -->
<script language="javascript">
function SlideShow(area) {
var SlideImg = new Array('img1', 'img2');
var SlideArea = document.getElementById(area);
for (i = 0; i < SlideImg.length; i++) {
if (SlideImg[i] == SlideImg[0]) {
var classname = 'active';
} else {
var classname = 'not-active';
}
var html = '<img src="images/room/' + SlideImg[i] + '.jpg" id="' + SlideImg[i] + '" class="' + classname + '" />';
SlideArea.innerHTML += html;
}
var a = 1;
function RunSlide() {
var before = a - 1;
if (a > SlideImg.length - 1) {
a = 0;
}
ImgBefore = document.getElementById(SlideImg[before]);
ImgBefore.className = 'not-active';
ImgNext = document.getElementById(SlideImg[a]);
ImgNext.className = 'active';
a++;
// set value of element on page so others can reference
document.getElementById("test1").innerHTML = a; // or set a hidden field
}
var run = setInterval(RunSlide, 5000);
}
</script>
The other way you might be able to do it is declare a return for your function with the value of a, and callers can ignore it if they want and others can retrieve it. This may not work with your setTimeOut re-calling the RunSlide function. I tried another inner function within SlideShow but that didn't seem to return a either. Original scoping suggestion remains.

Passing parameters in Javascript onClick event

I'm trying to pass a parameter in the onclick event. Below is a sample code:
<div id="div"></div>
<script language="javascript" type="text/javascript">
var div = document.getElementById('div');
for (var i = 0; i < 10; i++) {
var link = document.createElement('a');
link.setAttribute('href', '#');
link.innerHTML = i + '';
link.onclick= function() { onClickLink(i+'');};
div.appendChild(link);
div.appendChild(document.createElement('BR'));
}
function onClickLink(text) {
alert('Link ' + text + ' clicked');
return false;
}
</script>
However whenever I click on any of the links the alert always shows 'Link 10 clicked'!
Can anyone tell me what I'm doing wrong?
Thanks
This happens because the i propagates up the scope once the function is invoked. You can avoid this issue using a closure.
for (var i = 0; i < 10; i++) {
var link = document.createElement('a');
link.setAttribute('href', '#');
link.innerHTML = i + '';
link.onclick = (function() {
var currentI = i;
return function() {
onClickLink(currentI + '');
}
})();
div.appendChild(link);
div.appendChild(document.createElement('BR'));
}
Or if you want more concise syntax, I suggest you use Nick Craver's solution.
This is happening because they're all referencing the same i variable, which is changing every loop, and left as 10 at the end of the loop. You can resolve it using a closure like this:
link.onclick = function(j) { return function() { onClickLink(j+''); }; }(i);
You can give it a try here
Or, make this be the link you clicked in that handler, like this:
link.onclick = function(j) { return function() { onClickLink.call(this, j); }; }(i);
You can try that version here
link.onclick = function() { onClickLink(i+''); };
Is a closure and stores a reference to the variable i, not the value that i holds when the function is created. One solution would be to wrap the contents of the for loop in a function do this:
for (var i = 0; i < 10; i++) (function(i) {
var link = document.createElement('a');
link.setAttribute('href', '#');
link.innerHTML = i + '';
link.onclick= function() { onClickLink(i+'');};
div.appendChild(link);
div.appendChild(document.createElement('BR'));
}(i));
Try this:
<div id="div"></div>
<script language="javascript" type="text/javascript">
var div = document.getElementById('div');
for (var i = 0; i < 10; i++) {
var f = function() {
var link = document.createElement('a');
var j = i; // this j is scoped to our anonymous function
// while i is scoped outside the anonymous function,
// getting incremented by the for loop
link.setAttribute('href', '#');
link.innerHTML = j + '';
link.onclick= function() { onClickLink(j+'');};
div.appendChild(link);
div.appendChild(document.createElement('br')); // lower case BR, please!
}(); // call the function immediately
}
function onClickLink(text) {
alert('Link ' + text + ' clicked');
return false;
}
</script>
or you could use this line:
link.setAttribute('onClick', 'onClickLink('+i+')');
instead of this one:
link.onclick= function() { onClickLink(i+'');};
Another simple way ( might not be the best practice) but works like charm. Build the HTML tag of your element(hyperLink or Button) dynamically with javascript, and can pass multiple parameters as well.
// variable to hold the HTML Tags
var ProductButtonsHTML ="";
//Run your loop
for (var i = 0; i < ProductsJson.length; i++){
// Build the <input> Tag with the required parameters for Onclick call. Use double quotes.
ProductButtonsHTML += " <input type='button' value='" + ProductsJson[i].DisplayName + "'
onclick = \"BuildCartById('" + ProductsJson[i].SKU+ "'," + ProductsJson[i].Id + ")\"></input> ";
}
// Add the Tags to the Div's innerHTML.
document.getElementById("divProductsMenuStrip").innerHTML = ProductButtonsHTML;
It is probably better to create a dedicated function to create the link so you can avoid creating two anonymous functions. Thus:
<div id="div"></div>
<script>
function getLink(id)
{
var link = document.createElement('a');
link.setAttribute('href', '#');
link.innerHTML = id;
link.onclick = function()
{
onClickLink(id);
};
link.style.display = 'block';
return link;
}
var div = document.getElementById('div');
for (var i = 0; i < 10; i += 1)
{
div.appendChild(getLink(i.toString()));
}
</script>
Although in both cases you end up with two functions, I just think it is better to wrap it in a function that is semantically easier to comprehend.
onclick vs addEventListener. A matter of preference perhaps (where IE>9).
// Using closures
function onClickLink(e, index) {
alert(index);
return false;
}
var div = document.getElementById('div');
for (var i = 0; i < 10; i++) {
var link = document.createElement('a');
link.setAttribute('href', '#');
link.innerHTML = i + '';
link.addEventListener('click', (function(e) {
var index = i;
return function(e) {
return onClickLink(e, index);
}
})(), false);
div.appendChild(link);
div.appendChild(document.createElement('BR'));
}
How abut just using a plain data-* attribute, not as cool as a closure, but..
function onClickLink(e) {
alert(e.target.getAttribute('data-index'));
return false;
}
var div = document.getElementById('div');
for (var i = 0; i < 10; i++) {
var link = document.createElement('a');
link.setAttribute('href', '#');
link.setAttribute('data-index', i);
link.innerHTML = i + ' Hello';
link.addEventListener('click', onClickLink, false);
div.appendChild(link);
div.appendChild(document.createElement('BR'));
}
This will work from JS without coupling to HTML:
document.getElementById("click-button").onclick = onClickFunction;
function onClickFunction()
{
return functionWithArguments('You clicked the button!');
}
function functionWithArguments(text) {
document.getElementById("some-div").innerText = text;
}

Categories

Resources