Search code examples
javascriptdomdom-eventsmutation-observersmutation

Rewrite MutationObserver() with async/await


How can I write this mutation observer code, using async/await?

I want to return true after console.log("Button is appearing...");. Could someone show me the best way to write this code?

I also need to clarify, this code is watching for a button, which appears and then disappears. And the reappears again, multiple times.

So the mutationObserver, is watching for the button to appear multiple times. Not just once.

var target = document.querySelector('[search-model="SearchPodModel"]')
var observer = new MutationObserver(mutate);

function mutate(mutations) {
    for (let i = 0; i < mutations.length; i++) {
        if (mutations[i].oldValue === "ej-button rounded-corners arrow-button search-submit holiday-search ng-hide") {
            console.log("Button is appearing...");
            return true;
        };
    };
};

var config = { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true };
observer.observe(target, config);

Solution

  • Preface: I would strongly recommend not relying on a mutation observer to watch for a button's class attribute to change. It's very much a last resort thing to do. Look for anything else you can hook into that happens which is what makes the button appear/disappear and hook into that instead.

    But getting to your question:

    Since you want repeated notifications, promises (and thus async/await) is not the right model for this. A promise is only settled once.

    There's no JavaScript built-in for it, but what you want is often called an observable and it has (typically) subscribe and unsubscribe methods. Here's a really basic, naive implementation of an observable (using modern JavaScript; run it through Babel or similar if you need to support older environments), but you may want to go looking for a library (such as Rx.js — not an endorsement, I haven't used it, just an example I happen to know about) with something more feature-rich and, you know, tested:

    class Observable {
        // Constructs the observable
        constructor(setup) {
            // Call the observable executor function, give it the function to call with
            // notifications.
            setup((spent, value) => {
                // Do the notifications
                this.#notifyObservers(spent, value);
                if (spent) {
                    // Got a notification that the observable thing is completely done and
                    // won't be providing any more updates. Release the observers.
                    this.#observers = null;
                }
            });
        }
    
        // The observers
        #observers = new Set();
    
        // Notify observers
        #notifyObservers(spent, value) {
            // Grab the current list to notify
            const observers = new Set(this.#observers);
            for (const observer of observers) {
                try { observer(spent, value); } catch { }
            }
        }
    
        // Add an observer. Returns a true if the subscription was successful, false otherwise.
        // You can't subscribe to a spent observable, and you can't subscribe twice.
        subscribe(observer) {
            if (typeof observer !== "function") {
                throw new Error("The observer must be a function");
            }
            if (this.#observers.has(observer) || !this.#observers) {
                return false;
            }
            this.#observers.add(observer);
            return true;
        }
    
        // Remove an observer. Returns true if the unsubscription was successful, false otherwise.
        unsubscribe(observer) {
            return this.#observers ? this.#observers.delete(observer) : false;
        }
    }
    

    Then you might create an observable for this mutation:

    // Create an observable for the button
    const buttonAppearedObservable = new Observable(notify => {
        const target = document.querySelector('[search-model="SearchPodModel"]');
        const observer = new MutationObserver(mutate);
    
        function mutate(mutations) {
            for (const mutation of mutations) {
                if (mutation.oldValue === "ej-button rounded-corners arrow-button search-submit holiday-search ng-hide") {
                    // Notify observers. The first argument is `false` because this observable isn't "spent" (it may still
                    // send more notifications). If you wanted to pass a value, you'd pass a second argument.
                    notify(
                        false,          // This observable isn't "spent"
                        mutation.target // Pass along the mutation target element (presumably the button?)
                    );
                };
            };
        };
    
        // Set up the observer
        const config = { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true };
        observer.observe(target, config);
    });
    

    Once you'd set that observable up, you could subscribe to it:

    buttonAppearedObservable.subscribe((spent, button) => {
        if (spent) {
            // This is a notification that the button appeared event will never happen again
        }
        if (button) {
            // The button appeared!
            console.log(`Button "${button.value}" appeared!`);
        }
    });
    

    Live Exmaple:

    class Observable {
        // Constructs the observable
        constructor(setup) {
            // Call the observable executor function, give it the function to call with
            // notifications.
            setup((spent, value) => {
                // Do the notifications
                this.#notifyObservers(spent, value);
                if (spent) {
                    // Got a notification that the observable thing is completely done and
                    // won't be providing any more updates. Release the observers.
                    this.#observers = null;
                }
            });
        }
    
        // The observers
        #observers = new Set();
    
        // Notify observers
        #notifyObservers(spent, value) {
            // Grab the current list to notify
            const observers = new Set(this.#observers);
            for (const observer of observers) {
                try { observer(spent, value); } catch { }
            }
        }
    
        // Add an observer. Returns a true if the subscription was successful, false otherwise.
        // You can't subscribe to a spent observable, and you can't subscribe twice.
        subscribe(observer) {
            if (typeof observer !== "function") {
                throw new Error("The observer must be a function");
            }
            if (this.#observers.has(observer) || !this.#observers) {
                return false;
            }
            this.#observers.add(observer);
            return true;
        }
    
        // Remove an observer. Returns true if the unsubscription was successful, false otherwise.
        unsubscribe(observer) {
            return this.#observers ? this.#observers.delete(observer) : false;
        }
    }
    
    // Create an observable for the button
    const buttonAppearedObservable = new Observable(notify => {
        const target = document.querySelector('[search-model="SearchPodModel"]');
        const observer = new MutationObserver(mutate);
    
        function mutate(mutations) {
            for (const mutation of mutations) {
                if (mutation.oldValue === "ej-button rounded-corners arrow-button search-submit holiday-search ng-hide") {
                    // Notify observers. The first argument is `false` because this observable isn't "spent" (it may still
                    // send more notifications). If you wanted to pass a value, you'd pass a second argument.
                    notify(
                        false,          // This observable isn't "spent"
                        mutation.target // Pass along the mutation target element (presumably the button?)
                    );
                };
            };
        };
    
        // Set up the observer
        const config = { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true };
        observer.observe(target, config);
    });
    
    buttonAppearedObservable.subscribe((spent, button) => {
        if (spent) {
            // This is a notification that the button appeared event will never happen again
        }
        if (button) {
            // The button appeared!
            console.log(`Button "${button.value}" appeared!`);
        }
    });
    
    // Stand-in code to make a button appear/disappear every second
    let counter = 0;
    let button = document.querySelector(`[search-model="SearchPodModel"] input[type=button]`);
    let timer = setInterval(() => {
        if (button.classList.contains("ng-hide")) {
            ++counter;
        } else if (counter >= 10) {
            console.log("Stopping the timer");
            clearInterval(timer);
            timer = 0;
            return;
        }
        button.value = `Button ${counter}`;
        button.classList.toggle("ng-hide");
    }, 500);
    .ng-hide {
        display: none;
    }
    <!-- NOTE: `search-model` isnt' a valid attribute for any DOM element. Use the data-* prefix for custom attributes -->
    <div search-model="SearchPodModel">
        <input type="button" class="ej-button rounded-corners arrow-button search-submit holiday-search ng-hide" value="The Button">
    </div>

    All of that is very off-the-cuff. Again, you might look for robust libraries, etc.