I'm working on an Extension in Chrome, and I'm wondering: how do I find out when an element comes into existence? Using plain JavaScript, with an interval that checks until an element exists, or does jQuery have some easy way to do this?
33 Answers
Here is a simple solution using the MutationObserver api.
- No
jQuery - No
Timer - No third party libraries
Promisebased and works well withasync/await
I have used it in several projects.
function waitForElm(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
To use it:
waitForElm('.some-class').then((elm) => {
console.log('Element is ready');
console.log(elm.textContent);
});
Or with async/await:
const elm = await waitForElm('.some-class');
28 Comments
async / await too. You might also be able squeeze more performance out of it by doing mutations.addedNodes.find(node => node.matchesSelector("..."))mutations param is not used in the code and can be safely deleted. It has a lot of useful information on what is mutated. I put it there just in case you need to access it.noConflict()?DOMNodeInserted is being deprecated, along with the other DOM mutation events, because of performance issues - the recommended approach is to use a MutationObserver to watch the DOM. It's only supported in newer browsers though, so you should fall back onto DOMNodeInserted when MutationObserver isn't available.
let observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (!mutation.addedNodes) return
for (let i = 0; i < mutation.addedNodes.length; i++) {
// do things to your newly added nodes here
let node = mutation.addedNodes[i]
}
})
})
observer.observe(document.body, {
childList: true
, subtree: true
, attributes: false
, characterData: false
})
// stop watching using:
observer.disconnect()
6 Comments
if (mutation.addedNodes.length) since if (mutation.addedNodes) would still return true even if it's an empty array. (2) You can't do mutation.addedNodes.forEach() because addedNodes is a nodeList and you can't iterate through a nodeList with forEach. For a solution to this, see toddmotto.com/ditch-the-array-foreach-call-nodelist-hackHere is a core JavaScript function to wait for the display of an element (well, its insertion into the DOM to be more accurate).
// Call the below function
waitForElementToDisplay("#div1",function(){alert("Hi");},1000,9000);
function waitForElementToDisplay(selector, callback, checkFrequencyInMs, timeoutInMs) {
var startTimeInMs = Date.now();
(function loopSearch() {
if (document.querySelector(selector) != null) {
callback();
return;
}
else {
setTimeout(function () {
if (timeoutInMs && Date.now() - startTimeInMs > timeoutInMs)
return;
loopSearch();
}, checkFrequencyInMs);
}
})();
}
This call will look for the HTML tag whose id="div1" every 1000 milliseconds. If the element is found, it will display an alert message Hi. If no element is found after 9000 milliseconds, this function stops its execution.
Parameters:
selector: String : This function looks for the element ${selector}.callback: Function : This is a function that will be called if the element is found.checkFrequencyInMs: Number : This function checks whether this element exists every ${checkFrequencyInMs} milliseconds.timeoutInMs: Number : Optional. This function stops looking for the element after ${timeoutInMs} milliseconds.
NB : Selectors are explained at https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
10 Comments
querySelector? developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorI was having this same problem, so I went ahead and wrote a plugin for it.
$(selector).waitUntilExists(function);
Code:
;(function ($, window) {
var intervals = {};
var removeListener = function(selector) {
if (intervals[selector]) {
window.clearInterval(intervals[selector]);
intervals[selector] = null;
}
};
var found = 'waitUntilExists.found';
/**
* @function
* @property {object} jQuery plugin which runs handler function once specified
* element is inserted into the DOM
* @param {function|string} handler
* A function to execute at the time when the element is inserted or
* string "remove" to remove the listener from the given selector
* @param {bool} shouldRunHandlerOnce
* Optional: if true, handler is unbound after its first invocation
* @example jQuery(selector).waitUntilExists(function);
*/
$.fn.waitUntilExists = function(handler, shouldRunHandlerOnce, isChild) {
var selector = this.selector;
var $this = $(selector);
var $elements = $this.not(function() { return $(this).data(found); });
if (handler === 'remove') {
// Hijack and remove interval immediately if the code requests
removeListener(selector);
}
else {
// Run the handler on all found elements and mark as found
$elements.each(handler).data(found, true);
if (shouldRunHandlerOnce && $this.length) {
// Element was found, implying the handler already ran for all
// matched elements
removeListener(selector);
}
else if (!isChild) {
// If this is a recurring search or if the target has not yet been
// found, create an interval to continue searching for the target
intervals[selector] = window.setInterval(function () {
$this.waitUntilExists(handler, shouldRunHandlerOnce, true);
}, 500);
}
}
return $this;
};
}(jQuery, window));
11 Comments
window.setInterval). I don't know if the MutationObserver answer works by polling as well...; in the beginning of the function ( ;(function ($, window) { ) ?I used this approach to wait for an element to appear so I can execute the other functions after that.
Let's say doTheRestOfTheStuff(parameters) function should only be called after the element with ID the_Element_ID appears or finished loading, we can use,
var existCondition = setInterval(function() {
if ($('#the_Element_ID').length) {
console.log("Exists!");
clearInterval(existCondition);
doTheRestOfTheStuff(parameters);
}
}, 100); // check every 100ms
Comments
Update
Below there is an updated version that works with promises. It also "stops" if a specific number of tries is reached.
function _waitForElement(selector, delay = 50, tries = 100) {
const element = document.querySelector(selector);
if (!window[`__${selector}`]) {
window[`__${selector}`] = 0;
window[`__${selector}__delay`] = delay;
window[`__${selector}__tries`] = tries;
}
function _search() {
return new Promise((resolve) => {
window[`__${selector}`]++;
setTimeout(resolve, window[`__${selector}__delay`]);
});
}
if (element === null) {
if (window[`__${selector}`] >= window[`__${selector}__tries`]) {
window[`__${selector}`] = 0;
return Promise.resolve(null);
}
return _search().then(() => _waitForElement(selector));
} else {
return Promise.resolve(element);
}
}
Usage is very simple, to use it with await just make sure you're within an
async function:
const start = (async () => {
const $el = await _waitForElement(`.my-selector`);
console.log($el);
})();
Outdated version
Simply add the selector you want. Once the element is found you can have access to in the callback function.
const waitUntilElementExists = (selector, callback) => {
const el = document.querySelector(selector);
if (el){
return callback(el);
}
setTimeout(() => waitUntilElementExists(selector, callback), 500);
}
waitUntilElementExists('.wait-for-me', (el) => console.log(el));
4 Comments
I think that still there isnt any answer here with easy and readable working example. Use MutationObserver interface to detect DOM changes, like this:
var observer = new MutationObserver(function(mutations) {
if ($("p").length) {
console.log("Exist, lets do something");
observer.disconnect();
//We can disconnect observer once the element exist if we dont want observe more changes in the DOM
}
});
// Start observing
observer.observe(document.body, { //document.body is node target to observe
childList: true, //This is a must have for the observer with subtree
subtree: true //Set to true if changes must also be observed in descendants.
});
$(document).ready(function() {
$("button").on("click", function() {
$("p").remove();
setTimeout(function() {
$("#newContent").append("<p>New element</p>");
}, 2000);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button>New content</button>
<div id="newContent"></div>
Note: Spanish Mozilla docs about
MutationObserverare more detailed if you want more information.
1 Comment
You can do
$('#yourelement').ready(function() {
});
Please note that this will only work if the element is present in the DOM when being requested from the server. If the element is being dynamically added via JavaScript, it will not work and you may need to look at the other answers.
6 Comments
.ready() function works for most anything (if not anything), not just document. It just won't work with dynamically created elements, even on .live().$(document).ready(), not the element you think it will apply too. That's just how this special listener works. ExampleYou can listen to DOMNodeInserted or DOMSubtreeModified events which fire whenever a new element is added to the DOM.
There is also LiveQuery jQuery plugin which would detect when a new element is created:
$("#future_element").livequery(function(){
//element created
});
3 Comments
DOMSubtreeModified is deprecated in favor of the Mutation Observer APIFor a simple approach using jQuery I've found this to work well:
// Wait for element to exist.
function elementLoaded(el, cb) {
if ($(el).length) {
// Element is now loaded.
cb($(el));
} else {
// Repeat every 500ms.
setTimeout(function() {
elementLoaded(el, cb)
}, 500);
}
};
elementLoaded('.element-selector', function(el) {
// Element is ready to use.
el.click(function() {
alert("You just clicked a dynamically inserted element");
});
});
Here we simply check every 500ms to see whether the element is loaded, when it is, we can use it.
This is especially useful for adding click handlers to elements which have been dynamically added to the document.
Comments
Here's a function that acts as a thin wrapper around MutationObserver. The only requirement is that the browser support MutationObserver; there is no dependency on JQuery. Run the snippet below to see a working example.
function waitForMutation(parentNode, isMatchFunc, handlerFunc, observeSubtree, disconnectAfterMatch) {
var defaultIfUndefined = function(val, defaultVal) {
return (typeof val === "undefined") ? defaultVal : val;
};
observeSubtree = defaultIfUndefined(observeSubtree, false);
disconnectAfterMatch = defaultIfUndefined(disconnectAfterMatch, false);
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes) {
for (var i = 0; i < mutation.addedNodes.length; i++) {
var node = mutation.addedNodes[i];
if (isMatchFunc(node)) {
handlerFunc(node);
if (disconnectAfterMatch) observer.disconnect();
};
}
}
});
});
observer.observe(parentNode, {
childList: true,
attributes: false,
characterData: false,
subtree: observeSubtree
});
}
// Example
waitForMutation(
// parentNode: Root node to observe. If the mutation you're looking for
// might not occur directly below parentNode, pass 'true' to the
// observeSubtree parameter.
document.getElementById("outerContent"),
// isMatchFunc: Function to identify a match. If it returns true,
// handlerFunc will run.
// MutationObserver only fires once per mutation, not once for every node
// inside the mutation. If the element we're looking for is a child of
// the newly-added element, we need to use something like
// node.querySelector() to find it.
function(node) {
return node.querySelector(".foo") !== null;
},
// handlerFunc: Handler.
function(node) {
var elem = document.createElement("div");
elem.appendChild(document.createTextNode("Added node (" + node.innerText + ")"));
document.getElementById("log").appendChild(elem);
},
// observeSubtree
true,
// disconnectAfterMatch: If this is true the hanlerFunc will only run on
// the first time that isMatchFunc returns true. If it's false, the handler
// will continue to fire on matches.
false);
// Set up UI. Using JQuery here for convenience.
$outerContent = $("#outerContent");
$innerContent = $("#innerContent");
$("#addOuter").on("click", function() {
var newNode = $("<div><span class='foo'>Outer</span></div>");
$outerContent.append(newNode);
});
$("#addInner").on("click", function() {
var newNode = $("<div><span class='foo'>Inner</span></div>");
$innerContent.append(newNode);
});
.content {
padding: 1em;
border: solid 1px black;
overflow-y: auto;
}
#innerContent {
height: 100px;
}
#outerContent {
height: 200px;
}
#log {
font-family: Courier;
font-size: 10pt;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h2>Create some mutations</h2>
<div id="main">
<button id="addOuter">Add outer node</button>
<button id="addInner">Add inner node</button>
<div class="content" id="outerContent">
<div class="content" id="innerContent"></div>
</div>
</div>
<h2>Log</h2>
<div id="log"></div>
Comments
How about the insertionQuery library?
insertionQuery uses CSS Animation callbacks attached to the selector(s) specified to run a callback when an element is created. This method allows callbacks to be run whenever an element is created, not just the first time.
From github:
Non-dom-event way to catch nodes showing up. And it uses selectors.
It's not just for wider browser support, It can be better than DOMMutationObserver for certain things.
Why?
- Because DOM Events slow down the browser and insertionQuery doesn't
- Because DOM Mutation Observer has less browser support than insertionQuery
- Because with insertionQuery you can filter DOM changes using selectors without performance overhead!
Widespread support!
IE10+ and mostly anything else (including mobile)
Comments
Here's a Promise-returning solution in vanilla Javascript (no messy callbacks). By default it checks every 200ms.
function waitFor(selector) {
return new Promise(function (res, rej) {
waitForElementToDisplay(selector, 200);
function waitForElementToDisplay(selector, time) {
if (document.querySelector(selector) != null) {
res(document.querySelector(selector));
}
else {
setTimeout(function () {
waitForElementToDisplay(selector, time);
}, time);
}
}
});
}
Comments
You can try this:
const wait_until_element_appear = setInterval(() => {
if ($(element).length !== 0) {
// some code
clearInterval(wait_until_element_appear);
}
}, 0);
This solution works very good for me
1 Comment
Here's a pure Javascript function which allows you to wait for anything. Set the interval longer to take less CPU resource.
/**
* @brief Wait for something to be ready before triggering a timeout
* @param {callback} isready Function which returns true when the thing we're waiting for has happened
* @param {callback} success Function to call when the thing is ready
* @param {callback} error Function to call if we time out before the event becomes ready
* @param {int} count Number of times to retry the timeout (default 300 or 6s)
* @param {int} interval Number of milliseconds to wait between attempts (default 20ms)
*/
function waitUntil(isready, success, error, count, interval){
if (count === undefined) {
count = 300;
}
if (interval === undefined) {
interval = 20;
}
if (isready()) {
success();
return;
}
// The call back isn't ready. We need to wait for it
setTimeout(function(){
if (!count) {
// We have run out of retries
if (error !== undefined) {
error();
}
} else {
// Try again
waitUntil(isready, success, error, count -1, interval);
}
}, interval);
}
To call this, for example in jQuery, use something like:
waitUntil(function(){
return $('#myelement').length > 0;
}, function(){
alert("myelement now exists");
}, function(){
alert("I'm bored. I give up.");
});
Comments
The observe function below will allow you to listen to elements via a selector.
In the following example, after 2 seconds have passed, a .greeting will be inserted into the .container. Since we are listening to the insertion of this element, we can have a callback that triggers upon insertion.
const observe = (selector, callback, targetNode = document.body) =>
new MutationObserver(mutations => [...mutations]
.flatMap((mutation) => [...mutation.addedNodes])
.filter((node) => node.matches && node.matches(selector))
.forEach(callback))
.observe(targetNode, { childList: true, subtree: true });
const createGreeting = () => {
const el = document.createElement('DIV');
el.textContent = 'Hello World';
el.classList.add('greeting');
return el;
};
const container = document.querySelector('.container');
observe('.greeting', el => console.log('I have arrived!', el), container);
new Promise(res => setTimeout(() => res(createGreeting()), 2000))
.then(el => container.appendChild(el));
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
body { display: flex; }
.container { display: flex; flex: 1; align-items: center; justify-content: center; }
.greeting { font-weight: bold; font-size: 2em; }
<div class="container"></div>
Update
Here is an experimental async/await example.
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
const observe = (selector, targetNode = document.body) =>
new Promise(res => {
new MutationObserver(mutations =>
res([...mutations]
.flatMap((mutation) => [...mutation.addedNodes])
.find((node) => node.matches && node.matches(selector))))
.observe(targetNode, { childList: true, subtree: true });
});
const createGreeting = () => {
const el = document.createElement('DIV');
el.textContent = 'Hello World';
el.classList.add('greeting');
return el;
};
const container = document.querySelector('.container');
observe('.greeting', container)
.then(el => console.log('I have arrived!', el));
(async () => {
await sleep(2000);
container.appendChild(createGreeting());
})();
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
body { display: flex; }
.container { display: flex; flex: 1; align-items: center; justify-content: center; }
.greeting { font-weight: bold; font-size: 2em; }
<div class="container"></div>
Comments
This is a better version written on top of Yong Wang's answer (highest scored answer).
Added feature: you can wait for an element for a particular amount of time with location precision to increase performance.
async function waitForElement(selector, timeout = null, location = document.body) {
return new Promise((resolve) => {
let element = document.querySelector(selector);
if (element) {
return resolve(element);
}
const observer = new MutationObserver(async () => {
let element = document.querySelector(selector);
if (element) {
resolve(element);
observer.disconnect();
} else {
if (timeout) {
async function timeOver() {
return new Promise((resolve) => {
setTimeout(() => {
observer.disconnect();
resolve(false);
}, timeout);
});
}
resolve(await timeOver());
}
}
});
observer.observe(location, {
childList: true,
subtree: true,
});
});
}
Usage:
await waitForElement(".nav-alt", 500, ".main-body")
Bonus: Wait for a element to disappear from DOM.
async function waitForElementDeath(selector, location = document.body) {
return new Promise((resolve) => {
const observer = new MutationObserver(async () => {
if (!document.querySelector(selector)) {
resolve(true);
observer.disconnect();
}
});
observer.observe(location, {
childList: true,
subtree: true,
});
});
}
Usage:
await waitForElementDeath(".Popup-div", "Popup-Container")
Comments
A solution returning a Promise and allowing to use a timeout (compatible IE 11+).
For a single element (type Element):
"use strict";
function waitUntilElementLoaded(selector) {
var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var start = performance.now();
var now = 0;
return new Promise(function (resolve, reject) {
var interval = setInterval(function () {
var element = document.querySelector(selector);
if (element instanceof Element) {
clearInterval(interval);
resolve();
}
now = performance.now();
if (now - start >= timeout) {
reject("Could not find the element " + selector + " within " + timeout + " ms");
}
}, 100);
});
}
For multiple elements (type NodeList):
"use strict";
function waitUntilElementsLoaded(selector) {
var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var start = performance.now();
var now = 0;
return new Promise(function (resolve, reject) {
var interval = setInterval(function () {
var elements = document.querySelectorAll(selector);
if (elements instanceof NodeList) {
clearInterval(interval);
resolve(elements);
}
now = performance.now();
if (now - start >= timeout) {
reject("Could not find elements " + selector + " within " + timeout + " ms");
}
}, 100);
});
}
Examples:
waitUntilElementLoaded('#message', 800).then(function(element) {
// element found and available
element.innerHTML = '...';
}).catch(function() {
// element not found within 800 milliseconds
});
waitUntilElementsLoaded('.message', 10000).then(function(elements) {
for(const element of elements) {
// ....
}
}).catch(function(error) {
// elements not found withing 10 seconds
});
Works for both a list of elements and a single element.
2 Comments
element instanceof HTMLElement? Can it ever be anything other than null or HTMLElement?Element instead (fixed). I just make the check because I want to be sure the variable element has the property innerHTML as the Element MDN documentation states. Feel free to remove it if you do not care about it!A cleaner example using MutationObserver:
new MutationObserver( mutation => {
if (!mutation.addedNodes) return
mutation.addedNodes.forEach( node => {
// do stuff with node
})
})
1 Comment
I have developed an answer inspired by Jamie Hutber's.
It's a promise based function where you can set:
- maximum number of tries - default
10; - delay in milliseconds - default
100 ms.
Therefore, by default, it will wait 1 second until the element appears on the DOM.
If it does not show up it will return a promise.reject with null so you can handle the error as per your wish.
Code
export function _waitForElement(selector, delay = 10, tries = 100) {
const element = document.querySelector(selector);
if (!window[`__${selector}`]) {
window[`__${selector}`] = 0;
window[`__${selector}__delay`] = delay;
window[`__${selector}__tries`] = tries;
}
function _search() {
return new Promise((resolve) => {
window[`__${selector}`]++;
setTimeout(resolve, window[`__${selector}__delay`]);
});
}
if (element === null) {
if (window[`__${selector}`] >= window[`__${selector}__tries`]) {
window[`__${selector}`] = 0;
return Promise.resolve(null);
}
return _search().then(() => _waitForElement(selector));
} else {
return Promise.resolve(element);
}
}
Usage:
async function wait(){
try{
const $el = await waitForElement(".llama");
console.log($el);
} catch(err){
console.error("Timeout - couldn't find element.")
}
}
wait();
In the example above it will wait for the selector .llama. You can add a greater delay and test it here on the console of StackoverFlow.
Just add the class llama to any element on the DOM.
Comments
Another option for blocking until an element exists (or any other predicate) is requestAnimationFrame:
(function wait() {
const el = document.querySelector("p");
if (el) {
console.log(el.textContent);
} else {
requestAnimationFrame(wait);
}
})();
// test it:
setTimeout(() => (document.body.innerHTML += "<p>hi</p>"), 3000);
This is a busy-wait, so not necessarily as efficient as a mutation observer. But RAF is pretty optimized and is easier to write and remember off the top of my head, so I use this inline all the time in my userscripts and console automation.
Here's a promise-based version:
const wait = selector =>
new Promise(resolve =>
(function wait() {
const el = document.querySelector(selector);
el ? resolve(el) : requestAnimationFrame(wait);
})()
);
// test it:
wait("p").then(el => console.log(el.textContent));
setTimeout(() => (document.body.innerHTML += "<p>hi</p>"), 3000);
A possible improvement would be to optionally throw if the element doesn't appear within a certain amount of time.
If that's not enough, inspired by Puppeteer's waitForFunction and waitForSelector, I wrote a small browser library with functions that offer RAF and setTimeout polling and mutation observation.
Note: If logs don't show up in the sandbox console, check your actual browser dev tools.
// $ is short for waitForSelector
ps.$("p").then(element => {
console.log(element.textContent);
});
// test it
setTimeout(() => {
document.body.innerHTML += "<p>hi</p>";
}, 2000);
<script src="https://cdn.jsdelivr.net/gh/ggorlen/portascrape@9cca94d/portascrape.js"></script>
Wait for an element by text matching regex:
ps.$("p", {
matches: /this one/i,
timeout: 5_000,
polling: "mutation",
}).then((el) => console.log(el.textContent));
setTimeout(() => {
document.body.innerHTML += "<p>This one!</p>";
}, 2000);
<script src="https://cdn.jsdelivr.net/gh/ggorlen/portascrape@9cca94d/portascrape.js"></script>
Wait and get text:
ps.$text("p").then((text) => console.log(text));
setTimeout(() => {
document.body.innerHTML += "<p>hi</p>";
}, 3000);
<script src="https://cdn.jsdelivr.net/gh/ggorlen/portascrape@9cca94d/portascrape.js"></script>
Wait and click:
ps.$click("button").then(el => console.log(el.textContent));
setTimeout(() => {
document.body.innerHTML +=
`<button onclick="this.textContent='clicked'">x</button>`;
}, 2000);
<script src="https://cdn.jsdelivr.net/gh/ggorlen/portascrape@9cca94d/portascrape.js"></script>
Wait for an arbitrary predicate:
ps.wait(() =>
document.querySelector("p")?.textContent.includes("hello")
).then(() => console.log("hello exists"));
setTimeout(() => {
document.body.innerHTML += "<p>hello world</p>";
}, 3000);
<script src="https://cdn.jsdelivr.net/gh/ggorlen/portascrape@9cca94d/portascrape.js"></script>
Block all <h1>s with certain text forever:
(async () => {
for (;;) await ps.$remove("h1", { containsText: "never", timeout: 0 });
})();
setInterval(() => {
document.body.innerHTML += "<h1>This will never show up</h1>";
}, 3000);
setTimeout(() => {
document.body.innerHTML += "<h1>This will show up</h1>";
}, 3000);
<script src="https://cdn.jsdelivr.net/gh/ggorlen/portascrape@9cca94d/portascrape.js"></script>
Comments
Here is a TypeScript version of Yong Wang's accepted answer using MutationObserver which takes an optional return type that extends from HTMLElement.
This is useful if you need to access element type specific properties (like src on an <img> or href on a <a> link):
function waitFor<T extends HTMLElement>(selector: string): Promise<T> {
return new Promise((resolve) => {
const elm = document.querySelector<T>(selector)
if (elm) return resolve(elm)
const observer = new MutationObserver((mutations) => {
const elm = document.querySelector<T>(selector)
if (elm) {
resolve(elm)
observer.disconnect()
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
})
})
}
Usage:
// By default, returns the type "HTMLElement"
const elm = await waitFor('h1')
// Or specify element type if you know it:
const elm = await waitFor<HTMLFormElement>('form')
Comments
If you want it to stop looking after a while (timeout) then the following jQuery will work. It will time out after 10sec. I needed to use this code rather than pure JS because I needed to select an input via name and was having trouble implementing some of the other solutions.
// Wait for element to exist.
function imageLoaded(el, cb,time) {
if ($(el).length) {
// Element is now loaded.
cb($(el));
var imageInput = $('input[name=product\\[image_location\\]]');
console.log(imageInput);
} else if(time < 10000) {
// Repeat every 500ms.
setTimeout(function() {
time = time+500;
imageLoaded(el, cb, time)
}, 500);
}
};
var time = 500;
imageLoaded('input[name=product\\[image_location\\]]', function(el) {
//do stuff here
},time);
Comments
I try to avoid mutation observers if I can help it, so this is what I came up with. It looks similar to some of the other answers above. This function will look for the first element to exist within a given DOM call -- className being the expected usage but it can also accept tagName or Id. You could also add an argument for a precise index if you were looking for some number of elements with a given classname or tagname to have loaded.
async function waitUntilElementExits(domkey,domquery,maxtime){
const delay = (ms) => new Promise(res => setTimeout(res, ms));
for(let i=0; i<maxtime; i=i+200){
await delay(200);
let elm = document[domkey](domquery);
if( (domkey == 'getElementById' && elm) || elm?.[0] ) break;
}
}
// usage
await waitUntilElementExits('getElementByClassName','some_class_name',10000)
2 Comments
My take on @Yong Wong's solution, but it has an optional timeout and you can specify the root node from where you'd like to wait for the element.
Full async/await.
const $ = (selector, opts) => {
let timeout = undefined;
let root = undefined;
if (opts) {
({ root, timeout } = opts);
}
if (root === undefined) root = document.body;
const nodeFound = root.querySelector(selector);
if (nodeFound) return new Promise(resolve => resolve(nodeFound));
return new Promise((resolve, reject) => {
let callback = () => {
observer.disconnect();
};
const _resolve = (node) => {
callback();
resolve(node);
};
const _reject = (err) => {
callback();
reject(err);
};
if (timeout && timeout > 0) {
const handle = setTimeout(() => {
_reject(new Error("Element not found: timeout exceeded."));
}, timeout);
callback = () => {
observer.disconnect();
clearTimeout(handle);
};
}
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.matches(selector)) {
_resolve(addedNode);
return;
}
}
}
});
observer.observe(root, {
childList: true,
subtree: true,
});
});
}
Example call:
// wait for 10 seconds for 'div.bla-bla-bla' to appear as a child of 'div.some-container'
await $("div.bla-bla-bla", {
timeout: 10000,
root: document.querySelector("div.some-container")
});
Comments
Use the arrive.js library written by Uzair Farooq which internally uses the mutation observer api.
Examples from the project's readme:
// watch for creation of an element which satisfies the selector ".test-elem"
document.arrive(".test-elem", function(newElem) {
// newElem refers to the newly created element
});
// the above event would watch for creation of element in whole document
// it's better to be more specific whenever possible, for example
document.querySelector(".container-1").arrive(".test-elem", function(newElem) {
});
// you can bind event to multiple elements at once
// this will bind arrive event to all the elements returned by document.querySelectorAll()
document.querySelectorAll(".box").arrive(".test-elem", function(newElem) {
});
I read through the other answers and found this library as a comment to another answer.
I had already implemented my own solution using the mutation observer API but I think this library is a better, more thorough implementation of the concept than my solution or many of the others that are snippets but not as fully fleshed out as this library is.
Comments
Yong Wang's answer is accurate, can however sometimes fail to trigger for child elements of an added node. Querying children in those solves this.
For example, the following is a viable waitForKeyElements replacement which is popular in userscripts:
function waitForMutElements(selector, callback) {
function tagCall(el) {
if (!el.getAttribute("data-userscript-alreadyMut")) {
el.setAttribute("data-userscript-alreadyMut", true);
callback(el);
}
}
// Trigger for elements existing at call time.
for (const el of document.querySelectorAll(selector)) {
tagCall(el);
}
const observer = new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
for (const el of mutation.addedNodes) {
if (el.nodeType === Node.ELEMENT_NODE) {
if (el.matches(selector)) {
// Trigger for element added later.
tagCall(el);
} else {
// Trigger for child element of element added later.
for (const elChild of el.querySelectorAll(selector)) {
tagCall(elChild);
}
}
}
}
}
});
observer.observe(document.body, { subtree: true, childList: true });
}
- You may want to optimize the above to only require one
MutationObserver, for example by going through aMapof selector/callback pairs during mutation events to which you push for everywaitForMutElements(selector, callback)call. - You can remove the
data-attribute check if you are okay with same elements triggering multiple times.
Comments
I've adapted the function by @YongWang to use a lambda so anything can be used, including XPath:
function waitForElement(findElement) {
return new Promise(resolve => {
const elem = findElement();
if (elem) {
return resolve(elem);
}
const observer = new MutationObserver(_ => {
const elem = findElement();
if (elem) {
observer.disconnect();
resolve(elem);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
});
}
To use it with async/await and xpath, use this code:
function findByXPath(xpath) {
return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
(async() => {
const btn = await waitForElement(() => findByXPath("//*[text()='foobar']"));
console.log('button with text foobar appeared: ', btn);
})();
MutationObserver>DOM Mutation Events>setTimeout.setTimeoutis compatible, simple to implement, simple to maintain, and has negligible overhead.setTimeout+jQueryis less than ideal in my opinion for two reasons: 1.) jQuery bloat 2.) you're needlessly manually querying the DOM for elements, events beat that speed-wise easily, 3.) it will always be slower than any native implementation. If you need to do anything based on the presence of an element reasonably quickly, especially if seamless user experience is your goal, it is inferior.