-
-
Save jwilson8767/db379026efcbd932f64382db4b02853e to your computer and use it in GitHub Desktop.
// 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();}); |
section:has(div > strong)
example using closest()
. Adjacent and sibling combinators can also be implemented, but the important aspect in terms of performance is having a tokeniser to introspect and recognise which algorithm to pick based on the complexity of the selector. Although even without that, this should represent a substantial improvement.
const element = (addedNode.matches(selector) && addedNode)
|| addedNode.querySelector(selector)
|| addedNode.closest(selector);
ok boo
Is it possible to reuse the same MutationObserver multiple times without calling disconnect() on it? If the MutationObserver is observing the document, then if I am able to reuse it for all new node creations on the document, i wont have to keep creating/disconnecting it.
@precogtyrant MutationObserver's themselves with a callback function are probably your best bet then instead of using elementReady()
since promises can by nature only be resolved once. If you want more robust "reactive" / "observable" behavior, I can't recommend RXJS enough. I've used it to build entire apps, and it's really a game changer.
@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.
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.
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.
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.
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.
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.
@acropup, an alternative solution is to match on the unique ID only, and not use selectors. This would be intuitive, since the promise resolves after the first match is found.
However, if there was a way to reliably tokenise a selector, it could be found through introspection what kind combinators are present and adjust the match/query algorithm accordingly. There is a
css-selector-tokenizer
package, but that seems overkill for something that should be easily achievable.