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]);
}
Related
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
I'm thinking it should return only exact match.
I see something like in source I have to work with:
var someArray = someObject.getElementsByTagName("item");
Except when I inspect the DOM, I don't see any tags named "items".
There are some css classes '.some_item_details'.
Subsequently I see an error about the element being null, which makes sense to me. What bugs me is that I'm seeing this in production codebase. So I'm thinking, "surely no one would commit something like this, I must be missing something".
Wouldn't 'item' have to exist a custom tag?!?
check getElementsByTagName()
Indeed,getElementsByTagName matches makes exact matches.
Returns an HTMLCollection of elements with the given tag name. The
complete document is searched, including the root node. The returned
HTMLCollection is live, meaning that it updates itself automatically
to stay in sync with the DOM tree without having to call
document.getElementsByTagName() again.
Note:
document.getElementsByTagName() is similar to
element.getElementsByTagName(), except that its search encompasses the
whole document.
#NiettheDarkAbsol's comment :
var someArray = someObject.getElementsByTagName("item");
Indeed, the code snippet you have shown would be searching for item
tags. This might be okay if someObject is an XML context.
The issue is similar to the one described here, as far as I can tell. However, I am not using mootools and my question and results are different.
This is a test page to demonstrate the issue: http://jsfiddle.net/S7rtU/2/.
I add elements to a container using createElement and appendChild. As I add each, I also store a reference to it in a private array (elems).
Then, I decide to clear the container, and do so by setting container.innerHTML = ''.
(In my example, I set it first to a pending message, and then 3s later, using setTimeout, I clear it. This is to give time to observe the changes to the DOM.)
Then, I try to repopulate the container from the elems array, calling container.appendChild for each of the stored elements.
I have tested on the browsers I have at hand: Firefox 17.0.1, Chrome 23.0.1271.97, Safari 3.1.2, IE 6 and IE 7. All except IE 6 & 7 will actually restore those elements. So they are successfully stored in memory and references not destroyed. Furthermore, the event handlers registered to those elements still work.
In IE, the elements do not reappear.
What I have read about this issue, including the other SO question, seem to suggest that references are supposed to be broken when you modify innerHTML on the container. Event handlers are also supposed to be broken. However the 3 modern browsers I tested do not break the references, nor the event handlers.
Of course, to make this work in IE I can use something like this, but this is significant extra processing if there are lots of elements:
function explicitClearContainer() {
var e;
// Explicitly remove all elements
for (var i = 0; i < elems.length; ++i) {
e = elems[i];
// Update the reference to the removed Node
elems[i] = e.parentNode.removeChild(e);
}
}
My question is what is known about this behaviour, what can be expected in different environments, what are the pitfalls of using this sort of technique?
I would appreciate any comments.
The innerHTML property was only "standardised" in HTML5, which really just documents common browser behaviour for many features. Things such as innerHTML have been implemented differently in different browsers and will continue to be different for some time, so best to avoid using it if it's causing problems.
There are other approaches to clearing the child nodes of an element, e.g.:
function removeContent(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
Which should avoid your issues with innerHTML and is a bit less code than your version. You can also replace element with a shallow clone of itself, but that may cause other issues.
Libraries I've seen have DOM wrappers that inclusively handle only the first element of the list in some case, like:
return this[0].innerHTML
and use the whole list in some other like:
for( var i=0, l=this.length; ++i<l; ) this[i].className = cls;
return this
Why is this approach accepted?
I think singling out the first element defeats the purpose of having methods that apply the same thing on the rest of the list. Isn't it bad to have dubious functions? I know it suits many people..but it feels inconsistent and I'm interested in why this is accepted so widely.
EDIT as an example:
jQuery.html()
If the selector expression matches more than one element, only the
first match will have its HTML content returned.
why not all?
the hide() method in bonzo, from Dustin Diaz
//...
hide: function () {
return this.each(function (el) {
el.style.display = 'none'
})
}
why not only the first?
The accessor methods in jQuery return single values because it's simpler and more generally useful. If the .html() API were to return the value if innerHTML for all elements, that'd mean it'd have to return an array. That, in turn, would mean that in the most common case of wanting the contents of a single element, you'd have to add the array access. There's also the problem of knowing exactly which returned value goes with which selected element. In other words, if .html() returned an array of element contents:
var contentList = $('.someClass, span, .hidden .container').html();
If "contentList" were just a simple array, what use would it be? How would the code know for each element which DOM node it came from? Of course there are solutions to this, but again the simple case is made complicated in order to support a rare general case.
It's possible of course to get the list yourself with .map(). I think this is just an issue of smart, practical, pragmatic API design.
I'm trying to get a list in JavaScript (not using jQuery) of all the elements on the page with a specific class name. I therefore employ the getElementsByClassName() function as follows:
var expand_buttons = document.getElementsByClassName('expand');
console.log(expand_buttons, expand_buttons.length, expand_buttons[0]);
Note that I have three anchor elements on my page with the class 'expand'. This console.log() outputs
[] 0 undefined
Next, for kicks, I threw expand_buttons into its own array as follows:
var newArray = new Array(expand_buttons);
console.log(newArray, newArray.length);
This suddenly outputs
[NodeList[3]] 1
and I can click through the nodelist and see the attributes of the three 'expand' anchor elements on the page. It's also worth noting that I was able to get my code working in a w3schools test page.
It may also be of note that my use of document.getElementsByName actually does output (to the console) an array of elements, but when I ask for its length, it tells me 0. Similarly, if I try to access an array element using array_name[0] as normal, it outputs 'undefined', despite there clearly being an element inside of an array when I print the object to the console.
Does anybody have any idea why this might be? I just want to loop through DOM elements, and I'm avoiding jQuery at the moment because I'm trying to practice coding with vanilla JavaScript.
Thanks,
ParagonRG
It's not so much a JavaScript thing as it is a web browser thing. That API is supplied by a native object (the document object), and by the DOM spec it returns a NodeList object. You can treat a NodeList like an array, and it's similar, but distinctly different (as you've noticed).
You can always copy a NodeList to a new array:
var nodeArr = Array.prototype.slice.call(theNodeList, 0);
or in modern ES2015 environments:
var nodeArr = Array.from(theNodeList);
JavaScript always exists in some runtime context, and the context can include all sorts of APIs that provide facilities to JavaScript code. A web browser is one of those contexts. The DOM is specified in a way that's not especially partial to JavaScript; it's a language-neutral interface definition.
I guess the short version of this answer would be, "because it just does."
It doesn't return an array because the object it returns is "live", specifically it is a live NodeList:
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.