Watch for specific added nodes with MutationObserver

Last Updated

MutationObserver makes it easy to watch for the addition of specific nodes, if you know where to drill.
M0004627: Jeremiah Horrocks (1618-1641) first observing the transit of Venus
M0004627: Jeremiah Horrocks (1618-1641) first observing the transit of Venus. Public Domain Mark. Source: Wellcome Collection.

A MutationObserver interface picks up on changes to the DOM. But determining whether those changes include the addition of elements of interest takes some digging.

The MutationObserver constructor’s callback function receives an array of MutationRecords. Each MutationRecord has an addedNodes property, a NodeList of the nodes added to the view in the mutation which triggered the callback. Inspect these added nodes to see if any are ones you need to act on.

Say you want to add the attribute data-initialized to all elements with the class example. Start with a MutationObserver constructor that watches everything on the page (target of document.body, subtree: true) for the addition and removal of child nodes (childList: true)

js
js
const observer = new MutationObserver()
observer.observe(document.body, {
childList: true,
subtree: true,
});
js
const observer = new MutationObserver()
observer.observe(document.body, {
childList: true,
subtree: true,
});

Then, in the constructor’s callback function, find the nodes of interest and act on them

js
js
const observer = new MutationObserver((mutationRecords) => {
const addedNodes = mutationRecords.flatMap((mutationRecord) => {
return Array.from(mutationRecord.addedNodes);
})
const els = [];
// just the ones of interest
for (const addedNode of addedNodes) {
if (addedNode.classList?.contains("example")) {
els.push(addedNode);
}
if (!addedNode?.querySelectorAll) {
continue;
}
els.push(addedNode.querySelectorAll(".example"));
}
for (const el of els) {
// do stuff, for example
el.setAttribute("data-observed", "");
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
js
const observer = new MutationObserver((mutationRecords) => {
const addedNodes = mutationRecords.flatMap((mutationRecord) => {
return Array.from(mutationRecord.addedNodes);
})
const els = [];
// just the ones of interest
for (const addedNode of addedNodes) {
if (addedNode.classList?.contains("example")) {
els.push(addedNode);
}
if (!addedNode?.querySelectorAll) {
continue;
}
els.push(addedNode.querySelectorAll(".example"));
}
for (const el of els) {
// do stuff, for example
el.setAttribute("data-observed", "");
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});

Other patterns

Where I use the name “mutationRecords” for the array of MutationRecords, some people use “mutationList”.

If this script comes after markup, take into account elements already on the page

js
js
function initialize(els) {
for (const el of els) {
// do stuff, for example
el.setAttribute("data-observed", "");
}
}
function getChildEls(el) {
return Array.from(el.querySelectorAll(".example"))
}
const observer = new MutationObserver((mutationRecords) => {
const addedNodes = mutationRecords.flatMap((mutationRecord) => {
return Array.from(mutationRecord.addedNodes);
})
const els = []
// just the ones of interest
for (const addedNode of addedNodes) {
if (addedNode.classList?.contains("example")) {
els.push(addedNode);
}
if (!addedNode?.querySelectorAll) {
continue;
}
els.push(...getChildEls(addedNode));
}
initialize(els);
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
initialize(getChildEls(document.body));
js
function initialize(els) {
for (const el of els) {
// do stuff, for example
el.setAttribute("data-observed", "");
}
}
function getChildEls(el) {
return Array.from(el.querySelectorAll(".example"))
}
const observer = new MutationObserver((mutationRecords) => {
const addedNodes = mutationRecords.flatMap((mutationRecord) => {
return Array.from(mutationRecord.addedNodes);
})
const els = []
// just the ones of interest
for (const addedNode of addedNodes) {
if (addedNode.classList?.contains("example")) {
els.push(addedNode);
}
if (!addedNode?.querySelectorAll) {
continue;
}
els.push(...getChildEls(addedNode));
}
initialize(els);
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
initialize(getChildEls(document.body));

Here’s a CodePen for that

Generalized pattern

js
js
function initializeAndObserve() {
const root = // … e.g. `const root = document.body`
function getChildEls(el) {
return /* … e.g. `return root.querySelectorAll("[data-my-attribute]")` */
}
function initialize(els) {
for (const el of els) {
// … e.g. `el.setAttribute("data-initialized", true)`
}
}
const observer = new MutationObserver((mutationRecords) => {
const addedNodes = mutationRecords.flatMap((mutationRecord) => {
return Array.from(mutationRecord.addedNodes);
})
const els = []
// just the ones of interest
for (const addedNode of addedNodes) {
if (/* e.g. addedNode.hasAttribute("data-my-attribute")` */) {
els.push(addedNode);
}
if (!addedNode?.querySelectorAll) {
continue;
}
els.push(...getChildEls(addedNode));
}
// initialize elements of interest that were added to the page
initialize(els);
})
// observe changes to `root` and to its children
observer.observe(root, {
childList: true,
subtree: true,
});
// initialize elements of interest available at page load
initialize(getChildEls(root));
}
initializeAndObserve();
js
function initializeAndObserve() {
const root = // … e.g. `const root = document.body`
function getChildEls(el) {
return /* … e.g. `return root.querySelectorAll("[data-my-attribute]")` */
}
function initialize(els) {
for (const el of els) {
// … e.g. `el.setAttribute("data-initialized", true)`
}
}
const observer = new MutationObserver((mutationRecords) => {
const addedNodes = mutationRecords.flatMap((mutationRecord) => {
return Array.from(mutationRecord.addedNodes);
})
const els = []
// just the ones of interest
for (const addedNode of addedNodes) {
if (/* e.g. addedNode.hasAttribute("data-my-attribute")` */) {
els.push(addedNode);
}
if (!addedNode?.querySelectorAll) {
continue;
}
els.push(...getChildEls(addedNode));
}
// initialize elements of interest that were added to the page
initialize(els);
})
// observe changes to `root` and to its children
observer.observe(root, {
childList: true,
subtree: true,
});
// initialize elements of interest available at page load
initialize(getChildEls(root));
}
initializeAndObserve();

Updates

May 1, 2024: Check children of added nodes.

Articles You Might Enjoy

Or Go To All Articles