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)
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
Is there a function like map() in JS that stores the returns in the original array, instead of making a new array? If not, what would be the most efficient way to do this?
Just use .forEach() instead:
myArray.forEach(function(value, index, array) {
array[index] = ... // some mutation of value
});
The exact same code would still work with .map (which invokes the callback with the same three parameters and therefore allows in-place mutation) but .forEach avoids the overhead of creating a new array, only to have it thrown away again immediately.
Note also that whilst one could just refer to myArray inside the callback, it's much more efficient not to. Having the array parameter passed to the callback allows the callback to manipulate or access the original array without requiring it to be in the lexical scope (although using this to insert or delete elements would be ill advised)
I am currently using the Chrome console to do some debugging for a Greasemonkey script.
From the console I run var opp = document.querySelectorAll('a[class="F-reset"]'); and it works as expected.
But if I try to remove the first element with opp.splice(0,1) I get the following error
Uncaught TypeError: opp.splice is not a function
at <anonymous>:2:5
at Object.InjectedScript._evaluateOn (<anonymous>:905:140)
at Object.InjectedScript._evaluateAndWrap (<anonymous>:838:34)
at Object.InjectedScript.evaluate (<anonymous>:694:21)
If I try to run opp[0].indexOf("a"), i get the same thing.
How would I fix this?
Yes, this is because the result of querySelectorAll is a node list, not an array. You can apply the slice method of Array to a node list, however:
Array.prototype.slice.call(op, 0, 1);
This works more or less as expected, because a NodeList "quacks" in just the way slice expects, i.e. it contains elements indexed sequentially. However, you should be wary of using this in general; it is much safer to simply iterate over the NodeList.
querySelectorAll returns a NodeList. This is similar to an array (it has a .length property and you can index it with []), but it's not actually an array, and doesn't have most of the array methods. If you want to use array methods on an array-like object, you have to call the method explicitly:
Array.prototype.splice.call(opp, 0, 1);
or:
[].splice.call(opp, 0, 1);
However, another difference between arrays and NodeLists is that you can't modify NodeList in place, which .splice tries to do; you can only read them like arrays. You should just use .slice() to extract the parts you want. Or convert the NodeList to an array first, and then operate on that. See
Fastest way to convert JavaScript NodeList to Array?
querySelector/All returns a NodeList not an array, so those functions are not available.
You can use call to use those array methods though
[].splice.call(opp,0,1);
The first argument is the execution context that the function will use, all other arguments are the arguments that will be passed to the function
Function call reference
First,
splice is a method of Arrays, inherited through Array.prototype, although it is intentionally generic so can be called on other Arraylike objects
querySelectorAll returns a non-live NodeList, this is not an Array and does not share any inheritance with Array, meaning you can't simply access Array methods through it
A function can be invoked with a custom this via call or apply
splice needs to be able to assign on it's this, which will fail for a NodeList as you will get the following TypeError: Cannot set property length of #<NodeList> which has only a getter
Other intentionally generic Array methods which only read from this will work on a NodeList, e.g. slice, map, indexOf, forEach, filter, some, every, etc..
Now we are in a position to do something,
Convert the NodeList to an Array and store this reference, i.e. with Array.prototype.slice
Perform your splice on this object instead
So,
var opp = document.querySelectorAll('a[class="F-reset"]'); // NodeList
oop = Array.prototype.slice.call(oop); // Array
// ...
oop.splice(0, 1);
I am using a forEach to loop through a nodeList. My code is as follows
var array = document.querySelectorAll('items');
array.forEach(function (item) {
console.log(item);
});
And this code throws an error as
Uncaught TypeError: array.forEach is not a function
Then after reading few online blog articles i changed the code to this.
[].forEach.call(array, (function (item) {
console.log(item);
}));
Could someone please explain why it is not possible to call forEach on a nodeList and what does the above second code piece do. :)
Edit: 7/25/2017
This question does not valid for modern browsers. You can use forEach on node lists in them
Although NodeList is not an Array, it is possible to iterate on it
using forEach(). It can also be converted to an Array using
Array.from().
However some older browsers have not yet implemented
NodeList.forEach() nor Array.from(). But those limitations can be
circumvented by using Array.prototype.forEach() (more in this
document).
Ref: MDN
This is a fundamental thing in JavaScript: you can take a function from one object and apply to any other object. That is: call it with this set to the object you apply the function to. It is possible, because in JavaScript all property names etc. are (plainly speaking) identified by name. So despite NodeList.length being something different then Array.length the function Array.forEach can be applied to anything that exposes property length (and other stuff that forEach requires).
So what happens in your case is that:
querySelectorAll() returns an object of type NodeList, which happens to expose length property and is enumerable (let's say it is accessible by [] operator); NodeList does not expose forEach function (as you can see i.e here: https://developer.mozilla.org/en-US/docs/Web/API/NodeList) - that's why it's impossible to call forEach directly on the results of querySelectorAll()
[].forEach returns a function - this a not so clever shortcut for Array.prototype.forEach
with [].forEach.call(array, …) this function is applied onto an object referenced by array, an object of type NodeList (that is forEach is invoked with array as this in function body, so when inside forEach there is this.length it refers to length in array despite array being NodeList and not real Array)
this works, because forEach is using properties that Array and NodeList have in common; it would fail if, i.e. forEach wanted to use some property that Array has, but NodeList has not
the NodeList object doesnt contain the method forEach, its a method of the Array object. the below code:
[].forEach.call(array, (function (item) {
console.log(item);
}));
is using the forEach method from array and passing it a NodeList.
Another option you have, and arguabiliy better, is to convert your NodeList into an array, like this:
var myArrayOfNodes = [].slice.call(NodeList);
This uses the Array objects slice method to create an array of nodes from a NodeList. This is a better aproach as you can then use an array rather then hacking an array-like object
querySelectorAll gets the element in array-like object not an Array. So you need to use as you have in second code example.
I have two DIV-elements, which are assigned an array called "stack" using data().
Both DIV-elements have a class of .trigger.
I now want to pop() the last element of both arrays like so:
$('.trigger').data("stack").pop()
However this only takes off the last element of the FIRST array, although
$('trigger').length
Returns 2.
Is there a way to remove the last element from both arrays in a single line?
You could do this:
$('.trigger').each(function() { $(this).data('stack').pop(); });
In general, jQuery functions that return a value (like ".data()" or ".css()" when passed just 1 string argument) only operate on the first element in the matched array. Thus when you want to do something like your deal, you use an explicit ".each()" to get at each element yourself.
You could use 'map' -
var arr = $('.trigger').map(function() {
return $(this).data("stack").pop()
})
Demo - http://jsfiddle.net/5gvAH/
As Pointy points out this solution works well if you need the values that were popped from each array. If you just want to pop off the values and don't need the returned values then Pointy's suggestion would be the best fit.