In one of my projects I just discovered, that sometimes iterating over an array of html elements (and change all of them) just affects the last element. When I log the element's attributes I can see that the loop definitily adresses every element but nevertheless visibly just the last element is getting changed.
Can anyone explain me why?
I already figured out, that a solution is to use createElement() and appendChild() instead of insertHTML. I just want to understand why javascript behaves like this.
Here is my example code:
/* creating 5 elements and storing them into an array */
var elementArray = [];
for(var n = 0;n<5;n++)
{
document.body.innerHTML += "<div id='elmt_"+n+"'>"+n+"</div>\n";
elementArray[n] = document.getElementById("elmt_"+n);
}
/* loop over these 5 elements */
for(var n = 0;n<5;n++)
{
console.log(elementArray[n].id); // logs: elmt_0 elmt_1 elmt_2 elmt_3 elmt_4
elementArray[n].innerHTML = "test"; // changes just the last element (elmt_4) to "test"
}
I created an example here: http://jsfiddle.net/qwe44m1o/1/
1 - Using console.log(elementArray[n]); in your second loop shows that innerHTML in this loop is modifying html inside your array, not in your document. That means that you are storing the div element in your array, not a shortcut to document.getElementById("elmt_"+n)
See the JSFiddle
2 - If you want to store a shortcut in order to target an element by ID, you have to add quotes for elementArray[n] = "document.getElementById('elmt_"+n+"')";, and use it with eval like this : eval(elementArray[n]).innerHTML = n+"-test";
See the JSFiddle for this try
Related
I've got a task to do - to do list. I have a problem because i don't know how to move elements from parent "priority" to parent "standard".
My code of button to move is:
moveToStandardButton.addEventListener('click', function() {
const allListElements = document.querySelectorAll('[data-id="move"]');
for (let i = 0; i < allListElements.length; i++) {
let toClone = allListElements[i].checked;
}
})
toClone shoud have all list elements which are checked by user and then after click button it should move this elements to parent "standard". I've tried to do it like this but i can't use cloneNode at toClone or toClone[i].
If you log toClone, it is probably a boolean (true or false), as it is pulling whether the element is checked.
Try the following:
moveToStandardButton.addEventListener('click', function() {
// Get all elements that have a data-id of move
// I'm assuming these elements are the ones that need to move ;)
const allListElements = document.querySelectorAll('[data-id="move"]');
// iterate through the elements that need to move
for (let i = 0; i < allListElements.length; i++) {
let toClone = allListElements[i]; // Note that 'checked' isn't involved
if(!toClone.checked) continue; // If the element isn't checked, move to next element.
const clonedEl = toClone.cloneNode(true);
document.querySelector('#myOtherList').appendChild(clonedEl);
}
})
So after reading your question, I would recommend you watch or read some guides on JavaScript. You have some glaring issues that will become very apparent once you spend some time learning.
That said, I can lend some help to point you in the right direction.
1) It is not recommended to use data attributes as element identifiers. I would instead use a class, rather than data-id="move"
2) Your line let toClone is defined inside the for loop, and is scoped as such. It will not be available outside of the for loop.
3) Try creating an array outside of your for loop, and storing each checked element inside of it, and then operating on them after your loop.
As you see below in the code, I have a html snippet in string which is then parsed into a document. But when I iterate through it and get the item in the collection, only every other item is accessible. Also some behavior like losing an item in collection also happens and ends up being left with only two items remaining.
JSFiddle - https://jsfiddle.net/hari7190/xpvt214o/878427/
var response = "<option value='Bobs Dock'>Bobs Dock</option><option
value='Johns Dock'>Johns Dock</option><option value='Mikes Dock'>Mikes Dock</option><option value='Jacob Dock'>Jacob Dock</option><option value='Foo Dock'>Foo Dock</option>"
parser = new DOMParser();
doc = parser.parseFromString("<select>" + response + "</select>", "text/html");
var options = doc.getElementsByTagName("option");
for(i=0; i<options.length; i++){
console.log(i, options.length);
document.getElementById("list").append(options[i]);
}
Results of iteration goes like :
index - 1 options.length - 3
index - 2 options.length - 2
Can anyone explain why this implementation behaves this way?
Please note: I understand how to achieve the result (like here), but I am looking for an explanation why the above code behaves this way.
The answer lies in the append function.
You see the, append (which is based on the more standard appendChild) function adds the node and detaches it from the current parent.
See documentation:
The Node.appendChild() method adds a node to the end of the list of
children of a specified parent node. If the given child is a reference
to an existing node in the document, appendChild() moves it from its
current position to the new position (there is no requirement to
remove the node from its parent node before appending it to some other
node).
Now, the current parent of these option nodes is your select node.
Now, your options variable - It might look like it's an array, but its actually an HTMLCollection. And, again, from the documentation :
An HTMLCollection in the HTML DOM is live; it is automatically updated
when the underlying document is changed.
So in your loop, each time you append an element, the appended element is removed from the select node, the HTMLCollection options sees this and becomes shorter, but since i is incremented, you skip over an element.
To see this for yourself you can add a debugger; line in your jsfiddle and debug this line by line in the browser.
i am new to js.
can you tell me why I am getting empty values for sports-title and third.
since we have one div with content in it.
sports-title---->{"0":{}}
third---->{}
providing my code below.
findStringInsideDiv() {
/*
var str = document.getElementsByClassName("sports-title").innerHTML;
*/
var sportsTitle = document.getElementsByClassName("sports-title");
var third = sportsTitle[0];
var thirdHTML = third.innerHTML
//str = str.split(" ")[4];
console.log("sports-title---->" + JSON.stringify(sportsTitle));
console.log("third---->" + JSON.stringify(third));
console.log("thirdHTML---->" + JSON.stringify(thirdHTML));
if ( thirdHTML === " basketball football swimming " ) {
console.log("matching basketball---->");
var menu = document.querySelector('.sports');
menu.classList.add('sports-with-basketball');
// how to add this class name directly to the first div after body.
// but we are not rendering that div in accordion
//is it possible
}
else{
console.log("not matching");
}
}
When you call an object in the Document Object Model (DOM) using any of the GetElement selectors, it returns an object that can be considered that HTML element. This object includes much more than just the text included in the HTML element. In order to access the text of that element, you want to use the .textContent property.
In addition, an HTML class can potentially be assigned to several elements and therefore GetElementsByClassName returns an array so you would have to do the following, for example:
console.log("sports-title---->" + JSON.stringify(sportsTitle[0].textContent));
You can find a brief introduction to the DOM on the W3Schools Website. https://www.w3schools.com/js/js_htmldom.asp If you follow along it gives an overview of different aspects of the DOM including elements.
Maybe this would be helpful
As you see sportsTitle[0].textContent returns full heading and 0 is the index thus you get "0" when you stringify (serialize) sportsTitle. Why 0? Because you have one <h1> element . See this fiddle http://jsfiddle.net/cqj6g7f0/3/
I added second h1 and see the console.log and you get two indexes 0 and 1
if you want to get a word from element so get substring use substr() method https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr
One way is to change <h1> class attr to id and do sportsTitle.textContent;
and use substr() on this string
or
2nd way is to remain class attr and do sportsTitle[0].textContent;
and substr() on this string
The 2nd is the better way
I'm am battling with a javascript function I'm working on.
Inside a for loop I'm iterating all elements with class "visible", inside that loop
I'm preforming two actions.
elements[i].removeAttribute("class");
elements[i].setAttribute("class", "hidden");
For some reason only 1 is valid. 2 produces an error saying:
Uncaught TypeError: Cannot call method 'setAttribute' of undefined
Even when I log elements[i] using console.log; after the first console.log call
the element exists, but on the second console.log elements[i] is 'undefined'
What the hell am I missing here, this is driving me crazy, if my laptop wasn't so expensive
it would have been broken by now. Help :(
Here's the function:
function hide_visable_elements()
{
// remove body EventListener
var body = document.getElementsByTagName("body");
body[0].removeEventListener("click", hide_visable_elements, true);
var elements = document.getElementsByClassName("visible");
for (var i = 0; i < elements.length; i++)
{
console.log(elements[i]); // Works like a swiss clock
elements[i].removeAttribute("class");
console.log(elements[i]); // why elements[i] is 'undefined' now ???
elements[i].setAttribute("class", "hidden"); // << turns to useless code
}
}
This is because getElementsByClassName returns a NodeList, which is live. That is, it updates itself when the elements it refers to change.
When you remove the class attribute from an element in the NodeList, it gets removed from that list (since it no longer has the visible class name).
You don't actually need to remove the attribute. Just setting it will do the job just as well. But since the NodeList is changing as you manipulate the elements it contains, you need to count backwards through it (as each time you change one element of it, it is removed so the length decreases by one):
for (var i = elements.length - 1; i >= 0; i--) {
elements[i].setAttribute("class", "hidden");
}
getElementsByClassName is a live NodeList so changing className of the items immediately affects whole list. I would recommend use querySelectorAll insead.
Plus instead of var body = document.getElementsByTagName("body"); use document.body.
I think that the problem is elements[i].removeAttribute("class"); since you selected the element using a class getElementsByClassName("visible"); . I think so when you remove class attribute completely from the element things are going wrong.
Try some tweak with the code. You are not suppose to remove attribute class if you are planning to use the same element which is selected using class attribute.
I swear this was just working fine a few days ago...
elm = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
while (elm[0]){
frag.appendChild(elm[0]);
}
Right, so, this should append each node from our elm node list. When the first one is appended, the second "moves" to the first position in node list, hence the next one is always elm[0]. It should stop when the elm nodeList is completely appended. However, this is giving me an infinite loop. Thoughts?
EDIT - because I've gotten the same answer several times...
A nodeList is not an array, it is a live reference. When a node is "moved" (here, appended) it should be removed automatically from the node list. The answers all saying "you're appending the same element over and over" - this is what's happening, it shouldn't be. A for loop shouldn't work, because when the first node is appended, the next node takes its index.
2nd EDIT
So the question is now "why is the nodeList behaving as an array?". The node list SHOULD be updating every time a node is being appended somewhere. Most peculiar.
Solution (in case someone needs something to handle live + non-live node lists)
elm = (/*however you're getting a node list*/);
var frag = document.createDocumentFragment();
var elength = elm.length;
for (var b = 0; b<elength; b++){
if (elm.length === elength){
frag.appendChild(elm[b]);
} else {
frag.appendChild(elm[0].cloneNode());
}
}
Basically, just checking to see if the node list has changed length.
From the MDN Docs
Element.querySelectorAll
Summary
Returns a non-live NodeList of all elements descended from the element on which it is invoked that match the specified group of CSS selectors.
Syntax
elementList = baseElement.querySelectorAll(selectors);
where
elementList is a non-live list of element objects.
baseElement is an element object.
selectors is a group of selectors to match on.
From the docs above you can see it does not automatically remove it when you append it to another element since it is non live. Run a demo to show that feature.
var selector = "div";
elm = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
console.log("before",elm.length);
frag.appendChild(elm[0]);
console.log("after",elm.length);
When the code above runs, in the console you get.
before 3
after 3
If you want to do the while loop, convert to an array and shift() the items off
var selector = "div";
var elmNodeLIst = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
var elems = Array.prototype.slice.call(elmNodeLIst );
while (elems.length) {
frag.appendChild(elems.shift());
}
console.log(frag);
You are appending the first item in the node list, over and over and over. You never removing any items from the array, but always adding the first one to the fragment. And the first one is always the same.
elm = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
while (elm.length){
frag.appendChild(elm.shift());
}
This may be closer to what you meant to do. We can use while (elm.length) because as items get removed form the array, eventually length will be zero which is a flasy value and the loop will stop.
And we use elm.shift() to fetch the item from the array because that method will return the item at index zero and remove it from the array, which gives us the mutation of the original array we need.
I think you thought this might work because a node can only have one parent. Meaning adding somewhere removes it from the previous parent. However, elm is not a DOM fragment. It's just a aray (or perhaps a NodeList) that holds references to element. The array is not the parent node of these elements, it just holds references.
Your loop might work if you had it like this, since you are query the parent node each time for its children, a list of node that will actually change as you move around:
elm = document.getElementById(id);
var frag = document.createDocumentFragment();
while (elm.children[0]){
frag.appendChild(elm.children[0]);
}
I wouldn't have expected it to work in the first place.
Your elm array is initialized, and never updated. Even if the result from running document.querySelectorAll(selector); would return something different, this doesn't change your current references in the array.
You would either need to rerun the selector, or manually remove the first element in the array after appending it.
elm[0] is static and unchanging in above code
fix is as below
elm = document.querySelectorAll(".container");
var frag = document.createDocumentFragment();
console.log(elm);
var i=0;
while (elm[i]){
frag.appendChild(elm[i++]);
}
I didn't actually focus much on the code (and if it made sense -judging from the comments- or not); but if this worked a few days ago then the problem is in the input you are giving to your code selector.
That's when Unit Testing comes in handy. If you can remember the input with which the code worked, then you can make it work again and start debugging from there.
Otherwise, you are just lying to yourself.
It's an infinite loop as it's written right now because elm[0] always refers to the same element, and that element is never null (any non-null/non-zero result would be true). You also don't do anything with the elements themselves to make it iterate across the list. You should be using a for loop instead of a while or at least having some kind of indexer to try to traverse the collection.
elm = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
for (i= 0; i < elm.length; i++)
{
frag.appendChild(elm[i]);
}
Edit:
From the documentation:
A "live" collection
In most cases, the NodeList is a live collection. This means that changes on the DOM tree >are going to be reflected on the collection.
var links = document.getElementsByTagName('a'); // links.length === 2 for instance
document.body.appendChild( links[0].cloneNode(true) ); // another link is added to the document
// the 'links' NodeList is automatically updated
// links.length === 3 now. If the NodeList is the return value of document.querySelectorAll, it is NOT live.
Going on this documentation, your current usage of the method indicates you do not have a live NodeList. Thus appending will never modify the original list. You will either need to modify your usage within the loop to mirror this usage of .cloneNode(true) or iterate manually.