Skip to content

Instantly share code, notes, and snippets.

@jwilson8767
Last active October 10, 2024 00:35
Show Gist options
  • Save jwilson8767/db379026efcbd932f64382db4b02853e to your computer and use it in GitHub Desktop.
Save jwilson8767/db379026efcbd932f64382db4b02853e to your computer and use it in GitHub Desktop.
Wait for an element to exist. ES6, Promise, MutationObserver
// MIT Licensed
// Author: jwilson8767
/**
* Waits for an element satisfying selector to exist, then resolves promise with the element.
* Useful for resolving race conditions.
*
* @param selector
* @returns {Promise}
*/
export function elementReady(selector) {
return new Promise((resolve, reject) => {
let el = document.querySelector(selector);
if (el) {
resolve(el);
return
}
new MutationObserver((mutationRecords, observer) => {
// Query for elements matching the specified selector
Array.from(document.querySelectorAll(selector)).forEach((element) => {
resolve(element);
//Once we have resolved we don't need the observer anymore.
observer.disconnect();
});
})
.observe(document.documentElement, {
childList: true,
subtree: true
});
});
}
import { elementReady } from "es6-element-ready";
// Simple usage to delete an element if/when it exists:
elementReady('#someWidget').then((someWidget)=>{someWidget.remove();});
@jwilson8767
Copy link
Author

jwilson8767 commented Aug 31, 2024

@bezborodow Regarding performance issues, elementReady() should definitely be taken as a convenience function, I still use it occasionally but only where it is expected to run only a few times in the lifecycle of a page (such as to detect when a component's root element has been added to the page). MutationObservers as a whole aren't amazing performance-wise, really. For much better performance (while still working on arbitrary selectors), I'd actually recommend using a sort of long-polling strategy via setInterval. You could either create one setInterval per selector, or a shared setInterval and an array of outstanding selectors to match against. Here's an example of the latter:

// dict of {selector: [promise, resolve]}
const pendingElements = {};
let pendingElementsInterval;
const elementReadyBatchedFrequency = 100; // ms

/**
 * Wait for an element to be ready using a querySelector
 *
 * @param selector {string}
 * @param containerEl {Element} optional container element to search within
 * @returns {Promise<Element>}
 */
async function elementReadyBatched(selector, containerEl = document){
    let el = document.querySelector(selector);
    if (el) {
      return el;
    }
    // group outstanding requests
    if (pendingElements[selector]) {
      const [promise, _] = pendingElements[selector];
      delete pendingElements[selector];
      return promise;
    }
    let resolve;
    const promise = new Promise(r => resolve = r);
    pendingElements[selector] = [promise, resolve];
    if (!pendingElementsInterval) {
      pendingElementsInterval = setInterval(() => {
        for (const [_selector, [_, _resolve]] of Object.entries(pendingElements)) {
          const el = containerEl.querySelector(_selector);
          if (el) {
            _resolve(el);
            delete pendingElements[_selector];
          }
        }
        if (!Object.keys(pendingElements).length) {
          clearInterval(pendingElementsInterval);
          pendingElementsInterval = null;
        }
      }, elementReadyBatchedFrequency);
    }
    return pendingElements[selector][0];
}

edit: yes, I know this is very late in coming. Still <3 you.

@simonjoom
Copy link

Thanks @jwilson8767 for the last script,
it did work for me. also i m not sure about if it s more efficient than using mutationobserver..

Crazy hard to understand this script though, quite complicate.

i personaly removed the async and did a return Promise.resolve(el); for the first return.
also added a settimeout to test if the script run more than 4second to stop it in case it didnt find all matches.

@bezborodow Regarding performance issues, elementReady() should definitely be taken as a convenience function, I still use it occasionally but only where it is expected to run only a few times in the lifecycle of a page (such as to detect when a component's root element has been added to the page). MutationObservers as a whole aren't amazing performance-wise, really. For much better performance (while still working on arbitrary selectors), I'd actually recommend using a sort of long-polling strategy via setInterval. You could either create one setInterval per selector, or a shared setInterval and an array of outstanding selectors to match against. Here's an example of the latter:

// dict of {selector: [promise, resolve]}
const pendingElements = {};
let pendingElementsInterval;
const elementReadyBatchedFrequency = 100; // ms

/**
 * Wait for an element to be ready using a querySelector
 *
 * @param selector {string}
 * @param containerEl {Element} optional container element to search within
 * @returns {Promise<Element>}
 */
async function elementReadyBatched(selector, containerEl = document){
    let el = document.querySelector(selector);
    if (el) {
      return el;
    }
    // group outstanding requests
    if (pendingElements[selector]) {
      const [promise, _] = pendingElements[selector];
      delete pendingElements[selector];
      return promise;
    }
    let resolve;
    const promise = new Promise(r => resolve = r);
    pendingElements[selector] = [promise, resolve];
    if (!pendingElementsInterval) {
      pendingElementsInterval = setInterval(() => {
        for (const [_selector, [_, _resolve]] of Object.entries(pendingElements)) {
          const el = containerEl.querySelector(_selector);
          if (el) {
            _resolve(el);
            delete pendingElements[_selector];
          }
        }
        if (!Object.keys(pendingElements).length) {
          clearInterval(pendingElementsInterval);
          pendingElementsInterval = null;
        }
      }, elementReadyBatchedFrequency);
    }
    return pendingElements[selector][0];
}

edit: yes, I know this is very late in coming. Still <3 you.

@simonjoom
Copy link

Thanks @jwilson8767 for the last script,
it did work for me. also i m not sure about if it s more efficient than using mutationobserver..

Crazy hard to understand this script though, quite complicate.

i personaly removed the async and did a return Promise.resolve(el); for the first return.
also added a settimeout to test if the script run more than 4second to stop it in case it didnt find all matches.

@bezborodow Regarding performance issues, elementReady() should definitely be taken as a convenience function, I still use it occasionally but only where it is expected to run only a few times in the lifecycle of a page (such as to detect when a component's root element has been added to the page). MutationObservers as a whole aren't amazing performance-wise, really. For much better performance (while still working on arbitrary selectors), I'd actually recommend using a sort of long-polling strategy via setInterval. You could either create one setInterval per selector, or a shared setInterval and an array of outstanding selectors to match against. Here's an example of the latter:

// dict of {selector: [promise, resolve]}
const pendingElements = {};
let pendingElementsInterval;
const elementReadyBatchedFrequency = 100; // ms

/**
 * Wait for an element to be ready using a querySelector
 *
 * @param selector {string}
 * @param containerEl {Element} optional container element to search within
 * @returns {Promise<Element>}
 */
async function elementReadyBatched(selector, containerEl = document){
    let el = document.querySelector(selector);
    if (el) {
      return el;
    }
    // group outstanding requests
    if (pendingElements[selector]) {
      const [promise, _] = pendingElements[selector];
      delete pendingElements[selector];
      return promise;
    }
    let resolve;
    const promise = new Promise(r => resolve = r);
    pendingElements[selector] = [promise, resolve];
    if (!pendingElementsInterval) {
      pendingElementsInterval = setInterval(() => {
        for (const [_selector, [_, _resolve]] of Object.entries(pendingElements)) {
          const el = containerEl.querySelector(_selector);
          if (el) {
            _resolve(el);
            delete pendingElements[_selector];
          }
        }
        if (!Object.keys(pendingElements).length) {
          clearInterval(pendingElementsInterval);
          pendingElementsInterval = null;
        }
      }, elementReadyBatchedFrequency);
    }
    return pendingElements[selector][0];
}

edit: yes, I know this is very late in coming. Still <3 you.

@simonjoom
Copy link

Thanks @jwilson8767 for the last script,
it did work for me. also i m not sure about if it s more efficient than using mutationobserver..

Crazy hard to understand this script though, quite complicate.

i personaly removed the async and did a return Promise.resolve(el); for the first return.
also added a settimeout to test if the script run more than 4second to stop it in case it didnt find all matches.

@bezborodow Regarding performance issues, elementReady() should definitely be taken as a convenience function, I still use it occasionally but only where it is expected to run only a few times in the lifecycle of a page (such as to detect when a component's root element has been added to the page). MutationObservers as a whole aren't amazing performance-wise, really. For much better performance (while still working on arbitrary selectors), I'd actually recommend using a sort of long-polling strategy via setInterval. You could either create one setInterval per selector, or a shared setInterval and an array of outstanding selectors to match against. Here's an example of the latter:

// dict of {selector: [promise, resolve]}
const pendingElements = {};
let pendingElementsInterval;
const elementReadyBatchedFrequency = 100; // ms

/**
 * Wait for an element to be ready using a querySelector
 *
 * @param selector {string}
 * @param containerEl {Element} optional container element to search within
 * @returns {Promise<Element>}
 */
async function elementReadyBatched(selector, containerEl = document){
    let el = document.querySelector(selector);
    if (el) {
      return el;
    }
    // group outstanding requests
    if (pendingElements[selector]) {
      const [promise, _] = pendingElements[selector];
      delete pendingElements[selector];
      return promise;
    }
    let resolve;
    const promise = new Promise(r => resolve = r);
    pendingElements[selector] = [promise, resolve];
    if (!pendingElementsInterval) {
      pendingElementsInterval = setInterval(() => {
        for (const [_selector, [_, _resolve]] of Object.entries(pendingElements)) {
          const el = containerEl.querySelector(_selector);
          if (el) {
            _resolve(el);
            delete pendingElements[_selector];
          }
        }
        if (!Object.keys(pendingElements).length) {
          clearInterval(pendingElementsInterval);
          pendingElementsInterval = null;
        }
      }, elementReadyBatchedFrequency);
    }
    return pendingElements[selector][0];
}

edit: yes, I know this is very late in coming. Still <3 you.

@simonjoom
Copy link

Thanks @jwilson8767 for the last script,
it did work for me. also i m not sure about if it s more efficient than using mutationobserver..

Crazy hard to understand this script though, quite complicate.

i personaly removed the async and did a return Promise.resolve(el); for the first return.
also added a settimeout to test if the script run more than 4second to stop it in case it didnt find all matches.

@bezborodow Regarding performance issues, elementReady() should definitely be taken as a convenience function, I still use it occasionally but only where it is expected to run only a few times in the lifecycle of a page (such as to detect when a component's root element has been added to the page). MutationObservers as a whole aren't amazing performance-wise, really. For much better performance (while still working on arbitrary selectors), I'd actually recommend using a sort of long-polling strategy via setInterval. You could either create one setInterval per selector, or a shared setInterval and an array of outstanding selectors to match against. Here's an example of the latter:

// dict of {selector: [promise, resolve]}
const pendingElements = {};
let pendingElementsInterval;
const elementReadyBatchedFrequency = 100; // ms

/**
 * Wait for an element to be ready using a querySelector
 *
 * @param selector {string}
 * @param containerEl {Element} optional container element to search within
 * @returns {Promise<Element>}
 */
async function elementReadyBatched(selector, containerEl = document){
    let el = document.querySelector(selector);
    if (el) {
      return el;
    }
    // group outstanding requests
    if (pendingElements[selector]) {
      const [promise, _] = pendingElements[selector];
      delete pendingElements[selector];
      return promise;
    }
    let resolve;
    const promise = new Promise(r => resolve = r);
    pendingElements[selector] = [promise, resolve];
    if (!pendingElementsInterval) {
      pendingElementsInterval = setInterval(() => {
        for (const [_selector, [_, _resolve]] of Object.entries(pendingElements)) {
          const el = containerEl.querySelector(_selector);
          if (el) {
            _resolve(el);
            delete pendingElements[_selector];
          }
        }
        if (!Object.keys(pendingElements).length) {
          clearInterval(pendingElementsInterval);
          pendingElementsInterval = null;
        }
      }, elementReadyBatchedFrequency);
    }
    return pendingElements[selector][0];
}

edit: yes, I know this is very late in coming. Still <3 you.

@simonjoom
Copy link

Thanks @jwilson8767 for the last script,
it did work for me. also i m not sure about if it s more efficient than using mutationobserver..

Crazy hard to understand this script though, quite complicate.

i personaly removed the async and did a return Promise.resolve(el); for the first return.
also added a settimeout to test if the script run more than 4second to stop it in case it didnt find all matches.

@bezborodow Regarding performance issues, elementReady() should definitely be taken as a convenience function, I still use it occasionally but only where it is expected to run only a few times in the lifecycle of a page (such as to detect when a component's root element has been added to the page). MutationObservers as a whole aren't amazing performance-wise, really. For much better performance (while still working on arbitrary selectors), I'd actually recommend using a sort of long-polling strategy via setInterval. You could either create one setInterval per selector, or a shared setInterval and an array of outstanding selectors to match against. Here's an example of the latter:

// dict of {selector: [promise, resolve]}
const pendingElements = {};
let pendingElementsInterval;
const elementReadyBatchedFrequency = 100; // ms

/**
 * Wait for an element to be ready using a querySelector
 *
 * @param selector {string}
 * @param containerEl {Element} optional container element to search within
 * @returns {Promise<Element>}
 */
async function elementReadyBatched(selector, containerEl = document){
    let el = document.querySelector(selector);
    if (el) {
      return el;
    }
    // group outstanding requests
    if (pendingElements[selector]) {
      const [promise, _] = pendingElements[selector];
      delete pendingElements[selector];
      return promise;
    }
    let resolve;
    const promise = new Promise(r => resolve = r);
    pendingElements[selector] = [promise, resolve];
    if (!pendingElementsInterval) {
      pendingElementsInterval = setInterval(() => {
        for (const [_selector, [_, _resolve]] of Object.entries(pendingElements)) {
          const el = containerEl.querySelector(_selector);
          if (el) {
            _resolve(el);
            delete pendingElements[_selector];
          }
        }
        if (!Object.keys(pendingElements).length) {
          clearInterval(pendingElementsInterval);
          pendingElementsInterval = null;
        }
      }, elementReadyBatchedFrequency);
    }
    return pendingElements[selector][0];
}

edit: yes, I know this is very late in coming. Still <3 you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment