Could you please look at this jsFiddle example, and tell me why the number '11' is alerted rather than '5' (the number of <li> elements)?
From jsFiddle:
HTML
<ul id="list">
<li>milk</li>
<li>butter</li>
<li>eggs</li>
<li>orange juice</li>
<li>bananas</li>
</ul>
JavaScript
var list = document.getElementById('list');
var list_items = list.childNodes;
alert(list_items.length);
The childNodes, depending on the browser used, will return the text nodes, as well as the tags that are children of the parent node. So technically, the whitespace in between the <li> tags will also be counted among the childNodes.
To avoid processing them, you may check that nodeType != 3. Here is a list of node types.
var list = document.getElementById('list');
var list_items = list.childNodes;
var li_items = [];
for (var i=0; i<list_items.length; i++) {
console.log(list_items[i].nodeType);
// Add all the <li> nodes to an array, skip the text nodes
if (list_items[i].nodeType != 3) {
li_items.push(list_items[i]);
}
}
You have text nodes there.
You can skip them while iterating with...
for (var i = 0, length = list_items.length; i < length; i++) {
if (list_items[i].nodeType != 1) {
continue;
}
// Any code here that accesses list_items[i] will sure to be an element.
}
jsFiddle.
Alternatively, you could do it in a more functional way...
list_items = Array.prototype.filter.call(list_items, function(element) {
return element.nodeType == 1;
});
jsFiddle.
You must use convert it to a proper array to use the filter() method. childNodes property returns a NodeList object.
As others have pointed out, the childNode count inclues the text nodes, generated by the whitespace between the <li> elements.
<ul id="list"><li>milk</li><li>butter</li><li>eggs</li><li>orange juice</li><li>bananas</li></ul>
That will give you 5 childNodes because it omits the whitespace.
Text nodes are included in the child nodes count. To get the proper value, you'd need to strip out text nodes, or make sure they are not in your code. Any white space between code is considered a space and a text node, so your count is the total number of text nodes.
I cobbled together a solution for this that I like. (I got the idea from this blog post.)
1) First I get the number of child elements nodes by using:
nodeObject.childElementCount;
2) Then I wrote a function that will return any child element node by index number. I did this by using firstElementChild and nextElementSibling in a for loop.
function getElement(x, parentNode){
var item = parentNode.firstElementChild
for (i=0;i<x;i++){
item = item.nextElementSibling;
}
return item;
}
This returns the child element I need for anything I want to pull from it. It skips the problem with childNodes retuning all the different nodes that are not helpful when trying to parse just the elements. I am sure someone more experienced than me could clean this up. But I found this so helpful that I had to post it.
Use obj.children instead.
var list = document.getElementById('list');
var list_items = list.children;
alert(list_items.length);
The difference between this children and childNodes, is that childNodes contain all nodes, including text nodes and comment nodes, while children only contain element nodes.
from w3schools.
Related
I am Javascript beginner.
I am initing web page via the window.onload, I have to find bunch of elements by their class name (slide) and redistribute them into different nodes based on some logic. I have function Distribute(element) which takes an element as input and does the distribution. I want to do something like this (as outlined for example here or here):
var slides = getElementsByClassName("slide");
for(var i = 0; i < slides.length; i++)
{
Distribute(slides[i]);
}
however this does not do the magic for me, because getElementsByClassName does not actually return array, but a NodeList, which is...
...this is my speculation...
...being changed inside function Distribute (the DOM tree is being changed inside this function, and cloning of certain nodes happen). For-each loop structure does not help either.
The variable slides act's really un-deterministicaly, through every iteration it changes it's length and order of elements wildly.
What is the correct way to iterate through NodeList in my case? I was thinking about filling some temporary array, but am not sure how to do that...
EDIT:
important fact I forgot to mention is that there might be one slide inside another, this is actually what changes the slides variable as I have just found out thanks to user Alohci.
The solution for me was to clone each element into an array first and pass the array ono-by-one into Distribute() afterwards.
According to MDN, the way to retrieve an item from a NodeList is:
nodeItem = nodeList.item(index)
Thus:
var slides = document.getElementsByClassName("slide");
for (var i = 0; i < slides.length; i++) {
Distribute(slides.item(i));
}
I haven't tried this myself (the normal for loop has always worked for me), but give it a shot.
If you use the new querySelectorAll you can call forEach directly.
document.querySelectorAll('.edit').forEach(function(button) {
// Now do something with my button
});
Per the comment below. nodeLists do not have a forEach function.
If using this with babel you can add Array.from and it will convert non node lists to a forEach array. Array.from does not work natively in browsers below and including IE 11.
Array.from(document.querySelectorAll('.edit')).forEach(function(button) {
// Now do something with my button
});
At our meetup last night I discovered another way to handle node lists not having forEach
[...document.querySelectorAll('.edit')].forEach(function(button) {
// Now do something with my button
});
Browser Support for [...]
Showing as Node List
Showing as Array
An up-to-date answer in 2021
.getElementsBy* methods return a live HTMLCollection, not a NodeList, getElementsByName being an exception.
There are remarkable differencences between these two lists. Whereas HTMLCollection has two methods, NodeList has five methods, including NodeList.forEach, which can be used to iterate through a NodeList.
Live collections are problematic because there's no way to keep the collection updated under the hood. To achieve a reliable collection, the DOM is traversed every time a collection is accessed, in every current implementation of HTMLCollection. In practice this means, that every time you access a member of a live collection (including the length), the browser traverses the entire document to find the specific element.
The Standard says:
If a collection is live, then the attributes and methods on that object must operate on the actual underlying data, not a snapshot of the data.
Never iterate live HTMLCollection!
Instead, convert the collection to array, and iterate that array. Or rather get the elements using .querySelectorAll, which gives you a static NodeList and a more flexible way to select elements.
If you really need a live list of elements, use the closest possible common ancestor element as the context instead of document.
It's notable, that also live NodeLists exist. Examples of live NodeLists are Node.childNodes and the return value of getElementsByName.
You could always use array methods:
var slides = getElementsByClassName("slide");
Array.prototype.forEach.call(slides, function(slide, index) {
Distribute(slides.item(index));
});
Update 2022
Fastest and shortest solution
[...document.getElementsByClassName('className')].forEach(el => {
// Do something with each element
})
Why does it work?
Iterating live HTML collection is extremely inefficient
As mentioned in styks' answer above, [...htmlCollection] converts the the class collection into an array.
It is necessary to convert it to an array, since iterating a live HTMLCollection directly would be extremely inefficient. As teemu wrote above "Never iterate live HTMLCollection!":
Live collections are problematic because there's no way to keep the collection updated under the hood. To achieve a reliable collection, the DOM is traversed every time a collection is accessed, in every current implementation of HTMLCollection. In practice this means, that every time you access a member of a live collection (including the length), the browser traverses the entire document to find the specific element.
Why is it the fastest and most efficient?
Note that using [...arr]
is the fastest and most efficient way to convert htmlCollection to an array.
A performance comparison of all methods made by harpo can be found here:
http://jsben.ch/h2IFA
(See all details about htmlCollection conversion to an array here)
I followed Alohci's recommendation of looping in reverse because it's a live nodeList. Here's what I did for those who are curious...
var activeObjects = documents.getElementsByClassName('active'); // a live nodeList
//Use a reverse-loop because the array is an active NodeList
while(activeObjects.length > 0) {
var lastElem = activePaths[activePaths.length-1]; //select the last element
//Remove the 'active' class from the element.
//This will automatically update the nodeList's length too.
var className = lastElem.getAttribute('class').replace('active','');
lastElem.setAttribute('class', className);
}
You could use Object.values + for...of loop:
const listA = document.getElementById('A');
const listB = document.getElementById('B');
const listC = document.getElementById('C');
const btn = document.getElementById('btn');
btn.addEventListener('click', e => {
// Loop & manipulate live nodeLList
for (const li of Object.values(listA.getElementsByClassName('li'))) {
if (li.classList.contains('active')) {
listB.append(li);
} else {
listC.append(li);
}
}
});
ul {
display: inline-flex;
flex-direction: column;
border: 1px solid;
}
ul::before {
content: attr(id);
}
.active {
color: red;
}
.active::after {
content: " (active)";
}
<ul id="A">
<li class="li active">1. Item</li>
<li class="li">2. Item</li>
<li class="li">3. Item</li>
<li class="li active">4. Item</li>
<li class="li active">5. Item</li>
<li class="li">6. Item</li>
<li class="li active">7. Item</li>
<li class="li">8. Item</li>
</ul>
<button id="btn">Distribute A</button>
<ul id="B"></ul>
<ul id="C"></ul>
One-liner:
Object.values(listA.getElementsByClassName('li')).forEach(li => (li.classList.contains('active') ? listB : listC).append(li))
Here is the example I've done like this to assign random number to each class:
var a = $(".className").length;
for(var i = 0; i < a; i++){
var val = $(".className")[i];
var rand_ = (Math.random()*100).toFixed(0);
$(val).val(rand_);
}
It's just for reference
<!--something like this-->
<html>
<body>
<!-- i've used for loop...this pointer takes current element to apply a
particular change on it ...other elements take change by else condition
-->
<div class="classname" onclick="myFunction(this);">first</div>
<div class="classname" onclick="myFunction(this);">second</div>
<script>
function myFunction(p) {
var x = document.getElementsByClassName("classname");
var i;
for (i = 0; i < x.length; i++) {
if(x[i] == p)
{
x[i].style.background="blue";
}
else{
x[i].style.background="red";
}
}
}
</script>
<!--this script will only work for a class with onclick event but if u want
to use all class of same name then u can use querySelectorAll() ...-->
var variable_name=document.querySelectorAll('.classname');
for(var i=0;i<variable_name.length;i++){
variable_name[i].(--your option--);
}
<!--if u like to divide it on some logic apply it inside this for loop
using your nodelist-->
</body>
</html>
I had a similar issue with the iteration and I landed here. Maybe someone else is also doing the same mistake I did.
In my case, the selector was not the problem at all. The problem was that I had messed up the javascript code:
I had a loop and a subloop. The subloop was also using i as a counter, instead of j, so because the subloop was overriding the value of i of the main loop, this one never got to the second iteration.
var dayContainers = document.getElementsByClassName('day-container');
for(var i = 0; i < dayContainers.length; i++) { //loop of length = 2
var thisDayDiv = dayContainers[i];
// do whatever
var inputs = thisDayDiv.getElementsByTagName('input');
for(var j = 0; j < inputs.length; j++) { //loop of length = 4
var thisInput = inputs[j];
// do whatever
};
};
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
I need to figure out how I can the child number of a text element inside of a parent that may have other elements mixed in. Here is an example case:
<p>
Here is a picture of something:
<img src="something.png"/>
Now on to other things, like <span id="highlight">this</span> thing.
</p>
I want to get the <span> element child number, which should be 3 (0-based counting). How do I go about doing this? Using JQuery's index() doesn't work because it only counts the elements and not the text, which would give a 1 in this case. Thank you for your time in looking at this.
var span = document.getElementById('highlight'),
index = Array.prototype.indexOf.call(span.parentNode.childNodes, span);
Array.prototype.indexOf will operate on the NodeList (span.parentNode.childNodes) as though it was an Array and get you the index of your span element.
You'll need a compatibility patch for .indexOf() if you're supporting IE8 and lower.
jQuery has a .contents() method that grabs all children, including text nodes (and comment nodes). You can use that to grab the span at index 3:
$('p').contents()[3]; // your span!
Then you can use .index to get the index based on a node reference:
var pContents = $('p').contents();
var span = pContents[3]; // your span
var spanIndex = pContents.index(span); // 3
http://jsfiddle.net/yERLu/1
function getIndex(node) {
var n = 0;
while (node = node.previousSibling)
n++;
return n;
}
var idx = getIndex(document.getElementById("highlight"));
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.
I would like to find all occurrence of the $ character in the dom, how is this done?
You can't do something semantic like wrap $4.00 in a span element?
<span class="money">$4.00</span>
Then you would find elements belonging to class 'money' and manipulate them very easily. You could take it a step further...
<span class="money">$<span class="number">4.00</span></span>
I don't like being a jQuery plugger... but if you did that, jQuery would probably be the way to go.
One way to do it, though probably not the best, is to walk the DOM to find all the text nodes. Something like this might suffice:
var elements = document.getElementsByTagName("*");
var i, j, nodes;
for (i = 0; i < elements.length; i++) {
nodes = elements[i].childNodes;
for (j = 0; j < nodes.length; j++) {
if (nodes[j].nodeType !== 3) { // Node.TEXT_NODE
continue;
}
// regexp search or similar here
}
}
although, this would only work if the $ character was always in the same text node as the amount following it.
You could just use a Regular Expression search on the innerHTML of the body tag:
For instance - on this page:
var body = document.getElementsByTagName('body')[0];
var dollars = body.innerHTML.match(/\$[0-9]+\.?[0-9]*/g)
Results (at the time of my posting):
["$4.00", "$4.00", "$4.00"]
The easiest way to do this if you just need a bunch of strings and don't need a reference to the nodes containing $ would be to use a regular expression on the body's text content. Be aware that innerText and textContent aren't exactly the same. The main difference that could affect things here is that textContent contains the contents of <script> elements whereas innerText does not. If this matters, I'd suggest traversing the DOM instead.
var b = document.body, bodyText = b.textContent || b.innerText || "";
var matches = bodyText.match(/\$[\d.]*/g);
I'd like to add my 2 cents for prototype. Prototype has some very simple DOM traversal functions that might get exactly what you are looking for.
edit so here's a better answer
the decendants() function collects all of the children, and their children and allows them to be enumerated upon using the each() function
$('body').descendants().each(function(item){
if(item.innerHTML.match(/\$/))
{
// Do Fun scripts
}
});
or if you want to start from document
Element.descendants(document).each(function(item){
if(item.innerHTML.match(/\$/))
{
// Do Fun scripts
}
});