-
-
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();}); |
Thanks for this nice little function. I noticed one fairly insubstantial bug, due to how calling a Promise's resolve() or reject() function doesn't act like a return statement. Code will continue executing, and in this case, a MutationObserver will be created even if the initial querySelector call was successful. The start of the promise should read like this, taking note of the return after the resolve:
let el = document.querySelector(selector);
if (el) { resolve(el); return; }
@acropup Good point, fixed!
I would be looking to detect if one of two elements is ready, with totally different selectors. How would I go about that?
@LouisDeconinck It should be as simple as:
Promise.race([elementReady('#element1'), elementReady('#element2'), elementReady('.element3')) ]).then((matched_element)=>{
//callback function body
})
@LouisDeconinck @jwilson8767 Should work by simply concatenating with a comma same as you would to define a CSS style for two totally different selectors:
elementReady('#element1, #element2, .element3').then((el) => doSomethingWith(el))
Thank you bro!
This is querying the entire document every time a mutation occurs, rather than matching specifically against the added nodes. This may result in a performance issue.
Yes, what @bezborodow is recommending is that instead of doing a document.querySelectorAll(selector)
on line 20, it is more efficient to only try to match on the added notes in the mutationRecords
parameters (ex. check if mutationRecords[0].addedNodes[0].matches(selector)
, and repeat for all mutationRecords
and addedNodes
).
Even if performance is better, I don't think this strategy works in all cases. One reason: adding a node such as <div>This div has <strong>children</strong></div>
will put the outer div into the addedNodes
list, but none of its children will be in addedNodes
. So, matching on 'div > strong'
would not succeed. You could instead do addedNode.querySelectorAll(selector)
for all added nodes, but there are other CSS selectors for which this is insufficient. One example that comes to mind is '#parentElem:has(div > strong)'
. Even if adding the nodes would cause a valid match, #parentElem is never part of addedNodes
, because that particular node was never added. I believe using + and ~ in selectors is similarly problematic.
tl;dr Referring to addedNodes
might be more performant, but it can fail to match in some scenarios. document.querySelectorAll might be inefficient, but it is always correct.
@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.
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
@ivantacca What you're looking for is probably just to use the MutationObserver to give you all the matching elements, either once or perhaps periodically as the page changes. This is a bit different from what elementReady does, which is to give a single element as soon as it's added to the DOM. The first, and simplest way to wait for multiple elements to exist (when you know what their ids are) is to just call elementReady more than once and use Promise.all to wait for all them to exist:
The above only resolves once, so it's good for waiting during a page load where you know what elements need to load, and you don't need to repeat the callback function. For a more complicated case where you want to watch the entire page for new elements matching some selector, and periodically trigger a callback, I recommend the following approach:
Sorry if that doesn't work out of the box, I didn't have time to test it fully.
If you want a more robust solution for dealing with this sort of thing, check out the "Observer" pattern and check out RXJS. Since I originally wrote
elementReady.js
I have switched a large portion of my projects over to using an RXJS as it lets me synchronize user interactions, state changes, and component render cycles without creating hugely jumbled code.