├── .editorconfig ├── .gitattributes ├── .github ├── funding.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [sindresorhus, bfred-it] 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 20 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type {ParseSelector} from 'typed-query-selector/parser.js'; 2 | 3 | export type Options = { 4 | /** 5 | The element that's expected to contain a match. 6 | 7 | @default document 8 | */ 9 | readonly target?: HTMLElement | Document; 10 | 11 | /** 12 | Milliseconds to wait before stopping the search and resolving the promise to `undefined`. 13 | 14 | @default Infinity 15 | */ 16 | readonly timeout?: number; 17 | 18 | /** 19 | Automatically stop checking for the element to be ready after the DOM ready event. The promise is then resolved to `undefined`. 20 | 21 | @default true 22 | */ 23 | readonly stopOnDomReady?: boolean; 24 | 25 | /** 26 | Since the current document’s HTML is downloaded and parsed gradually, elements may appear in the DOM before _all_ of their children are “ready”. 27 | 28 | By default, `element-ready` guarantees the element and all of its children have been parsed. This is useful if you want to interact with them or if you want to `.append()` something inside. 29 | 30 | By setting this to `false`, `element-ready` will resolve the promise as soon as it finds the requested selector, regardless of its content. This is ok if you're just checking if the element exists or if you want to read/change its attributes. 31 | 32 | @default true 33 | */ 34 | readonly waitForChildren?: boolean; 35 | 36 | /** 37 | A predicate function will be called for each element that matches the selector. If it returns `true`, the element will be returned. 38 | 39 | @default undefined 40 | 41 | For example, if the content is dynamic or a selector cannot be specific enough, you could check `.textContent` of each element and only match the one that has the required text. 42 | 43 | @example 44 | ```html 45 | 51 | ``` 52 | 53 | ``` 54 | import elementReady from 'element-ready'; 55 | 56 | const wantedCountryElement = await elementReady('#country-list li', { 57 | predicate: listItemElement => listItemElement.textContent === 'wanted country' 58 | }); 59 | ``` 60 | */ 61 | predicate?(element: HTMLElement): boolean; 62 | }; 63 | 64 | export type StoppablePromise = Promise & { 65 | /** 66 | Stop checking for the element to be ready. The stop is synchronous and the original promise is then resolved to `undefined`. 67 | 68 | Calling it after the promise has settled or multiple times does nothing. 69 | */ 70 | stop(): void; 71 | }; 72 | 73 | /** 74 | Detect when an element is ready in the DOM. 75 | 76 | @param selector - [CSS selector.](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) Prefix the element type to get a better return type. For example, `button.my-btn` instead of `.my-btn`. 77 | @returns The matching element, or `undefined` if the element could not be found. 78 | 79 | @example 80 | ``` 81 | import elementReady from 'element-ready'; 82 | 83 | const element = await elementReady('#unicorn'); 84 | 85 | console.log(element.id); 86 | //=> 'unicorn' 87 | ``` 88 | */ 89 | export default function elementReady>( 90 | selector: Selector, 91 | options?: Options 92 | ): StoppablePromise; 93 | export default function elementReady( 94 | selector: string, 95 | options?: Options 96 | ): StoppablePromise; 97 | 98 | /** 99 | Detect when elements are ready in the DOM. 100 | 101 | Useful for user-scripts that modify elements when they are added. 102 | 103 | @param selector - [CSS selector.](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) Prefix the element type to get a better return type. For example, `button.my-btn` instead of `.my-btn`. 104 | @returns An async iterable which yields with each new matching element. 105 | 106 | @example 107 | ``` 108 | import {observeReadyElements} from 'element-ready'; 109 | 110 | for await (const element of observeReadyElements('#unicorn')) { 111 | console.log(element.id); 112 | //=> 'unicorn' 113 | 114 | if (element.id === 'elephant') { 115 | break; 116 | } 117 | } 118 | ``` 119 | */ 120 | export function observeReadyElements>( 121 | selector: Selector, 122 | options?: Options 123 | ): AsyncIterable; 124 | export function observeReadyElements( 125 | selector: string, 126 | options?: Options 127 | ): AsyncIterable; 128 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import requestAnimationFrames from 'request-animation-frames'; 2 | import domMutations from 'dom-mutations'; 3 | 4 | const isDomReady = target => 5 | ['interactive', 'complete'].includes((target.ownerDocument ?? target).readyState); 6 | 7 | export default function elementReady(selector, { 8 | target = document, 9 | stopOnDomReady = true, 10 | waitForChildren = true, 11 | timeout = Number.POSITIVE_INFINITY, 12 | predicate, 13 | } = {}) { 14 | // Not necessary, it just acts faster and avoids listener setup 15 | if (stopOnDomReady && isDomReady(target)) { 16 | const promise = Promise.resolve(getMatchingElement({target, selector, predicate})); 17 | promise.stop = () => {}; 18 | return promise; 19 | } 20 | 21 | let shouldStop = false; 22 | 23 | const stop = () => { 24 | shouldStop = true; 25 | }; 26 | 27 | if (timeout !== Number.POSITIVE_INFINITY) { 28 | setTimeout(stop, timeout); 29 | } 30 | 31 | // Interval to keep checking for it to come into the DOM 32 | const promise = (async () => { 33 | for await (const _ of requestAnimationFrames()) { 34 | if (shouldStop) { 35 | return; 36 | } 37 | 38 | const element = getMatchingElement({target, selector, predicate}); 39 | 40 | // When it's ready, only stop if requested or found 41 | if (isDomReady(target) && (stopOnDomReady || element)) { 42 | return element; 43 | } 44 | 45 | let current = element; 46 | while (current) { 47 | if (!waitForChildren || current.nextSibling) { 48 | return element; 49 | } 50 | 51 | current = current.parentElement; 52 | } 53 | } 54 | })(); 55 | 56 | promise.stop = stop; 57 | 58 | return promise; 59 | } 60 | 61 | export function observeReadyElements(selector, { 62 | target = document, 63 | stopOnDomReady = true, 64 | waitForChildren = true, 65 | timeout = Number.POSITIVE_INFINITY, 66 | predicate, 67 | } = {}) { 68 | return { 69 | async * [Symbol.asyncIterator]() { 70 | const iterator = domMutations(target, {childList: true, subtree: true})[Symbol.asyncIterator](); 71 | 72 | if (stopOnDomReady) { 73 | if (isDomReady(target)) { 74 | return; 75 | } 76 | 77 | target.addEventListener('DOMContentLoaded', () => { 78 | iterator.return(); 79 | }, {once: true}); 80 | } 81 | 82 | if (timeout !== Number.POSITIVE_INFINITY) { 83 | setTimeout(() => { 84 | iterator.return(); 85 | }, timeout); 86 | } 87 | 88 | for await (const {addedNodes} of iterator) { 89 | for (const element of addedNodes) { 90 | if (element.nodeType !== 1 || !element.matches(selector) || (predicate && !predicate(element))) { 91 | continue; 92 | } 93 | 94 | // When it's ready, only stop if requested or found 95 | if (isDomReady(target) && element) { 96 | yield element; 97 | continue; 98 | } 99 | 100 | let current = element; 101 | while (current) { 102 | if (!waitForChildren || current.nextSibling) { 103 | yield element; 104 | break; 105 | } 106 | 107 | current = current.parentElement; 108 | } 109 | } 110 | } 111 | }, 112 | }; 113 | } 114 | 115 | function getMatchingElement({target, selector, predicate}) { 116 | if (!predicate) { 117 | return target.querySelector(selector) ?? undefined; // No `null` 118 | } 119 | 120 | for (const element of target.querySelectorAll(selector)) { 121 | if (predicate(element)) { 122 | return element; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-floating-promises */ 2 | import {expectType} from 'tsd'; 3 | import elementReady, {type StoppablePromise, observeReadyElements} from './index.js'; 4 | 5 | const promise = elementReady('#unicorn'); 6 | elementReady('#unicorn', {target: document}); 7 | elementReady('#unicorn', {target: document.documentElement}); 8 | elementReady('#unicorn', {timeout: 1_000_000}); 9 | 10 | elementReady('#unicorn', {stopOnDomReady: false}); 11 | 12 | expectType>(promise); 13 | expectType>(elementReady('div')); 14 | expectType>(elementReady('text')); 15 | 16 | expectType>(elementReady('.class')); 17 | expectType>(elementReady('div.class')); 18 | expectType>(elementReady('a#id')); 19 | expectType>(elementReady('input[type="checkbox"]')); 20 | expectType>(elementReady(':root > button')); 21 | 22 | promise.stop(); 23 | 24 | const readyElements = observeReadyElements('#unicorn'); 25 | 26 | expectType>(readyElements); 27 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element-ready", 3 | "version": "8.0.0", 4 | "description": "Detect when an element is ready in the DOM", 5 | "license": "MIT", 6 | "repository": "sindresorhus/element-ready", 7 | "funding": "https://github.com/sindresorhus/element-ready?sponsor=1", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=20" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "browser", 31 | "element", 32 | "ready", 33 | "dom", 34 | "css", 35 | "selector", 36 | "wait", 37 | "detect", 38 | "check", 39 | "dom", 40 | "domcontentloaded", 41 | "domready" 42 | ], 43 | "dependencies": { 44 | "dom-mutations": "^1.0.0", 45 | "request-animation-frames": "^1.0.0", 46 | "typed-query-selector": "^2.12.0" 47 | }, 48 | "devDependencies": { 49 | "ava": "^5.3.1", 50 | "jsdom": "^26.1.0", 51 | "p-state": "^2.0.1", 52 | "tsd": "^0.32.0", 53 | "xo": "^0.60.0" 54 | }, 55 | "xo": { 56 | "envs": [ 57 | "node", 58 | "browser" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # element-ready 2 | 3 | > Detect when an element is ready in the DOM 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm install element-ready 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import elementReady from 'element-ready'; 15 | 16 | const element = await elementReady('#unicorn'); 17 | 18 | console.log(element.id); 19 | //=> 'unicorn' 20 | ``` 21 | 22 | ## API 23 | 24 | ### elementReady(selector, options?) 25 | 26 | Returns a promise for a matching element. 27 | 28 | ### observeReadyElements(selector, options?) 29 | 30 | Returns an async iterable which yields with each new matching element. Useful for user-scripts that modify elements when they are added. 31 | 32 | ```js 33 | import {observeReadyElements} from 'element-ready'; 34 | 35 | for await (const element of observeReadyElements('#unicorn')) { 36 | console.log(element.id); 37 | //=> 'unicorn' 38 | 39 | if (element.id === 'elephant') { 40 | break; 41 | } 42 | } 43 | ``` 44 | 45 | #### selector 46 | 47 | Type: `string` 48 | 49 | [CSS selector.](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 50 | 51 | Prefix the element type to get a better TypeScript return type. For example, `button.my-btn` instead of `.my-btn`. 52 | 53 | #### options 54 | 55 | Type: `object` 56 | 57 | ##### target 58 | 59 | Type: `Element | document`\ 60 | Default: `document` 61 | 62 | The element that's expected to contain a match. 63 | 64 | ##### stopOnDomReady 65 | 66 | Type: `boolean`\ 67 | Default: `true` 68 | 69 | Automatically stop checking for the element to be ready after the [DOM ready event](https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event). The promise is then resolved to `undefined`. 70 | 71 | ##### timeout 72 | 73 | Type: `number`\ 74 | Default: `Infinity` 75 | 76 | Milliseconds to wait before stopping the search and resolving the promise to `undefined`. 77 | 78 | ##### waitForChildren 79 | 80 | Type: `boolean`\ 81 | Default: `true` 82 | 83 | Since the current document’s HTML is downloaded and parsed gradually, elements may appear in the DOM before _all_ of their children are “ready”. 84 | 85 | By default, `element-ready` guarantees the element and all of its children have been parsed. This is useful if you want to interact with them or if you want to `.append()` something inside. 86 | 87 | By setting this to `false`, `element-ready` will resolve the promise as soon as it finds the requested selector, regardless of its content. This is ok if you're just checking if the element exists or if you want to read/change its attributes. 88 | 89 | ##### predicate 90 | 91 | Type: `(element: HTMLElement) => boolean`\ 92 | Default: `undefined` 93 | 94 | A predicate function will be called for each element that matches the selector. If it returns `true`, the element will be returned. 95 | 96 | For example, if the content is dynamic or a selector cannot be specific enough, you could check `.textContent` of each element and only match the one that has the required text. 97 | 98 | ```html 99 |
    100 |
  • country a
  • 101 | ... 102 |
  • wanted country
  • 103 | ... 104 |
105 | ``` 106 | 107 | ```js 108 | import elementReady from 'element-ready'; 109 | 110 | const wantedCountryElement = await elementReady('#country-list li', { 111 | predicate: listItemElement => listItemElement.textContent === 'wanted country' 112 | }); 113 | ``` 114 | 115 | ### elementReadyPromise#stop() 116 | 117 | Type: `Function` 118 | 119 | Stop checking for the element to be ready. The stop is synchronous and the original promise is then resolved to `undefined`. 120 | 121 | Calling it after the promise has settled or multiple times does nothing. 122 | 123 | ## Related 124 | 125 | - [dom-loaded](https://github.com/sindresorhus/dom-loaded) - Check when the DOM is loaded like `DOMContentLoaded` 126 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import {setTimeout as delay} from 'node:timers/promises'; 2 | import test from 'ava'; 3 | import {JSDOM} from 'jsdom'; 4 | import {promiseStateSync} from 'p-state'; 5 | import elementReady, {observeReadyElements} from './index.js'; 6 | 7 | const {window} = new JSDOM(); 8 | globalThis.window = window; 9 | globalThis.document = window.document; 10 | globalThis.MutationObserver = window.MutationObserver; 11 | 12 | test('check if element ready', async t => { 13 | const elementCheck = elementReady('#unicorn', {stopOnDomReady: false}); 14 | 15 | (async () => { 16 | await delay(500); 17 | const element = document.createElement('p'); 18 | element.id = 'unicorn'; 19 | document.body.append(element); 20 | })(); 21 | 22 | const element = await elementCheck; 23 | t.is(element.id, 'unicorn'); 24 | }); 25 | 26 | test('check elements against a predicate', async t => { 27 | const elementCheck = elementReady('li', { 28 | stopOnDomReady: false, 29 | predicate: element => element.textContent && element.textContent.match(/wanted/i), 30 | }); 31 | 32 | (async () => { 33 | await delay(500); 34 | const listElement = document.createElement('ul'); 35 | for (const text of ['some text', 'wanted text']) { 36 | const li = document.createElement('li'); 37 | li.textContent = text; 38 | listElement.append(li); 39 | } 40 | 41 | document.body.append(listElement); 42 | })(); 43 | 44 | const element = await elementCheck; 45 | t.is(element.textContent, 'wanted text'); 46 | }); 47 | 48 | test('check if element ready inside target', async t => { 49 | const target = document.createElement('p'); 50 | const elementCheck = elementReady('#unicorn', { 51 | target, 52 | stopOnDomReady: false, 53 | }); 54 | 55 | (async () => { 56 | await delay(500); 57 | const element = document.createElement('p'); 58 | element.id = 'unicorn'; 59 | target.append(element); 60 | })(); 61 | 62 | const element = await elementCheck; 63 | t.is(element.id, 'unicorn'); 64 | }); 65 | 66 | test('check if different elements ready inside different targets with same selector', async t => { 67 | const target1 = document.createElement('p'); 68 | const elementCheck1 = elementReady('.unicorn', { 69 | target: target1, 70 | stopOnDomReady: false, 71 | }); 72 | const target2 = document.createElement('span'); 73 | const elementCheck2 = elementReady('.unicorn', { 74 | target: target2, 75 | stopOnDomReady: false, 76 | }); 77 | 78 | (async () => { 79 | await delay(500); 80 | const element1 = document.createElement('p'); 81 | element1.id = 'unicorn1'; 82 | element1.className = 'unicorn'; 83 | target1.append(element1); 84 | 85 | const element2 = document.createElement('span'); 86 | element2.id = 'unicorn2'; 87 | element2.className = 'unicorn'; 88 | target2.append(element2); 89 | })(); 90 | 91 | const element1 = await elementCheck1; 92 | t.is(element1.id, 'unicorn1'); 93 | 94 | const element2 = await elementCheck2; 95 | t.is(element2.id, 'unicorn2'); 96 | }); 97 | 98 | test('check if element ready after dom loaded', async t => { 99 | const elementCheck = elementReady('#bio', { 100 | stopOnDomReady: true, 101 | }); 102 | 103 | // The element will be added eventually, but we're not around to wait for it 104 | setTimeout(() => { 105 | const element = document.createElement('p'); 106 | element.id = 'bio'; 107 | document.body.append(element); 108 | }, 50_000); 109 | 110 | const element = await elementCheck; 111 | t.is(element, undefined); 112 | }); 113 | 114 | test('check if element ready before dom loaded', async t => { 115 | const element = document.createElement('p'); 116 | element.id = 'history'; 117 | document.body.append(element); 118 | 119 | const elementCheck = elementReady('#history', { 120 | stopOnDomReady: true, 121 | }); 122 | 123 | t.is(await elementCheck, element); 124 | }); 125 | 126 | test('stop checking if DOM was already ready', async t => { 127 | const elementCheck = elementReady('#no-gonna-get-us', { 128 | stopOnDomReady: true, 129 | }); 130 | 131 | t.is(await elementCheck, undefined); 132 | }); 133 | 134 | test('check if element ready after timeout', async t => { 135 | const elementCheck = elementReady('#cheezburger', { 136 | stopOnDomReady: false, 137 | timeout: 1000, 138 | }); 139 | 140 | // The element will be added eventually, but we're not around to wait for it 141 | const timeoutId = setTimeout(() => { 142 | const element = document.createElement('p'); 143 | element.id = 'cheezburger'; 144 | document.body.append(element); 145 | }, 50_000); 146 | 147 | const element = await elementCheck; 148 | t.is(element, undefined); 149 | clearTimeout(timeoutId); 150 | }); 151 | 152 | test('check if element ready before timeout', async t => { 153 | const element = document.createElement('p'); 154 | element.id = 'thunders'; 155 | document.body.append(element); 156 | 157 | const elementCheck = elementReady('#thunders', { 158 | stopOnDomReady: false, 159 | timeout: 10, 160 | }); 161 | 162 | t.is(await elementCheck, element); 163 | }); 164 | 165 | test('check if wait can be stopped', async t => { 166 | const elementCheck = elementReady('#dofle', {stopOnDomReady: false}); 167 | 168 | await delay(200); 169 | elementCheck.stop(); 170 | 171 | await delay(500); 172 | const element = document.createElement('p'); 173 | element.id = 'dofle'; 174 | document.body.append(element); 175 | 176 | t.is(await elementCheck, undefined); 177 | }); 178 | 179 | test('ensure different promises are returned on second call with the same selector when first was stopped', async t => { 180 | const elementCheck1 = elementReady('.unicorn', {stopOnDomReady: false}); 181 | 182 | elementCheck1.stop(); 183 | 184 | const elementCheck2 = elementReady('.unicorn', {stopOnDomReady: false}); 185 | 186 | t.not(elementCheck1, elementCheck2); 187 | t.is(await elementCheck1, undefined); 188 | }); 189 | 190 | test('ensure different promises are returned on second call with the same selector when first was found', async t => { 191 | const prependElement = () => { 192 | const element = document.createElement('p'); 193 | element.className = 'unicorn'; 194 | document.body.prepend(element); 195 | return element; 196 | }; 197 | 198 | t.is(prependElement(), await elementReady('.unicorn')); 199 | 200 | document.querySelector('.unicorn').remove(); 201 | t.is(prependElement(), await elementReady('.unicorn')); 202 | 203 | document.querySelector('.unicorn').remove(); 204 | t.is(prependElement(), await elementReady('.unicorn')); 205 | }); 206 | 207 | test('ensure that the whole element has loaded', async t => { 208 | const {window} = new JSDOM('