├── .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 |
46 |
country a
47 | ...
48 |
wanted country
49 | ...
50 |
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 |