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?
}
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.
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);
}
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);