How can I efficiently respond to a single node being added or removed from the DOM?

Viewed 190

I have a node which is being added to the DOM is some unknown place. I have no idea what the parent will be, and the only assumption I can make is that it'll be in the subtree of document.body.

Further, I can not assume the node will be added as itself; it might be hidden in the subtree of some other element when it enters the DOM.

I would like to have a callback occur when the element is removed from the DOM and when it is added.

I tried using Mutation Observer but it is the wrong tool for the job. Mutation Observers can not observe a specific element, but instead the children of one. Given that I don't know what the parent element will be, I can't observe the parent for the addition of this one specific child.


So far, the only solution I've been able to find is to use a mutation observer on THE ENTIRE DOM starting from document.body with subtree, and then iterate through the entire subtree of every added node searching for the one node I'm looking for.

The next best solution I have is to check every node I'm trying to observe for being on the page anytime anything is added or removed. The big issue with this one is that it requires holding references to potentially deprecated HTMLELements, and would end up blocking the garbage collector.

Neither of these approaches is efficient.

Surely, there must be a better way? This can't be that hard of a problem, can it?

function onElementAdd(node, cb);
function onElementRemove(node, cb);
2 Answers

I first tried to hook all the functions and properties of the element to see if any fire when the element is added, but no luck.

Then I tried a proxy and the MutationObserver with no luck either.

Now this solution uses some super hacky solution that I love.

It adds a hidden image to the element that fires a callback only when it is added to the body's dom. Thats the added callback. Once its shown it adds a observer to the parent and fires the remove callback once the element no longer has a parent node. Adjust it to your needs.

function addLazyImage(el)
{
    let img = document.createElement("img");

    img.setAttribute("loading", "lazy");

    img.width = 0;
    img.height = 0;

    img.src = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png";
    
    el.appendChild(img);

    return img;
}


function monitorElement(el, added, removed)
{
    let img = addLazyImage(el);

    img.onload = function()
    {
        // The element has been added to the visible dom

        const observer = new MutationObserver(function(e)
        {
            console.log("event happened");
    
            // If the element no longer has a parent, assume its been removed
            if(el.parentNode == null)
            {
                // The lazy loading only happens once, recreate the image element
                // every time its used
                let onload = img.onload;

                img = addLazyImage(el);

                img.onload = onload;

                removed();
            }
        });
    
        observer.observe(el.parentNode, {subtree: true, childList: true});
        
        added();
    }
}



$(document).ready(function()
{
    let el = document.createElement("div");

    el.innerHTML = "you wot";

    monitorElement(el, () =>
    {
        console.log("Im in the dom");
    }, () =>
    {
        console.log("Im not in the dom");
    });

    setTimeout(function()
    {
        console.log("appending element to body");

        document.body.appendChild(el);
        
    }, 1000);

    setTimeout(function()
    {
        console.log("Removing from the body");

        document.body.removeChild(el);

    }, 2000);

    setTimeout(function()
    {
        console.log("appending element to another element");

        document.querySelector("#div-container").appendChild(el);

    }, 3000);


        
    
});

It's not efficient, but this is the best I've got:

const div = document.createElement("div");
div.textContent = "Hello";

observeNodeAddRemove(div, (added) => {
  console.log(`div added: ${added}`);
});

setTimeout(() => document.body.append(div), 50);
setTimeout(() => div.remove(), 500);



//////-------------------------

function observeNodeAddRemove(domNode, cb) {
  assertMutationObserver();
  domNode._addRemoveCb = cb;
}


// group together any mutations which happen within the same 10ms
var mutationsBuffer = [];  //: MutationRecord[]
var bufferTimeout;
var mutationObserver; //: MutationObserver
function assertMutationObserver() {
  if (mutationObserver) { return; }
  if (typeof MutationObserver === "undefined") { return; } // Node JS testing
  if (typeof window !== "undefined") { return; } // Node JS testing
  mutationObserver = new MutationObserver((mutationsList) => {
    mutationsList.forEach((mutation) => {
      if (mutation.type !== 'childList') { return; }
      mutationsBuffer.push(mutation);
    });
    
    // timeout/buffer stops thrashing from happening
    if (bufferTimeout) { clearTimeout(bufferTimeout); }
    bufferTimeout = setTimeout(() => {
      bufferTimeout = undefined;
      const oldBuffer = mutationsBuffer;
      mutationsBuffer = [];

      // every node that's been added or removed
      const allNodes = new Map(); //<Node, void>
      for (const mutation of oldBuffer) {
        mutation.removedNodes.forEach((node) => allNodes.set(node));
        mutation.addedNodes.forEach((node) => allNodes.set(node));
      }

      // function for traversing sub tree
      // (root: Node, cb: (node: Node) => any) => {
      const permeate = (root, cb) => {
        cb(root);
        root.childNodes.forEach((child) => permeate(child, cb));
      }

      
      const nodeList = Array.from(allNodes.keys());
      nodeList.forEach((root) => permeate(root, (child) => {
        if (child._addRemoveCb) {
          const onPage = child.getRootNode() === document;
          child._addRemoveCb(onPage);
        }
      }));
    }, 10);
  });

  var config = { childList: true, subtree: true };
  const tryAddObserver = () => mutationObserver.observe(document.body, config);
  if (document && document.body) { 
    tryAddObserver(); 
  } else {
    document.addEventListener("DOMContentLoaded", () => tryAddObserver());
  }
}

Related