javascriptdomdom-eventsmutation-observersmutation-events

Having a reference to an element, how to detect when it is appended to the document?


I am developing a JavaScript module, which knows nothing about the environment in which it will be used in.

And, technically speaking, I want to implement the next function:

onceAppended(element, callback);

element is an HTMLElement and the parent of this element may be unknown during the module initialization. callback is a function, which must be triggered once element appears on the page.

Callback must be called immediately if the element is appended to the document. In case element is not appended yet, function will trigger callback once element appears on the document.

The problem is, we can detect element append event with using DOMNodeInserted mutation event. But mutation events are now deprecated. And it seems that MutationObserver can't handle this task, can it?

Here is my code snippet:

function onceAppended (element, callback) {
    let el = element,
        listener;
    while (el.parentNode)
        el = el.parentNode;
    if (el instanceof Document) {
        callback();
        return;
    }
    if (typeof MutationObserver === "undefined") { // use deprecated method
        element.addEventListener("DOMNodeInserted", listener = (ev) => {
            if (ev.path.length > 1 && ev.path[ev.length - 2] instanceof Document) {
                element.removeEventListener("DOMNodeInserted", listener);
                callback();
            }
        }, false);
        return;
    }
    // Can't MutationObserver detect append event for the case?
}

Solution

  • By taking a wOxxOm's hint about the Alternative to DOMNodeInserted, and skyline3000's answer I have developed two methods of this task solution. The first method onceAppended is fast, but it has a delay of around 25ms before callback is triggered. The second method triggers callback right after the element is inserted, but it may be slow when a lot of elements append in the application.

    The solution is available on GitHub and as an npm ES6 module. Below are the plain code of the two solutions.

    Method 1 (using CSS animations)

    function useDeprecatedMethod (element, callback) {
        let listener;
        return element.addEventListener(`DOMNodeInserted`, listener = (ev) => {
            if (ev.path.length > 1 && ev.path[ev.length - 2] instanceof Document) {
                element.removeEventListener(`DOMNodeInserted`, listener);
                callback();
            }
        }, false);
    }
    
    function isAppended (element) {
        while (element.parentNode)
            element = element.parentNode;
        return element instanceof Document;
    }
    
    /**
     * Method 1. Asynchronous. Has a better performance but also has an one-frame delay after element is
     * appended (around 25ms delay) of callback triggering.
     * This method is based on CSS3 animations and animationstart event handling.
     * Fires callback once element is appended to the document.
     * @author ZitRo (https://github.com/ZitRos)
     * @see https://stackoverflow.com/questions/38588741/having-a-reference-to-an-element-how-to-detect-once-it-appended-to-the-document (StackOverflow original question)
     * @see https://github.com/ZitRos/dom-onceAppended (Home repository)
     * @see https://www.npmjs.com/package/dom-once-appended (npm package)
     * @param {HTMLElement} element - Element to be appended
     * @param {function} callback - Append event handler
     */
    export function onceAppended (element, callback) {
    
        if (isAppended(element)) {
            callback();
            return;
        }
    
        let sName = `animation`, pName = ``;
    
        if ( // since DOMNodeInserted event is deprecated, we will try to avoid using it
            typeof element.style[sName] === `undefined`
            && (sName = `webkitAnimation`) && (pName = "-webkit-")
                && typeof element.style[sName] === `undefined`
            && (sName = `mozAnimation`) && (pName = "-moz-")
                && typeof element.style[sName] === `undefined`
            && (sName = `oAnimation`) && (pName = "-o-")
                && typeof element.style[sName] === `undefined`
        ) {
            return useDeprecatedMethod(element, callback);
        }
    
        if (!document.__ONCE_APPENDED) {
            document.__ONCE_APPENDED = document.createElement('style');
            document.__ONCE_APPENDED.textContent = `@${ pName }keyframes ONCE_APPENDED{from{}to{}}`;
            document.head.appendChild(document.__ONCE_APPENDED);
        }
    
        let oldAnimation = element.style[sName];
        element.style[sName] = `ONCE_APPENDED`;
        element.addEventListener(`animationstart`, () => {
            element.style[sName] = oldAnimation;
            callback();
        }, true);
    
    }
    

    Method 2 (using MutationObserver)

    function useDeprecatedMethod (element, callback) {
        let listener;
        return element.addEventListener(`DOMNodeInserted`, listener = (ev) => {
            if (ev.path.length > 1 && ev.path[ev.length - 2] instanceof Document) {
                element.removeEventListener(`DOMNodeInserted`, listener);
                callback();
            }
        }, false);
    }
    
    function isAppended (element) {
        while (element.parentNode)
            element = element.parentNode;
        return element instanceof Document;
    }
    
    /**
     * Method 2. Synchronous. Has a lower performance for pages with a lot of elements being inserted,
     * but triggers callback immediately after element insert.
     * This method is based on MutationObserver.
     * Fires callback once element is appended to the document.
     * @author ZitRo (https://github.com/ZitRos)
     * @see https://stackoverflow.com/questions/38588741/having-a-reference-to-an-element-how-to-detect-once-it-appended-to-the-document (StackOverflow original question)
     * @see https://github.com/ZitRos/dom-onceAppended (Home repository)
     * @see https://www.npmjs.com/package/dom-once-appended (npm package)
     * @param {HTMLElement} element - Element to be appended
     * @param {function} callback - Append event handler
     */
    export function onceAppendedSync (element, callback) {
    
        if (isAppended(element)) {
            callback();
            return;
        }
    
        const MutationObserver =
            window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
    
        if (!MutationObserver)
            return useDeprecatedMethod(element, callback);
    
        const observer = new MutationObserver((mutations) => {
            if (mutations[0].addedNodes.length === 0)
                return;
            if (Array.prototype.indexOf.call(mutations[0].addedNodes, element) === -1)
                return;
            observer.disconnect();
            callback();
        });
    
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    
    }
    

    Both of this methods has the same usage, which differs only in function names:

    import { onceAppended } from "dom-once-appended"; // or onceAppendedSync
    
    function myModule () {
        let sampleElement = document.createElement("div");
        onceAppended(sampleElement, () => { // or onceAppendedSync
            console.log(`Sample element is appended!`);
        });
        return sampleElement;
    }
    
    // somewhere else in the sources (example)
    let element = myModule();
    setTimeout(() => document.body.appendChild(element), 200);