Search code examples
javascriptmutation-observers

MutationObserver not picking up on child nodes


Fiddle: https://jsfiddle.net/7gj26hqu/

I would like to have one MutationObserver that can detect all new nodes within itself. In the example, I have set {childList: true, subtree: true}, but div#nested does not appear in the MutationObserver (shown in the console).

How can I have an observer detects any depth of child nodes?

const domObserver = new MutationObserver((records) => {
  records.forEach((record) => {
    console.log(record)
  })
})

domObserver.observe(document.querySelector('#frame'), {childList: true, subtree: true})

// copy child nodes out of #template (as a string) and inject them into #frame for the observer to detect
document.querySelector('#frame').innerHTML = document.querySelector('#template').innerHTML
<div id="frame"></div>

<div id="template" style="display: none;">
   <div class="level-1">
    <div class="level-2">

      <div id="nested">
        I exist in the DOM but am not being seen by the MutationObserver
      </div>

    </div>
  </div>

  <div class="level-1">
    <div class="level-2">

    </div>
  </div>

  <div class="level-1">
    <div class="level-2">

    </div>
  </div>
</div>


Solution

  • It looks like when a container that's being observed has its innerHTML set, the children of the container are emptied, and then new children get added fully intact. Using a synchronous observer (just so you can see what's happening), see how child nodes exist when the element is being appended:

    // Look at results in browser console:
    
    window.addEventListener('DOMNodeInserted', (e) => {
      console.log(e.path[0].childNodes.length);
    });
    
    document.querySelector('#frame').innerHTML = document.querySelector('#template').innerHTML
    <div id="frame"></div>
    
    <div id="template" style="display: none;">
       <div class="level-1">
        <div class="level-2">
        
          <div id="nested">
            I exist in the DOM but am not being seen by the MutationObserver
          </div>
          
        </div>
      </div>
      
      <div class="level-1">
        <div class="level-2">
        
        </div>
      </div>
      
      <div class="level-1">
        <div class="level-2">
        
        </div>
      </div>
    </div>

    The grandchild of the container being observed is attached to the child before the child gets attached to the container, so the subtree: true doesn't see the attachment action.

    To detect all child nodes inserted this way, you'll have to recursively iterate through all elements in the MutationRecord manually, despite the subtree: true.

    const recurse = (parent) => {
      
      console.log(parent);
      if (parent.childNodes) {
        [...parent.childNodes].forEach(recurse);
      }
    };
    const domObserver = new MutationObserver((records) => {
      for (const record of records) {
        for (const node of record.addedNodes) {
          recurse(node);
        }
      }
    })
    
    domObserver.observe(document.querySelector('#frame'), {childList: true, subtree: true})
    
    // copy child nodes out of #template (as a string) and inject them into #frame for the observer to detect
    document.querySelector('#frame').innerHTML = document.querySelector('#template').innerHTML
    <div id="frame"></div>
    
    <div id="template" style="display: none;">
       <div class="level-1">
        <div class="level-2">
    
          <div id="nested">
            I exist in the DOM but am not being seen by the MutationObserver
          </div>
    
        </div>
      </div>
    
      <div class="level-1">
        <div class="level-2">
    
        </div>
      </div>
    
      <div class="level-1">
        <div class="level-2">
    
        </div>
      </div>
    </div>

    Result:

    enter image description here

    If you want to use a TreeWalker instead:

    const frame = document.querySelector('#frame');
    const domObserver = new MutationObserver(() => {
      // If the container's innerHTML was assigned to, iterate over all descendants:
      const treeWalker = document.createTreeWalker(frame);
      const nodes = [];
      let currentNode = treeWalker.currentNode;
      while (currentNode) {
        nodes.push(currentNode);
        currentNode = treeWalker.nextNode();
      }
      console.log(nodes);
    });
    
    domObserver.observe(frame, {
      childList: true,
      subtree: true
    })
    
    // copy child nodes out of #template (as a string) and inject them into #frame for the observer to detect
    document.querySelector('#frame').innerHTML = document.querySelector('#template').innerHTML
    <div id="frame"></div>
    
    <div id="template" style="display: none;">
      <div class="level-1">
        <div class="level-2">
    
          <div id="nested">
            I exist in the DOM but am not being seen by the MutationObserver
          </div>
    
        </div>
      </div>
    
      <div class="level-1">
        <div class="level-2">
    
        </div>
      </div>
    
      <div class="level-1">
        <div class="level-2">
    
        </div>
      </div>
    </div>