I'd like to detect when a specific DOMNode is added to the document, from within the context of the creation of that DOMNode.
Here's what I have so far:
function getThingThatFormatsItself() {
const MY_NODE = createNode();
const observer = new MutationObserver(function (records) {
records.forEach(record => {
record.addedNodes.forEach(n => {
if (n === MY_NODE) {
observer.disconnect();
doFormatting();
}
});
})
});
observer.observe(document, {childList: true, subtree: true});
// do formatting stuff that relies on the element being in DOM
function doFormatting() {
console.log(`It's been added.`);
}
return MY_NODE;
}
/* ELSEWHERE IN MY CODE */
// Now that Thing is added to DOM, it can format itself.
// However, this doesn't work unless it's added via setTimeout!
// Also, in the meantime, we are needlessly iterating through every single node added to the document.
$("#Foo").append(getThingThatFormatsItself());
Two issues with this:
setTimeout
before adding thingNode to the document. It appears .observe()
doesn't take effect immediately. Is this true?Is there a way to see when my node is added without having to rely on external callers using setTimeout, and without having to iterate through every single added node in the meantime?
It's really "confusing", to put it nicely, that I can't observe the actual node itself for addition and removal -- only its children nodes. Some design. It's also quite "confusing" that .observe()
seems to be put on the event queue rather than executed immediately.
MutationObserver callback runs at the end of an event loop cycle during the microtask queue processing phase which occurs after the main code phase has completed which is why doFormatting() is called after the currently running code completes (the entire function call stack so to speak).
Unless there's something else in your other code that makes assumptions on doFormatting being called in the current event loop or depends on DOM layout being updated it should be more or less the same as using setTimeout which schedules the callback to run in the next event loop cycle.
The reason MutationObserver accumulates batches of mutations and reports them all in the microtask queue is to provide a much faster observation capability compared to the deprecated synchronous DOM Mutation Events.
Solution 1: use callbacks to run code after doFormatting()
function onNodeAdopted(node, callback) {
new MutationObserver((mutations, observer) => {
if (node.parentNode) {
observer.disconnect();
callback(node);
}
}).observe(document, {childList: true, subtree: true});
return node;
}
function getThingThatFormatsItself(callback) {
return onNodeAdopted(createNode(), node => {
doFormatting(node);
console.log('Formatted');
callback(node);
});
}
$("#Foo").append(getThingThatFormatsItself(node => {
console.log('This runs after doFormatting()');
doMoreThings();
}));
console.log('This runs BEFORE doFormatting() as MutationObserver is asynchronous')
Solution 2: don't use MutationObserver, instead intercept Node.prototype.appendChild:
const formatOnAppend = (() => {
const hooks = new Map();
let appendChild;
function appendChildHook(node) {
appendChild.call(this, node);
const fn = hooks.get(node);
if (fn) {
hooks.delete(node);
// only restore if no one chained later
if (!hooks.size && Node.prototype.appendChild === appendChildHook) {
Node.prototype.appendChild = appendChild;
}
fn(node);
}
return node;
}
return {
register(node, callback) {
if (!hooks.size) {
appendChild = Node.prototype.appendChild;
Node.prototype.appendChild = appendChildHook;
}
hooks.set(node, callback);
return node;
},
}
})();
Usage:
function getThingThatFormatsItself() {
return formatOnAppend.register(createNode(), node => {
console.log('%o is added', node);
});
}
Other things to try: window.queueMicrotask(callback) instead of setTimeout to enqueue some of the dependent code in the microtask queue. For older browsers there's a simple polyfill right there in the article.
Check document.contains(MY_NODE)
(won't help if inside ShadowDOM) or MY_NODE.parentNode
instead of enumerating the mutations:
new MutationObserver((mutations, observer) => {
if (MY_NODE.parentNode) {
observer.disconnect();
doFormatting();
}
}).observe(document, {childList: true, subtree: true});
This is also more reliable because in a general case the node may be a child of another added node, not as as a separate item in the addedNodes array.