Skip to content
Advertisement

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);

Advertisement

Answer

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.

User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement