3

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);
Seph Reed
  • 8,797
  • 11
  • 60
  • 125
  • Wow. Apparently I asked this same question 4 years ago: https://stackoverflow.com/questions/40364156/what-is-the-new-equivalent-of-domnodeinserted – Seph Reed Jan 05 '21 at 23:06
  • Mutationobservers seem like exactly what you are asking for, given you watch the subtree of document.body. Don't the mutation records your receive hold all the required information? – connexo Jan 05 '21 at 23:07
  • You have to iterate over the entire subtree of every added element searching for the one. It's ridiculously inefficient. – Seph Reed Jan 05 '21 at 23:08
  • 1
    No, you have MutationRecord.addedNodes/removedNodes, see https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord – connexo Jan 05 '21 at 23:09
  • That's for the nodes added to the target. Has nothing to do with adding or removing the target itself. – Seph Reed Jan 05 '21 at 23:09
  • Then I really don't get what you are asking. – connexo Jan 05 '21 at 23:10
  • https://stackoverflow.com/questions/65587654/can-mutations-observer-be-used-to-observe-the-addition-and-removal-of-a-single-n – Seph Reed Jan 05 '21 at 23:10
  • Well, custom elements have connectedCallback and disconnectedCallback, is that what you are looking for in any node? – connexo Jan 05 '21 at 23:12
  • Yes, but not for custom elements. Just a general div. – Seph Reed Jan 05 '21 at 23:14
  • 1
    You can extend a general `div` which will give you access to those callbacks. Interesting to learn where these callbacks come from, since you can even do `super.connectedCallback()` inside those. I suppose these are only exposed if extended. – connexo Jan 05 '21 at 23:15
  • How about you just bind the listener to the parent of the node you want to monitor? – John Jan 05 '21 at 23:16
  • @John They have no information on that parent. – connexo Jan 05 '21 at 23:16
  • @SephReed do you have access to the script that is inserting the element? – John Jan 05 '21 at 23:27
  • No, it's a generated element which is passed off to any number of projects. – Seph Reed Jan 05 '21 at 23:35
  • Is it a specific element? – connexo Jan 06 '21 at 00:02
  • It is not. It's generated by the user, it could be literally any element at all. – Seph Reed Jan 06 '21 at 00:20
  • So do your users pass it inside some function of yours where you can add your observer? Otherwise, how do you recognize it's the proper element? – Kaiido Jan 06 '21 at 02:13
  • If there was an observer to add, yes. But in this case the element has a hidden property to identify it. If I could directly observe it, that would be preferable, but I don't want to hold a reference to it because that will block the garbage collector. – Seph Reed Jan 06 '21 at 02:29
  • Ok... so you just want a [WeakRef](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef). – Kaiido Jan 06 '21 at 03:06

2 Answers2

1

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);


        
    
});
John
  • 5,942
  • 3
  • 42
  • 79
  • 1
    You don't even have to make a network request. Instead of `onload` listen for `onerror` and set the `src` to the empty string. – Kaiido Jan 06 '21 at 02:22
  • @Kaiido nice shortcut – John Jan 06 '21 at 02:29
  • This is a cool solution for most projects. One drawback here is that any elements that are monitored can never be garbage collected. There will always be a reference to them in the mutation observer. Also, those mutation observers stack up. And moving the element I think might break the removal mutation observer. – Seph Reed Jan 06 '21 at 02:35
  • 1
    @SephReed you'd just have to call `observer.disconnect()` when the element is removed from the DOM for `el` to be collectable again. BTW, John, another issue here, don't look at `el.parentNode` to tell you if the element is still in the DOM, they could have removed the grandparent and that check would fail. Instead there is a [`Node.isConnected`](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) property if we want to know globally in the DOM, or maybe OP wants more something like [`document.body.contains(el)`](https://developer.mozilla.org/en-US/docs/Web/API/Node/contains) – Kaiido Jan 06 '21 at 03:03
0

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());
  }
}
Seph Reed
  • 8,797
  • 11
  • 60
  • 125