I am looping through some elements and then adding new elements which should manipulate these elements when clicked. It's tough to explain, so please have a look at this Fiddle to make it much clearer:
http://jsfiddle.net/pgFcn/3/
The interesting part is this code (pseudocode for the sake of brevity):
for (var i = 0; i < divs.length; i++) {
var div = divs[i];
someOtherElement.addEventListener("click", function () {
testDiv(div); // always refers to the last div because variable is overwritten next loop
});
}
I expect the testDiv call to refer to div 1, div 2, div 3 respectively, but instead, they all refer to div 3 because the variable gets overwritten in the next loop iteration. How can I solve this?
That's a classical problem. Here's how it's usually solved :
for (var i = 0; i < divs.length; i++) {
(function(div){
someOtherElement.addEventListener("click", function () {
testDiv(div);
});
})(divs[i]);
}
To understand both the problem and the solution, you have to know that the scope of a not global variable, in JavaScript, is the call of the function where it is declared. This means that
in your code, there is only one div variable
in the solution, calling the intermediate functions make different div variables
This is another way of resolving the issue...
function testDiv(d) {
// this doesn't work as expected, it always shows "Div 3"
alert(d.innerText);
}
var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++) {
var div = divs[i];
var a = document.createElement("a");
a.index = i;
a.innerText = "[this should point to Div" + (i+1) + "]";
a.href = "#";
a.addEventListener("click", function (e) {
var e = e || window.event;
var tg = e.target || e.srcElement;
testDiv(divs[tg.index]);
});
document.body.appendChild(a);
}
Related
I am trying to create a table for a part of my assignment and I am having a hard time implementing event listener the right way. It seems like the data that I am trying to display <td>here</td>is fixed and I don't know how to fix this issue.
There's some great help on event listeners on table rows however I couldn't find an example that uses loops to assign the data.
Here is my code:
var functionCreate = function(intWidth, intHeight) {
var myRow;
var intCell;
$('#output').append('<table border = \"1\">');
for(var i = 0; i< intHeight;i++){
$('#output').find('table').append('<tr>');
for(var j = 0; j < intWidth; j ++){
intCell = 'click me';
$('#output').find('tr:last').append('<td>'+intCell)
$('#output').on('click',"td", function(){
$(this).text((i+1).toString()+'.'+(j+1).toString());//(row.col)
})
}
}
return jQuery('output');
};
what happens is that the final row.col value is assigned to all <td></td> here. I don't know how to give each one a different value.
so if I pass functionCreate(3,5). All data in rows (after click) become 5.3.
I guess my question is how do I assign the click behavior to the relevant <td></td> only? Or is there any other way to pass the data?
Thanks ahead.
This issue is because of hoisting:
Minimal reproduction of your error
// Demonstration of how easy it is for this to mess up your loops.
var txt = ["a","b","c"];
for (var i = 0; i < 3; ++i ) {
var msg = txt[i];
setTimeout(function() { alert(msg); }, i*1000);
}
// Alerts 'c', 'c', 'c'
Solution
// Pattern to avoid that by binding to variable in another function.
var txt = ["a","b","c"];
for (var i = 0; i < 3; ++i ) {
setTimeout((function(msg) {
return function() { alert(msg); }
})(txt[i]), i*1000);
}
// Alerts 'a','b','c'
You can use data attribute to avoid hoisting issue.
var functionCreate = function(intWidth, intHeight) {
var myRow;
var intCell;
$('#output').append('<table border = \"1\">');
for(var i = 0; i< intHeight;i++){
$('#output').find('table').append('<tr>');
for(var j = 0; j < intWidth; j ++){
intCell = 'click me';
var cell = $('<td>'+intCell).data('row', i+1).data('col', j+1);
$('#output').find('tr:last').append(cell);
}
}
return jQuery('output');
};
$('#output').on('click',"td", function(){
var $this = $(this);
var row = $this.data('row');
var col = $this.data('col');
$this.text(row+'.'+col);//(row.col)
})
and, event registration can be moved to outside from loop.
I am practicing my javascript. I have created a link to show hide paragraphs. The code currently uses 2 'for' loops. Should I somehow be creating a function for the 'for' loop and then re-use the function?
var paragraphs = document.getElementsByTagName('p'),
firstParagraph = paragraphs[0],
link = document.createElement('a');
link.innerHTML = 'Show more';
link.setAttribute('class', 'link');
link.setAttribute('href', '#');
firstParagraph.appendChild(link);
for (var i = 1; i <= paragraphs.length - 1; i++) {
paragraphs[i].classList.add('hide')
}
function toggleHide(e) {
e.preventDefault;
var paragraphs = document.getElementsByTagName('p');
for (i = 1; i <= paragraphs.length - 1; i++) {
paragraphs[i].classList.toggle('hide');
}
}
link.addEventListener('click', toggleHide)
Since toggle('hide') will also do the same thing of add('hide') when initializing the paragraph list, it is good to pull up the duplicate code to a single function.
For example:
var paragraphs = document.getElementsByTagName('p'),
firstParagraph = paragraphs[0],
link = document.createElement('a');
link.innerHTML = 'Show more';
link.setAttribute('class' , 'link');
link.setAttribute('href' , '#');
firstParagraph.appendChild(link);
toggleHideAll();
function toggleHide( e ){
e.preventDefault;
var paragraphs = document.getElementsByTagName('p');
toggleHideAll();
}
function toggleHideAll(){
for( i = 1 ; i <= paragraphs.length-1 ; i++){
paragraphs[i].classList.toggle('hide');
}
}
link.addEventListener( 'click' , toggleHide)
Yes, a single loop to achieve both ends would be good, as #Solmon says:
function toggleHideAll(){
for (var i = 1; i <= paragraphs.length-1; i++) {
paragraphs[i].classList.toggle('hide');
}
}
There is a more idiomatic way to express this loop, however, and I would advise you to use it, because the original form is confusing to developers who are accustomed to the standard form:
function toggleHideAll() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].classList.toggle('hide');
}
}
That is, loop starting at zero, while the loop variable is less than length (not less than or equal to length minus one. And in this case, the loop does not do exactly the same as your original, because the original actually skips your first paragraph. If that's intentional, rather than tweaking the loop parameters, I would recommend toggling all of the paragraphs and then handling the special case with a line of code outside the loop:
function toggleHideAll() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].classList.toggle('hide');
}
paragraphs[0].classList.remove('hide');
}
Also, it's really nice when you can avoid explicit loops in your code altogether:
function toggleHideAll() {
paragraphs.forEach(p => p.classList.toggle('hide'));
paragraphs[0].classList.remove('hide');
}
I am trying to select elements on a class ,put them on an array. For each element in that class I want to select the "a" Tag and then I want to make a listener for each element, but this is a mess that seems impossible for me in JS. Heres the code I have so far.
var elemento = new Array();
var y=document.getElementsByClassName("thumbnails");
for (var i=0; i < y.length; i++) {
elemento = ( y[i].getElementsByTagName("a"));
elemento[0].addEventListener('click', function(){alert("jo")}, false);
}
This works for the first element but not for the rest, and yes, elemento is [0] because there's no more "a" tags inside each thumbnail.
One short way for modern browsers is to use CSS selectors in querySelector:
var elements = document.querySelectorAll('.thumbnails a');
for (var i = 0, len = elements.length; i < len; i++) {
elements[i].addEventListener('click', function() { ... }, false);
}
DEMO: http://jsfiddle.net/AApw2/
While .querySelectorAll is a good solution, it's important to understand how this would be handled without it.
What you simply need is an inner loop to loop over the current set of a elements held by the elemento variable.
var elemento;
var y = document.getElementsByClassName("thumbnails");
var handler = function(){alert("jo")};
for (var i=0; i < y.length; i++) {
elemento = y[i].getElementsByTagName("a");
for (var j = 0; j < elemento.length; j++) {
elemento[j].addEventListener('click', handler, false);
}
}
Notice that I use a different counter j for the inner loop since i is already in use.
Notice also that I moved the handler function to a variable outside the loop. This makes all the elements use the same function object, which is more efficient.
Side note:
You may also want to create some helper methods that will shorten the long method names, and convert them to Arrays. This allows you to use Array methods like .forEach().
var _slice = Function.call.bind([].slice);
function byClass(ctx, clss) {
if (typeof ctx === "string") {
clss = ctx;
ctx = document;
}
return _slice(ctx.getElementsByClassName(clss));
}
function byTag(ctx, tag) {
if (typeof ctx === "string") {
tag = ctx;
ctx = document;
}
return _slice(ctx.getElementsByTagName(tag));
}
That reduces your code to this:
var handler = function(){alert("jo")};
byClass("thumbnails").forEach(function(thumb) {
byTag(thumb, "a").forEach(function(a) {
a.addEventListener('click', handler, false);
});
});
The following code works fine but I want to make it better:
function prodNameTrim(selector){
var el = document.getElementsByClassName(selector);
var len = el.length;
for(i = 0; i<len; i++){
aObj = el[i].getElementsByTagName('a');
txtNode = aObj[0].childNodes[0].nodeValue;
if(txtNode.length > 26){
txtNode = txtNode.substring(0, 27) + ' ...';
}
aObj[0].childNodes[0].nodeValue = txtNode;
}
}
What I don't like about it is that I first establish txtNode before the condition like so:
txtNode = aObj[0].childNodes[0].nodeValue;
After I process the variable through my conditional to truncate the string with an ellipses, I do the following to replace the text in the DOM:
aObj[0].childNodes[0].nodeValue = txtNode;
I have to believe there is a better way to do this but I'm not sure what that is, I feel as if I'm breaking the DRY rule.
I would suggest this:
function prodNameTrim(selector){
var el = document.getElementsByClassName(selector),
len = el.length, node;
for(var i = 0; i < len; i++) {
node = el[i].getElementsByTagName('a')[0].firstChild;
if(node.nodeValue.length > 26){
node.nodeValue = node.nodeValue.substring(0, 27) + ' ...';
}
}
}
Changes:
Declare all local variables (so no implicit globals - you weren't declaring i, aObj or txtNode)
Skip the aObj intermediate as it isn't needed
Avoid the textNode intermediate as it isn't need
Assign directly to nodeValue
Make the assignment to nodeValue only inside the if statement since that's the only place it changes
Use .firstChild instead of .childNodes[0]
I wonder if this would work (probably requires IE9 or higher and would like to see the HTML to know for sure and run some browser tests), but the general idea is to use querySelectorAll() and combine the two searches into one more involved CSS selector:
function prodNameTrim(rootClass) {
var items = document.querySelectorAll("." + rootClass + " a:first-of-type"), node;
for (var i = 0; i < items.length; i++) {
node = items[i].firstChild;
if (node.nodeValue.length > 26) {
node.nodeValue = node.nodeValue.substring(0, 27) + ' ...';
}
}
}
Note, this second implementation assumes that the argument to prodNameTrim() is a class name (as it was in the OP's version).
Or, if there's only one link tag in each selector parent, then you could simply use this which should work in all modern browsers:
function prodNameTrim(rootClass) {
var items = document.querySelectorAll("." + rootClass + " a"), node;
for (var i = 0; i < items.length; i++) {
node = items[i].firstChild;
if (node.nodeValue.length > 26) {
node.nodeValue = node.nodeValue.substring(0, 27) + ' ...';
}
}
}
If you just want to avoid repeating the aObj[0].childNodes[0] part everywhere you can use your txtNode variable to refer to the node itself, rather than its value:
function prodNameTrim(selector){
var el = document.getElementsByClassName(selector);
var len = el.length;
var txtNode; // declare txtNode with var
for(var i = 0; i<len; i++){
txtNode = el[i].getElementsByTagName('a')[0].childNodes[0];
if(txtNode.nodeValue.length > 26){
txtNode.nodeValue = txtNode.nodeValue.substring(0, 27) + ' ...';
}
}
}
You'll notice that you still end up repeating txtNode.nodeValue everywhere, but it's better than having to include aObj[0].childNodes[0] every time. And the line that you had after the if statement to write the substring version back to the node is not needed, since that update now takes place directly inside the if.
Note also that you should declare all of your variables with var or they'll become globals. (And the way I've shown above doesn't actually need the aObj variable at all.)
If you'd like to do the conditional truncation in one line, you could do something like this:
function prodNameTrim(selector){
var el = document.getElementsByClassName(selector),
len = el.length, node;
for(var i = 0; i < len; i++) {
node = el[i].getElementsByTagName('a')[0].firstChild;
node.nodeValue.substring(0, 27) + (node.nodeValue.length > 26 ? ' ...', '')
}
}
You are doing it the right way – except for the fact that you're introducing an unnecessary global variable. The first assignment should be:
var txtNode = aObj[0].childNodes[0].nodeValue;
Use the var statement! You can also declare the variable before the for loop, at the top of the function. As noted by nnnnnn, you're also creating unintended globals for i and aObj. So you could fix all three problems by inserting this right before the for loop:
var i, aObj, txtNode;
As you can see I am still a novice in javascript
Why is it so that you can append a Textnode only once? When you add it again somewhere else the first one disappears
I do not need a solution to a problem I was just curious what is causing this behavior.
Example where the textnode is only added to the last element of an array:
function hideAdd(){
var hide = document.createTextNode('Afbeelding verbergen');
var afb = collectionToArray(document.getElementsByTagName('img'));
afb.pop();
var divs = [];
for (i=0; i < afb.length; i++){
divs.push(afb[i].parentNode);
}
console.log(divs);
for ( i = 0; i < divs.length;i++){
divs[i].appendChild(hide);
}
}
This is where you use an unique textnode so it works:
function hideAdd(){
var hide = []
var afb = collectionToArray(document.getElementsByTagName('img'));
afb.pop();
var divs = [];
for (i=0; i < afb.length; i++){
divs.push(afb[i].parentNode);
hide[i] = document.createTextNode('Afbeelding verbergen');
}
console.log(divs);
for ( i = 0; i < divs.length;i++){
divs[i].appendChild(hide[i]);
}
}
Short answer is the DOM is a tree, not a network. Each node can have only one parent. If you could add a node in more than one location, it would have more than one parent.