Why is this while loop infinite? JavaScript appendChild - javascript

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.

Related

Cloning element in for loop breaks and runs forever

I want to get all elements (svgs) that have a specific class and clone them inside a div.
const images = document.getElementsByClassName('image_svg'); // collection of all elements (around 5 or so of them)
const myDiv = document.getElementsByClassName('myDiv')[0];
for (let i = 0; i < images.length; i++) {
var clone = images[i].cloneNode(true);
myDiv.appendChild(clone);
}
When I execute my code, it runs forever and the browser stops responding. What am I doing wrong here?
Note, this is a pure JS solution, so no jQuery answers please.
The reason is getElementsByClassName() returns a live collection.
Try with Document.querySelectorAll() and Document.querySelector()
The Document method querySelectorAll() returns a static (not live) NodeList representing a list of the document's elements that match the specified group of selectors.
The Document method querySelector() returns the first Element within the document that matches the specified selector, or group of selectors. If no matches are found, null is returned.
const images = document.querySelectorAll('.image_svg'); // collection of all elements (around 5 or so of them)
const myDiv = document.querySelector('.myDiv');

HTMLCollection seems to be losing every other element when accessing

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.

looping over array of elements just affects the last element

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

What is the idiomatic way to iterate over a NodeList and move its elements without converting to an Array?

This jsFiddle illustrates the problem. If I'm understanding what's going on correctly, as I iterate over and modify the NodeList in place, the counter variable i misses every other node.
In that fiddle, I have two lists, #one and #two and I'd like to move all the children of #one to #two--
<ol id='one'>
<li class='move'>one</li>
<li class='move'>two</li>
<li class='move'>three</li>
<li class='move'>four</li>
<li class='move'>four</li>
</ol>
<ol id='two'>
</ol>
with some minimal JavaScript
var lisToMove = document.getElementsByClassName('move');
var destination = document.getElementById('two');
for (var i = 0; i < lisToMove.length; i++) {
destination.insertBefore(lisToMove[i], null);
}
I know I can fix this by simply converting the NodeList to an Array and iterating over the Array instead, but I was wondering what the right way to iterate over a NodeList if you are modifying the NodeList itself (and not just the nodes) is?
Node lists are often implemented as node iterators with a filter. This means that getting a property like length is O(n), and iterating over the list by re-checking the length will be O(n^2).
var paragraphs = document.getElementsByTagName('p');
for (var i = 0; i < paragraphs.length; i++) {
doSomething(paragraphs[i]);
}
It is better to do this instead:
var paragraphs = document.getElementsByTagName('p');
for (var i = 0, paragraph; paragraph = paragraphs[i]; i++) {
doSomething(paragraph);
}
This works well for all collections and arrays as long as the array does not contain things that are treated as boolean false.
In cases where you are iterating over the childNodes you can also use the firstChild and nextSibling properties.
var parentNode = document.getElementById('foo');
for (var child = parentNode.firstChild; child; child = child.nextSibling) {
doSomething(child);
}
I'm not sure there's a "right" way. Whatever is easiest for you to implement, is easy to understand, and works, should be what you use. A simple slice of the NodeList shouldn't be a problem, even if there's the tiniest performance hit for simply creating a new array and copying references to its contents.
If you truly don't want to copy an array, you can loop over the NodeList backwards, guaranteeing that any changes you make to that node in the list won't actually affect the list.
Here's an example of what I mean:
var lisToMove = document.getElementsByClassName('move');
var destination = document.getElementById('two');
var i = lisToMove.length;
while (i--) {
destination.insertBefore(lisToMove[i], destination.firstChild);
}
DEMO: http://jsfiddle.net/TYZPD/
As you can see, it's not a straightforward change in looping - the logic of where/how to insert the node has to change too, since everything is backwards. That's why I changed the second argument to insertBefore to be destination.firstChild. I'm sure there's other ways to handle the logic, but the principle is the same.
References:
Node.firstChild - https://developer.mozilla.org/en-US/docs/Web/API/Node.firstChild
Node.insertBefore - https://developer.mozilla.org/en-US/docs/Web/API/Node.insertBefore
Using an array is the correct way to do things like this. Just like if you ever have to modify an array while you loop over it, you probably want to separate the modification part and the loop part so one can't affect the other.
In this case, doing that is accomplished by converting the NodeList to an array.
As suggested by Benjamin Gruenbaum, you could also use querySelectorAll, which returns a frozen NodeList.
Demo here

Finding the index of the element with class in native Javascript

Is there a way to get the index of class name (I.e. the third element with the class "className" would be 3 without using jQ?
I don't know jQ, and I don't have time to learn it right now, and I don't want to include code into my code that I don't understand at least some.
Thanks.
BTW, I've used jQ instead of spelling it out so those results can be filtered out in Google should somebody have the same question. If I spelled it out, and somebody used the NOT operator in Google, this one would also disappear.
You could do something like:
// the element you're looking for
var target = document.getElementById("an-element");
// the collection you're looking in
var nodes = document.querySelectorAll(".yourclass");
var index = [].indexOf.call(nodes, target);
See: Array's indexOf.
If you have already a proper array as nodes instead of a NodeList, you can just do nodes.indexOf(target).
you can use document.getElementsByClassName
var el = document.getElementsByClassName('className');
for (var i = 0; i < el.length; i++) {
// your index is inside here
}
el[i] is the element in the current iteration, i is the index
(I.e. the third element with the class "className" would be 3)
if (i == 3)
return el[i]
JsFiddle: here.
Just use getElementsByClassName, it returns a list of elements with the specified classes.
elements = document.getElementsByClassName("test")
element = elements[2] //get the 3rd element
Hope this helps!
these work as of es6:
Array.from(document.querySelectorAll('.elements')).indexOf(anElement)
or
[...document.querySelectorAll('.elements')].indexOf(anElement)

Categories

Resources