javascripthtmlselectors-api

querySelectorAll not working


I have a requirement where I have to pickup the last .div within a container and apply some business logic to it. The selection of the last .div has to be dynamic because the user has the option to add/remove .div elements.

Initially I tried with querySelectorAll but it did not seem to work. So I decided to change it to getElementsByClassName and surprisingly it worked with the same logic. Can somebody please help me with the reason for why the remove_div doesn't work while the second one (remove_div_2) does?

Note: I am not looking for a fix/solution to the issue because I have already proceeded with the second option. I just want to know the reason why the option with querySelectorAll doesn't work.

Below is my code:

HTML:

<div id='container'>
    <div id='div1' class='div'>This is Div 1</div>
    <div id='div2' class='div'>This is Div 2</div>
    <div id='div3' class='div'>This is Div 3</div>
</div>
<button type='button' id='append_div'>Append Div</button>
<button type='button' id='remove_div'>Remove Div</button>
<button type='button' id='remove_div_2'>Remove Div 2</button>

JavaScript:

window.onload = function () {
    var elementToStyle = document.querySelectorAll("#container .div");
    elementToStyle[elementToStyle.length - 1].classList.add('red');

    document.getElementById('append_div').onclick = function () {
       var divToInsert = document.createElement('div');
       divToInsert.id = 'new_div';
       divToInsert.className = 'div';
       divToInsert.innerHTML = 'This is an appended div';
       document.getElementById('container').appendChild(divToInsert);
       var elToStyle = document.querySelectorAll("#container .div");
       for (i = 0; i < elToStyle.length; i++)
           elToStyle[i].classList.remove('red');
           elToStyle[elToStyle.length - 1].classList.add('red');
       };

       document.getElementById('remove_div').onclick = function () {
           var elToStyle = document.querySelectorAll("#container .div");
           document.getElementById('container').removeChild(elToStyle[elToStyle.length - 1]);
           elToStyle[elToStyle.length - 1].classList.add('red');
       };

       document.getElementById('remove_div_2').onclick = function () {
           var elToStyle = document.getElementsByClassName('div');
           document.getElementById('container').removeChild(elToStyle[elToStyle.length - 1]);
           elToStyle[elToStyle.length - 1].classList.add('red');
       };
   }

Solution

  • The reason is because querySelectorAll method returns a static list. Any changes made to the document after the querySelectorAll is used (like removeChild in this case) will not be reflected in the list of nodes returned. Hence elToStyle[elToStyle.length - 1] would still point to the node that was removed.

    Whereas, getElementsByClassName on the other hand returns a live list of nodes. This implies that elToStyle[elToStyle.length - 1] would always point to the last .div irrespective of any changes were done to the document after the node list was prepared or not.

    Below is the extract from the official documentation available here

    The NodeList object returned by the querySelectorAll() method must be static, not live ([DOM-LEVEL-3-CORE], section 1.1.1). Subsequent changes to the structure of the underlying document must not be reflected in the NodeList object. This means that the object will instead contain a list of matching Element nodes that were in the document at the time the list was created.

    Note: You can see this by doing a console.log(elToStyle); both before and after the removeChild.