0

Since the forEach array method can now be called on a NodeList. I was wondering why if you make changes to the content of a childNodes NodeList while looping with forEach, forEach does not detect the changes.

grid.childNodes.forEach(function(tableRow, index, nodeList) {
  tableRow.remove();
});

Here, i'm trying to loop through my table(grid) elements children and remove each children(tableRow) from the table. However, forEach never completely loop over every element, always leaving out one last tableRow to be removed.I think it has something to do with the childNodes NodeList being live, and the forEach not being able to properly follow it or something.

Dami
  • 197
  • 3
  • 8
  • What’s your relevant html? What’s the selector for `grid`? – David Thomas Jul 07 '18 at 22:43
  • @DavidThomas `const grid = document.querySelector("table");` – Dami Jul 07 '18 at 22:46
  • You know you can just do `grid.innerHTML = ""`, right? – trincot Jul 07 '18 at 22:48
  • I’d hazard a guess that part of the problem is that the count of element children `parentNode.children` is not equal to the count of `parentNode.childNodes`. – David Thomas Jul 07 '18 at 22:48
  • Possible duplicate of [How do I remove an element in a list, using forEach?](https://stackoverflow.com/questions/6950236/how-do-i-remove-an-element-in-a-list-using-foreach) – Heretic Monkey Jul 07 '18 at 22:54
  • @HereticMonkey It is similar, but `NodeList` rather than a regular `List` makes the difference for me. The answers in the post does help, thank you. – Dami Jul 07 '18 at 23:21

1 Answers1

1

This is because, at a lower level, the .forEach() is still keeping a count and using an index of the elements. As you remove some, you are removing them from the beginning of the array/node list, and then the indexer becomes inaccurate because all the element indexes have to get shifted down by one. The element that used to be at index position 5, is now at index position 4, for example.

For something like this, it's always better to use a counting loop and remove elements starting from the last index and working backwards. This will maintain a proper count for the duration of the loop because the element to index mapping won't be modified.

Here's an example:

let grid = document.getElementById("myTable");
document.querySelector("input").addEventListener("click", function(){
  for(i = grid.childNodes.length-1; i > -1; i--) {
    grid.removeChild(grid.childNodes[i]);
  }
});
<table id="myTable">
 <tr>
   <td>Row 1</td>
 </tr>
 <tr>
   <td>Row 2</td>
 </tr>
 <tr>
   <td>Row 3</td>
 </tr>
 <tr>
   <td>Row 4</td>
 </tr>
 <tr>
   <td>Row 5</td>
 </tr> 
</table>

<input type="button" value="Delete Rows">

Now, you could also make this a little simpler if you were to use the DOM Table API and .deleteRow()

let grid = document.getElementById("myTable");
document.querySelector("input").addEventListener("click", function(){
  for(i = grid.rows.length-1; i > -1; i--) {
    grid.deleteRow(i);
  }
});
<table id="myTable">
 <tr>
   <td>Row 1</td>
 </tr>
 <tr>
   <td>Row 2</td>
 </tr>
 <tr>
   <td>Row 3</td>
 </tr>
 <tr>
   <td>Row 4</td>
 </tr>
 <tr>
   <td>Row 5</td>
 </tr> 
</table>

<input type="button" value="Delete Rows">

And, of course, you could just wipe out all of the contents of your table by clearing the .innerHTML.

let grid = document.getElementById("myTable");
document.querySelector("input").addEventListener("click", function(){      
    grid.innerHTML = "";
});
<table id="myTable">
 <tr>
   <td>Row 1</td>
 </tr>
 <tr>
   <td>Row 2</td>
 </tr>
 <tr>
   <td>Row 3</td>
 </tr>
 <tr>
   <td>Row 4</td>
 </tr>
 <tr>
   <td>Row 5</td>
 </tr> 
</table>

<input type="button" value="Delete Rows">
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
  • why starting from the last index? – Dami Jul 07 '18 at 22:50
  • @ObayanjuDamilare I've explained that in the answer. When removing from the beginning, the element that used to be index, say 5, becomes index 4 - - the index to element mapping gets thrown off. But when you remove from the end, this won't happen. – Scott Marcus Jul 07 '18 at 22:51
  • @ObayanjuDamilare You can still use `.forEach` and `.remove` by using `Array.from(grid.childNodes)` instead of `grid.childNodes`. Arrays don’t keep track of which elements are still part of the DOM like `NodeList`s and `HTMLCollection`s do. It may be worth to note that `.childNodes` produces a _live_ `NodeList` (meaning that it changes when the child nodes change) unlike other methods that return a `NodeList`. `HTMLCollection`s are always live lists. – Sebastian Simon Jul 07 '18 at 23:07
  • @ScottMarcus Thank you for your time. I understand your explanation, and the code works fine now. – Dami Jul 07 '18 at 23:17
  • Done @ScottMarcus – Dami Jul 09 '18 at 04:09