Search code examples
javascripthtmlcssdommutation-observers

Is there a way to observe a DOM mutation of an element by a CSS selector and if so, how to do so?


There is a certain website with a certain HTML element which, if I understand correctly, has the same class at DOMContentLoaded event and at load event, but sometime after the load event, this class (and possibly also the ID and also HTML attributes) get changed.

I would like to "observe" this element from the first moment it exists on DOM, so to automatically follow any change its HTML might have.

Is there a way to observe a DOM mutation of an element by a CSS selector and if so, how to do so?

I ask this assuming I understood the "observation" concept in JavaScript correctly.


Solution

  • Is there a way to observe a DOM mutation of an element by a CSS selector...

    Sort of. You can observe all modifications in an element and its descendant elements with a mutation observer, and you can use the matches method on the elements you see added or modified to see whether they match a given CSS selector (or use querySelector on the container you're watching).

    With the combination of a mutation observer and matches/querySelector(All), you can see any change you need to see.

    // Get the container
    let container = document.getElementById("container");
    
    // The selector we're interested in
    let selector = "div.foo";
    
    // Set up a mutation observer
    let observer = new MutationObserver(records => {
        // A mutation occurred within the container.
        // `records` contains the information about the mutation(s)
    
        // If you're looking to see if a new element matching the selector
        // was *added*, you can loop through the added nodes (if any)
        for (const record of records) {
            for (const added of record.addedNodes) {
                if (added.nodeType === Node.ELEMENT_NODE && added.matches(selector)) {
                    console.log("Matching element added: " + added.textContent);
                }
            }
        }
    
        // If you're looking to see if an element *changed* to match the
        // selector, you can look at the target of each record
        for (const {target} of records) {
            if (target.nodeType === Node.ELEMENT_NODE && target.matches(selector)) {
                console.log("Element changed to match: " + target.textContent);
            }
        }
    
        // Or alternatively ignore the records and *why* an element now matches,
        // and just see if any does.
        const found = container.querySelectorAll(selector);
        if (found.length) {
            console.log("Matching elements found: " + found.length);
            for (const {textContent} of found) {
                console.log("Matching element found: " + textContent);
            }
        }
    });
    observer.observe(container, {
        // Tweak what you're looking for depending on what change you want to find.
        childList: true,
        subtree: true,
        attributes: true
    });
    
    // Add five div elements; the fourth will have the
    // class we're interested in.
    let counter = 0;
    let timer = setInterval(() => {
        const div = document.createElement("div");
        ++counter;
        div.textContent = "Div #" + counter;
        if (counter === 4) {
            div.className = "foo";
        }
        console.log("Adding div: " + div.textContent);
        container.appendChild(div);
        if (counter === 5) {
            clearInterval(timer);
        }
    }, 200);
    
    // After 1200ms, *change* one of the divs to match the selector
    setTimeout(() => {
        const div = container.querySelector("div:not(.foo)");
        console.log("Changing element to match: " + div.textContent);
        div.className = "foo";
    }, 1200);
    <div id="container">
        This is the container.
    </div>

    You'll want to tweak the options you pass the observer and the code in the observer callback to match your scenario of course. :-)