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.