GithubHelp home page GithubHelp logo

element-ready's Introduction

element-ready

Detect when an element is ready in the DOM

Install

npm install element-ready

Usage

import elementReady from 'element-ready';

const element = await elementReady('#unicorn');

console.log(element.id);
//=> 'unicorn'

API

elementReady(selector, options?)

Returns a promise for a matching element.

observeReadyElements(selector, options?)

Returns an async iterable which yields with each new matching element. Useful for user-scripts that modify elements when they are added.

import {observeReadyElements} from 'element-ready';

for await (const element of observeReadyElements('#unicorn')) {
	console.log(element.id);
	//=> 'unicorn'

	if (element.id === 'elephant') {
		break;
	}
}

selector

Type: string

CSS selector.

Prefix the element type to get a better TypeScript return type. For example, button.my-btn instead of .my-btn.

options

Type: object

target

Type: Element | document
Default: document

The element that's expected to contain a match.

stopOnDomReady

Type: boolean
Default: true

Automatically stop checking for the element to be ready after the DOM ready event. The promise is then resolved to undefined.

timeout

Type: number
Default: Infinity

Milliseconds to wait before stopping the search and resolving the promise to undefined.

waitForChildren

Type: boolean
Default: true

Since the current document’s HTML is downloaded and parsed gradually, elements may appear in the DOM before all of their children are “ready”.

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.

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.

predicate

Type: (element: HTMLElement) => boolean
Default: undefined

A predicate function will be called for each element that matches the selector. If it returns true, the element will be returned.

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.

<ul id="country-list">
	<li>country a</li>
	...
	<li>wanted country</li>
	...
</ul>
import elementReady from 'element-ready';

const wantedCountryElement = await elementReady('#country-list li', {
	predicate: listItemElement => listItemElement.textContent === 'wanted country'
});

elementReadyPromise#stop()

Type: Function

Stop checking for the element to be ready. The stop is synchronous and the original promise is then resolved to undefined.

Calling it after the promise has settled or multiple times does nothing.

Related

  • dom-loaded - Check when the DOM is loaded like DOMContentLoaded

element-ready's People

Contributors

bendingbender avatar bengry avatar bfred-it avatar cheap-glitch avatar fregante avatar reyronald avatar richienb avatar riophae avatar sindresorhus avatar soenkekluth avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

element-ready's Issues

Use types directly from querySelector, without copying them

If the selector is 'a', querySelector‘s return type is be HTMLAnchorElement. To achieve the same in element-ready, we had to manually copy its types and adapt them to elementReady

Ideally, instead, we should try to “inherit” directly from the global querySelector so that, if its types are enhanced (via typed-query-selector or natively), element-ready automatically gets upgraded.

Note: We can't use Parameters<typeof querySelector> and such because it doesn't support overloads nor generics.

This has been unsuccessfully tried in #31. More info about it in that PR. A possible solution might be microsoft/TypeScript#33185 (comment)

Update many-keys-map to v2 ESM

I recently stumbled upon an issue while using Jest with element-ready where it has a type error.

  ● Test suite failed to run

           TypeError: many_keys_map_1.default is not a constructor

           > 1 | import elementReady from "element-ready"

During investigation, I just noticed that element-ready has some peers that can be updated. many-keys-map e.g. now is ESM only which match your philosophy of switching to ESM only.

I already tested v2 by doing a pnpm override of many-keys-map (no API changes), and it fixes my issue above.

requestAnimationFrame instead of setInterval

I used element-ready for refined-github/refined-github#441 but it's not fast enough. Here's a comparison with the simple rAF-based solution I used in github-clean-feed and how that compares with element-ready's setInterval

const domReady = new Promise(resolve => {
	(function check() {
		if (document.querySelector('.ajax-pagination-form')) {
			resolve();
		} else {
			requestAnimationFrame(check);
		}
	})();
});

reload screencast

⬆️ I'm refreshing the page. Notice the feed being instantly changed by github-clean-feed, but the Trending link appearing after the first paint.

document.querySelector is not that CPU-intensive, so running it on every frame until domready is not that much to ask. Perhaps it can be an option. Either way I hope to see the change in RefinedGH

Needs compilation step

browserify automatically skips transforms (babel) on external modules so arrow functions are imported as is and don't work in Safari and IE.

I'd suggest rollup + rollup-buble

Found elements are not uncached

add element
wait for element
resolve promise

add element (different, but same selector)
wait for element
resolve promise (resolves with first element)

I'm adding a test

Support document's readyState individually

Abstract

  • A document has these ready states
    • loading
      • The document is loading.
    • interactive
      • The document has finished loading and we can access DOM elements.
      • Sub-resources such as scripts, images, stylesheets and frames are still loading.
    • complete
      • The page is fully loaded.

Currently, element-ready is checking is DOM loaded like below.

const isDomReady = target =>
	['interactive', 'complete'].includes((target.ownerDocument || target).readyState);

But if we need or want to check some elements which generated with script, We cannot use element-ready.

Suggestion

  • Support DOM reasource ready and Full page loading individually

from refined-github/refined-github#6837 (comment)

Sometimes it can't capture the matching elements with `stopOnDomReady=true`

Demo: https://riophae.github.io/element-ready-bug-reproduction/
You can find the source code at https://github.com/riophae/element-ready-bug-reproduction

image

You see elementReady('#target') can't find the div#target element that it should do. This is repeatable on Chrome 76, Firefox 68 & Edge 18. The OS is Windows 10 x64 1903.
I have also included the fixed version for comparison.

The reason behind this is, the div#target element is placed near </html>. Before element-ready spots it, dom-loaded has cancelled requestAnimationFrame.

Subscribe to new elements on the page

Issuehunt badges

elementReady only works for one element, but sometimes it’s useful to just get all the elements on the page as they load. For example:

elementReady.subscribe('a', shortenLink);

This will run the function on each new element found on the page before reaching DOM ready.

MutationObserver with subtree:true would be ideal for this but somewhere at some point I read that it’s heavy to run this while the page loads as there are too many events. It would be great to actually test this and compare it to a simpler setInterval + querySelectorAll. Without performance tests, I’d avoid MutationObserver


Note: This issue has a bounty, so it's expected that you are an experienced programmer and that you give it your best effort. Don't forget, if applicable, to add tests, docs (double-check for typos), and update TypeScript definitions. Instead of asking too many questions, present solutions. The point of an issue bounty is to reduce my workload, not give me more. Thanks for helping out 🙌


IssueHunt Summary

richienb richienb has been rewarded.

Backers (Total: $80.00)

Submitted pull Requests


Tips

Allow specifying a predicate function to only resolve on a more specific element

The current behavior of elementReady is great, but sometimes there are multiple items matching the selector. Currently, this means that the first matching element will be returned. However, there are many use-cases where other ones are wanted, and a more specific CSS selector cannot be used. For example:

<ul id="country-list">
	<li>country a</li>
	...
	<li>wanted country</li>
	...
</ul>

Currently, you can't get the <li> with wanted country, where the entire <ul> (or <lis) is not present on the page from the get-go.

My suggestion is to add an optional predicate function to the elementReady options, such that you could solve the above scenario like so:

const someSpecificCountry = await elementReady('#country-list > li', {
	predicate: item => !!item.textContent && /wanted country/i.test(item.textContent),
});

I already have a working implementation of this as a patch (using patch-package) and would be happy to submit a PR if this is an acceptable for the problem.

Timeout or CancelablePromise

I think this needs a way to stop looking for the element instead of trying ad infinitum.

Possible solutions:

elementReady('.button', {timeout: 10000});

elementReady('.button', {until: require('dom-loaded')});

const ready = elementReady('.button');
gitHubInjection(window, () => ready.cancel()); // stop looking when the page changes

I think the CancelablePromise is the most flexible one.

MutationObserver

What about using a MutationObserver instead of an interval in order to know when the element is inserted in the dom?

I'd like to do the pr if you are ok with the feature 🚀

`waitForChildren` might still not be reliable

So this happened:

Screen Shot 7 Screen Shot 8

"7 years old" should be .appended to this block, but on my slow connection it was .appended before the block finished loading.

This matches exactly the situation that this feature was supposed to prevent. It selected the correct element but when elementReady resolved the element wasn't fully done yet.

Happened on: https://github.com/npmhub/npmhub
Source: https://github.com/sindresorhus/refined-github/blob/cd2788a4114de6fdfc42a9a13a018cf4ee8caf8e/source/features/repo-age.tsx#L102
Browser: Safari

Automatically stop on dom ready

Issuehunt badges

  const waiting = elementReady(selector);

  // Don't check ad-infinitum
  domLoaded.then(() => requestAnimationFrame(() => waiting.cancel()));

  // If cancelled, return null like a regular querySelector() would
  return waiting.catch(() => null);

Note to self: This should really be an option in element-ready.
@sindresorhus on refined-github/refined-github#605 (comment)

IssueHunt Summary

bfred-it bfred-it has been rewarded.

Sponsors (Total: $40.00)

Tips

Returns the wrong element when calling it with the same selector, but in different targets

I came across this scenario:

<div id="target1">
  <p class="unicorn">Some element</p>
</div>

<div id="target2">
  <span class="unicorn">Some other completely different element</span>
</div>

Using elementReady in the following way with this markup will output:

// Returns the <p> element (correct)
elementReady('.unicorn', { target: $('#target1') }); 

// Returns the same <p> element (wrong), should return <span>
elementReady('.unicorn', { target: $('#target2') }); 

The culprit is the caching implementation, which is not taking into account the targets, just the selectors.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.