removing childNodes using node.childNodes.forEach - javascript

Traditionally, a suggested way of removing a node's children in Javascript is to do something like this:
while(node.firstChild) {
node.removeChild(node.firstChild);
}
Recently, I attempted to remove all of a node's children using the built in forEach() method:
node.childNodes.forEach(child => {
node.removeChild(child);
}
This didn't function as I expected. Instead of removing all child nodes, the forEach stopped executing, leaving nodes leftover. What I ended up having to do was use Array.from:
Array.from(node.childNodes)
And then I could remove nodes with forEach. The reason I could not use the traditional method mentioned above is because for some reason, one child was always left behind, causing an infinite loop.
Why does the childNodes.forEach method not remove all the nodes as I thought it would? What am I misunderstanding?

node.childNodes is a live collection. As you remove items from it, the collection itself is modified (live while you're iterating). Trying to iterate it as you are, causes elements to be removed from the collection and moved down in the array-like structure while you're iterating it, causing you to miss nodes.
As an example, when you call removeChild() on the 2nd element in the collection, that element itself is then removed from the collection. That causes what was the 3rd element to be moved into the spot in the collection where the 2nd element was. Now, your loop moves on to the 3rd element in the collection. But, that will skip over the element that is now in the 2nd position causing you to never remove it.
That means the only safe way to iterate through the actual collection and remove things is with a backwards traversal because removing things form the end does not cause other elements to change their position in the collection. Removing items from the front (which is what you were doing) does cause items to move in the collection.
Array.from() converts the live collection to a static array where items are not removed from the array while deleting items from the DOM.
I have a personal rule of DOM development to NEVER use a live collection while I'm modifying the DOM in any way because the danger that the live collection gets modified while I'm trying to use it is too high. Array.from() is a very simple way to get a copy of a live collection that's static and is safe to work with, even as the DOM is being modified.
Another safe way to delete them all is with this backwards iteration because items are removed from the end of the live collection which doesn't cause any items to move in the collection that you haven't yet processed:
for (let i = node.childNodes.length - 1; i >= 0; i--) {
node.removeChild(node.childNodes[i]);
}
But, I generally find this more cumbersome than just converting to a static array with Array.from() as you've already discovered.

node.childNodes is a live collection, so when you remove a child from node in the forEach you mess with the iterator.
https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes

Related

.forEach() stops after first iteration

A simple .forEach() stop after first iteration depending on the content...
My initial loop looked like this :
document.getElementsByClassName("app-form-to-js").forEach(function(node) {
node.classList.remove("app-form-to-js");
jsForms[node.id]=node.cloneNode(true);
node.remove();
});
While the first loop occur, it doesn't go beyond.
Then, after some tweaking, I managed to get the loop working with this code :
document.getElementsByClassName("app-form-to-js").forEach(function(node) {
jsForms[node.id]=node.cloneNode(true);
jsForms[node.id].classList.remove("app-form-to-js");
});
But, the moment I use node.classList.remove("app-form-to-js"); or node.remove(); in the loop,
it will always stop after first iteration.
Any idea why loop is stopping after first iteration (so it iterate at least once) if I alter the node?
document.getElementsByClassName returns an HTMLCollection, which doesn't have a definition for forEach. You can however turn this into an array, which does have one defined, by using Array.from() like so:
var collection = Array.from(document.getElementsByClassName("app-form-to-js"));
collection.forEach(function(node){...});
EDIT:
As was pointed out to me by #Preciel, the NodeList object does have a forEach() function similar to an array. If you would rather use this, you can replace document.getElementsByClassName("app-form-to-js") with document.querySelectorAll(".app-form-to-js") and this will return a NodeList rather than an HTMLCollection
An alternative way to create an array using ES6 spread operator (this is my preferred method):
const elements = [...document.getElementsByClassName("app-form-to-js")];
elements.forEach((node)=>{...});
//OR:
elements.map((node,i)=>{... return node});
//Etc...
If you use the .map() method, you can make changes directly to the elements and it will be reflected in the DOM etc. - very powerful technique (remember to return the modified node in the function)

Understanding how document.getElementByClassName works - Javascript

I've read the this and this, but I'm trying to figure out how document.getELementbyClassName works so that I can re-implement it as an exercise. Obviously, I don't wan to just imitate the source code; my version will probably be much slower and more crude. I have a few questions, but any insight provided beyond my questions is appreciated.
When called on the document object, the complete document is searched, including the root node.
How does it search the entire document? Does this use some sort of regular expression?
document.getElementsByClassName('red test'); This is supposed to return all elements that have both red and test classes. But doesn't each element only have one class? Or is this to mean something like red orange test?
Is it correct that the elements are returned in an array? Something like [element1, element2, ...]. I'm not sure what is meant by "array-like."
Note: I'm new to JavaScript and have even less exerpeince with HTML, CSS, and jQuery.
To answer your questions:
Probably recursively. Otherwise there are plenty of different ways to traverse n-ary trees which is what the DOM is. Depth-first, breadth-first, however-you-want-first-really, they can all be implemented recursively, or alternatively use some data structure like a stack or a queue. How it does it really isn't important, what matters is how YOU think it should be done.
A simple algorithm for recursively identifying elements with those classes would be something like this
getByClassName(class, root) {
ret = []
if (root has class) {
ret.push(root);
}
for (each child of root) {
append getByClassName(class, child) to ret;
}
return ret;
}
Elements can have multiple classes. <a class="foo bar baz"> has classes foo,bar, and baz
All the Javascript methods like this operate on the DOM, not the HTML source code. When the HTML is loaded, the browser parses the HTML into the DOM, which is a data structure containing objects that represent the document contents. So it doesn't need to do pattern matching, it simply searches through the data structure for elements whose class list contains the specified class.
ELements can have more than one class, and this is very common. For instance, you might have a button class for all buttons, and an active class for active elements. The active button might then be <span class="button active">contents</span>.
The elements are returned in an HTMLCollection. This is an array-like object, so you can use elements.length to get the number of elements, and elements[i] to access a specific element in the collection. It's also a "live" collection, which means that if you change the DOM, the collection will automatically be updated to reflect the changes (e.g. if you remove the class from the object, it will no longer be in the collection).

How do you add an object to a jQuery collection at a specified index?

I am creating a JavaScript class that represents a data-bound table, using jQuery to handle DOM manipulation. The class has a $table.$body.$rows property where I am keeping a collection of the table's jQuery-wrapped rows to avoid performing a $table.$body.children('tr') call whenever a row is added or removed. When a row enters or leaves edit mode, I need to be able to add and remove objects from that $table.$body.$rows property, which I accomplish with jQuery's .add() and .not() methods.
These methods are inadequate, however, when the row being edited is not at the very end of the table, since the .add() method adds the new item to the end of the internal collection maintained in the jQuery instance. In order to make sure the $table.$body.$rows collection is correctly ordered, I need to be able to insert the new item at a specified index within the jQuery collection. Does such a method already exist, or am I going to have to write it myself?
I could just let the HTMLTableSectionElement.rows property keep track of the rows for me and simply wrap a particular row in an jQuery object when necessary, but this seems inefficient. This raises a secondary question: how expensive is a call like .children('tr') anyway, and would I be better off simply reassigning $table.$body.$rows each time a row is added or removed?
Technically, jQuery do add splice() to jQuery.fn, but it isn't documented. They almost removed it from the public interface recently, but decided against it;
We want to avoid the appearance that using these methods [jQuery.fn.{push,sort,splice}] is encouraged, but we don't want to remove them either.
... make of that what you will.
You could use Array.prototype.splice.call, e.g.:
Array.prototype.splice.call($table.$body.$rows, 3, 0, newItem);
... and I can't see how that'd be wrong.
Although in all honesty, I think you're trying to solve a problem that doesn't exist.
I could just let the HTMLTableSectionElement.rows property keep track of the rows for me and simply wrap a particular row in an jQuery object when necessary
... I'd do this. Calling .children('tr'), and updating $table.$body.$rows when rows are modified is not going to kill your application.
You could the splice function on the native array object.
arr.splice(index, 0, item); will insert item into arr at the specified index.

Why can't styles be removed in forward order?

I modified the question/answer in the SO post below to remove styles, in forward order.
The question/answer removes scripts in reverse order.
It did not work. However, if I changed it back to reverse order things worked.
My guess, was that if you remove style[0], that style[1] would immediately update to become style[0], in an example w/ only two styles. Hence the loop would fail.
Is this correct?
Is the style array updated near instantaneously as it is modified?
Reference Post
var scripts = document.getElementsByTagName("script");
for (var i=scripts.length; i--; ){
(scripts[i]).parentNode.removeChild(scripts[i]);
}
Your guess is correct; getElementsByTagName returns a live "array" (which is not actually an array, but rather a NodeList or HTMLCollection, depending on browser and version) that reflects subsequent updates to the DOM.
So, you can write:
var styles = document.getElementsByTagName("style");
while (styles.length) {
styles[0].parentNode.removeChild(styles[0]);
}
That said, there's no reason to prefer this way. Since JavaScript is run in the same thread that paints the UI, the result of removing the styles won't take effect until after the loop is complete, so the order doesn't matter to you.
Useful MDN links:
element.getElementsByTagName
NodeList
HTMLCollection
From https://developer.mozilla.org/en-US/docs/Web/API/element.getElementsByTagName:
elements is a live NodeList (but see the note below) of found elements in the order they appear in the subtree. If no elements were found, the NodeList is empty.
So as soon as you remove elements[0], elements[0] is filled with elements[1], thus removing elements[1] (unless there was an elements[2]).
You can do it in "normal" order like this (although it's not as performant due to the repeating test of scripts.length):
var scripts = document.getElementsByTagName("script");
while (scripts.length) {
(scripts[0]).parentNode.removeChild(scripts[0]);
}

retain reference to appended node javascript

No libraries please. Beyond my control.
I'm appending a document fragment to the dom. THIS ALL WORKS. No problemo.
How do I retain/retrieve the node list after the fragment is appended? Relevant code:
var frag = document.createDocumentFragment();
//add some content to the fragment
element.appendChild(frag);
Again, this works! I do NOT need a troubleshoot on how to add things to the dom!
If I set var e = element.appendChild(frag);, everything gets appended normally, but e = an empty document fragment.
I'm looking for some smooth magic voodoo here. Don't send me looping over the entire dom. The content could be anything, one or many nodes with or without children. If there's some trick with querySelectorAll or something that would be acceptable.
Thanks!
EDIT
Upon further poking, it appears that e above is in fact a returned reference to the frag var, that is empty after appending it to the dom. It's much like the elements were neatly slid off of the fragment into the dom, and the fragment just lays around empty.
It's exactly what you've described; when the elements are appended to an element, they're removed from the fragment to prevent memory leaks from lingering references.
One way to get those child nodes is to make a shallow copy of the fragment's childNodes before doing appendChild():
// make shallow copy, use Array.prototype to get .slice() goodness
var children = [].slice.call(frag.childNodes, 0);
element.appendChild(frag);
return children;
If you're looking just after appending it, the lastChild is the way to go. Use it like this:
var e = element.lastChild;
More info in SitePoint
From doc of DocumentFragment:
Various other methods can take a document fragment as an argument (e.g., any Node interface methods such as appendChild and insertBefore), in which case the children of the fragment are appended or inserted, not the fragment itself.
So appendChild method return the documentFragment but it's empty because its child nodes are inserted in your element object and (from appendChild doc):
[...] a node can't be in two points of the document simultaneously. So if the node already has a parent, it is first removed, then appended at the new position.
Now...
You can store in a cache array your children and after return it (or see Jack solution :-))
Like Jack's solution, but if you are using esnext you could use Array.from as
const children = Array.from(frag.children);
element.appendChild(frag);
return children;
If you have single main element in your document fragment, you can use .firstElementChild and that's all. To make multiple copies use .cloneNode(true).
Example - https://codepen.io/SergeiMinaev/pen/abLzQjZ
Thanks to #The Sloth's comment, I found this solution, which finally solved it for me:
var node = element.lastElementChild
In my case, element.lastChild returned a text node instead of the appended node.

Categories