Refactoring javascript 'for' loop - javascript

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');
}

Related

click listener on table td while using for loop to assign data

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.

JQuery not replacing html

here is the deal, i have the following jquery code that should add the array values to specific #id, buf it does not replace the code, only add more, and i need a little help to make it replace the html on othe link click.
Code:
function changeClass(curClass){
switch(curClass){
case "Schoolgirl":
case "Fighter":
var charSkillsNames = ["text1","text2","text4","text5"];
//loop show array values
listSkillsNames(charSkillsNames);
break;
}
}
function listSkillsNames(arr){
var length = arr.length,
element = null;
$("#skills").html(function(){
for (var i = 0; i < length; i++) {
element = arr[i];
$(this).append("<li>"+element+"</li>");
}
});
}
this works well but i need it to replace the html inside the "#skills" id when i click on the link that makes it work
PS: problem is really here
The issue is that you don't empty the HTML of #skills element. Use $("#skills").html("") to empty it.
function listSkillsNames(arr){
var length = arr.length,
element = null;
var $skills = $("#skills");
$skills.html(""); // empty the HTML
for (var i = 0; i < length; i++) {
element = arr[i];
$skills.append("<li>"+element+"</li>"); // append new items
}
}
The problem is because you are keep appending new items to the element always without removing the existing items.
Just empty the skill element, also there is no need to use the .html(function(){}) here
function listSkillsNames(arr) {
var length = arr.length,
element = null;
var $skill = $("#skills").empty();
for (var i = 0; i < length; i++) {
element = arr[i];
$skill.append("<li>" + element + "</li>");
}
}

assigning variable new value to change element nodeValue

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;

Variable changes after being used as function parameter

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);
}

How do I find the last item in a js loop so I can add a class to it?

I want to build a list. No problem.
for (i = 0; i < 7; i++) {
$('#domid').append(variable.clone());
}
How do I get the LAST item in the list (in this case i.7) and add a class to it?
for (i = 0; i < 7; i++) {
$('#domid').append(variable.clone());
if (i===7) {
$('.domclass').addClass('last');
};
}
But that won't work. That just makes all of the .todo items have the class when the counter get's to 7.
Any suggestions on how to find this?
Thanks!
$('.domclass:last').addClass('last');
Or, if you'd like to do it in the loop (so, as the commenter pointed out, you don't have to traverse the DOM to get to the element again):
var newElement;
for(var i = 0; i < 7; i++){
newElement = variable.clone();
$('#domid').append(newElement);
if(i === 6) {
$(newElement).addClass('last');
}
}
i never equals 7 because the loop runs while i < 7
eq() with a negative index count backward.
$('.domclass:eq(-1)');
http://api.jquery.com/eq/
One way to do it is within the loop store a reference to the cloned item:
var i,
$item,
$domid = $('#domid');
for (i = 0; i < 7; i++) {
$item = variable.clone();
$domid.append($item);
}
$item.addClass('last');
When the loop ends $item will be the last one added. Of course this assumes the loop will always run for at least one iteration - if the number of iterations is variable and might be zero you could say:
if ($item) $item.addClass('last');
(Note that I'm also storing a reference to the #domid element rather than reselecting it on every iteration.)
This his how I would do it:
var $domid = $( '#domid' );
for ( var i = 0; i < 7; i+= 1 ) {
$domid.append( variable.clone() );
}
$domid.children( ':last' ).addClass( 'last' );

Categories