dom-track
Version:
Minimal utility for observing DOM elements appearing and disappearing using callbacks or Promises.
245 lines (193 loc) โข 7.46 kB
Markdown
# domTrack
[[_TOC_]]
## ๐ง Tracks DOM changes with Promise chaining
Fluent utility to track DOM elements as they appear, change, or get removed โ using both callback and Promise-based APIs.
A lazy-evaluated DSL powered by `MutationObserver` with a simpler, ergonomic API designed for real-world UIs. Efficient enough for isolated components, and scalable for full-page applications.
Supports:
- `seen()` โ when elements matching a selector appear
- `changed()` โ when matching elements have their content updated
- `gone()` โ when elements are removed from the DOM
- `goneForElement()` โ one-off removal watcher
- `.once()` `.timeout()` โ Promise chaining support
## ๐ฆ Install
```sh
npm install dom-track
```
```ts
// Typescript
import {DomTrack} from 'dom-track';
```
```js
// CommonJS (require)
const {DomTrack} = require('dom-track');
// ES Modules (import)
import {DomTrack} from 'dom-track';
```
## ๐ง API
```ts
new DomTrack(container: HTMLElement, options?: DomTrackOptions)
```
Options:
```ts
interface DomTrackOptions {
observeAttributes?: boolean; // default false
observeSubtree?: boolean; // default true
debounceMs?: number; // default 0
waitForTimeoutMs?: number; // default 5000
signal?: AbortSignal;
}
```
Note:
- `debounceMs` must be set on the observer instance and applies to all its `.seen()` and `.changed()` handlers.
- `.gone()` and `.goneForElement()` callbacks are invoked immediately and are **not debounced**.
### watches
#### .seen(selector, callback?)
Watches for elements matching the selector appearing in the DOM.
- With callback โ returns `WatchHandle` (`.cancel()`)
- Without callback โ returns `WatchPromise<HTMLElement>` (`.once()`, `.timeout()`, etc.)
#### .changed(selector, callback?)
Watches for DOM changes inside elements matching the selector.
(Same return types as `.seen()`)
#### .gone(selector, callback)
Watches for matching elements being removed.
Returns `WatchHandle`.
#### .goneForElement(element, callback)
Watches for a specific element to be removed.
Returns `WatchHandle`.
#### .seen() and .changed() support two usage styles
##### Callback style
Pass a callback to get a WatchHandle
```ts
const handle = tracker.seen('.item', el => { ... });
handle.cancel();
```
#### Promise style
Omit the callback to get a WatchPromise<HTMLElement> that supports fluent chaining:
```ts
tracker.seen('.item')
.once()
.timeout(2000)
.then(el => { ... });
```
The WatchPromise supports:
- `.once()` โ Resolve only on first match
- `.timeout(ms, onTimeout?)` โ Reject after timeout
- `.abortSignal(signal)` โ Cancel via AbortController
- `.then()`, `.catch()`, `.finally()` โ Standard Promise chaining
## โจ Example
```ts
import {DomTrack} from 'dom-track';
// Init tracker on a DOM object
const tracker = new DomTrack(document.body, {debounceMs: 3000});
tracker.seen('.toasts', el => {
console.log('One of many toasts appeared', el)
});
// Limit to once and and set timeout
tracker.seen('.toasts')
.once()
.timeout(3000, ()=>{console.log('Toast didn\'t appear');})
.then(el => {
console.log('One of many toasts appeared', el);
});
// Use external callback
function callback(el:HTMLElement){
console.log('Toast has appeared', el);
}
tracker.seen('.toast').then(callback);
// Watch for changes
tracker.changed('.chat-box')
.once()
.timeout(2000)
.then(el => {
console.log("Chat box content changed once, exiting", el);
});
```
## โก Performance
To keep DomTrack efficient:
- Use `.gone()` / `.goneForElement()` to stop watching removed elements.
- Call `.cancel()` when a watch is no longer needed. Once all watchers of an instance are cancelled, the internally managed `MutationObserver is automatically suspended and reinitialized only when required.
- Use `debounceMs` to reduce how often callbacks are triggered by rapid DOM changes.
- Scope wisely: avoid observing all of `document.body` unless truly necessary. Narrower containers are more performant. This especially applies to `DomTrack` instances with expensive options like `observeAttributes` enabled.
- Combine multiple scoped `DomTrack` instances with a single high-level `DomTrackRemovals` to monitor removals without incurring extra observation overhead.
- Call `.disconnect()` when you're done tracking or if the container element has been removed. `MutationObservers` remain active even after their target is detached, which can lead to wasted processing and memory overhead.
- Test performance in your environment โ use browser dev tools to measure observer impact in context.
### About DomTrackRemovals
`DomTrackRemovals` is a optional lightweight removal-only tracker ideal for watching large containers like `document.body`. It observes only element removals, making it faster than a full DomTrack instance that tracks all mutation types.
Best used when watching removals from a higher-level container in the DOM.
Best practices:
- Use scoped `DomTrack` instances for fine-grained `.seen()` or `.changed()` tracking
- Use `DomTrackRemovals` higher in the DOM to monitor removals efficiently
- Avoid using both on the same broad container โ this will slow down execution unnecessarily
- If you're already using `DomTrack` on `document.body`, `DomTrackRemovals` likely won't add value
#### Example
```html
<body>
<div id="app">
<div id="head">
<div class="toast">Toast 1</div>
</div>
<!-- ...other stuff going on... -->
<div id="content">
<div class="chat-box">Chat content</div>
</div>
</div>
</body>
```
```ts
import { DomTrack, DomTrackRemovals } from 'dom-track';
// Scoped trackers for additions and changes
const trackerHead = new DomTrack(document.getElementById('head')!, { debounceMs: 200 });
trackerHead.seen('.toast', el => console.log('Toast appeared:', el));
const trackerContent = new DomTrack(document.getElementById('content')!, { debounceMs: 200 });
trackerContent.changed('.chat-box', el => console.log('Chat box changed:', el));
// Efficient high-level removal tracker
const removalTracker = new DomTrackRemovals(document.body);
removalTracker.gone('.toast', el => console.log('Toast removed:', el));
removalTracker.gone('.chat-box', el => console.log('Chat box removed:', el));
removalTracker.gone('#app', el => {
console.log('App removed:', el);
trackerHead.disconnect();
trackerContent.disconnect();
});
```
## ๐ก Advanced Usage examples
```ts
const tracker = new DomTrack(document.body);
// Cancel via .cancel()
const toasts = tracker.seen('.toasts')
.then(el => {
console.log('Toast appeared', el);
toasts.cancel();
});
// Cancel the entire tracker
tracker.disconnect();
// Abort via AbortController
const controller = new AbortController();
tracker.seen('.toast')
.abortSignal(controller.signal)
.then(el => {
console.log("Toast appeared", el);
});
controller.abort();
// Cancel a callback-based watcher
const chatBoxWatcher = tracker.changed(".chat-box", (el) => {
console.log("Chat box content changed:", el);
});
chatBoxWatcher.cancel();
// Abort the entire tracker via signal
const controller2 = new AbortController();
const observer2 = new DomTrack(document.body, { signal: controller2.signal });
controller2.abort();
```
## ๐งช DomTrack development
```sh
npm install
npm run test
```
### publish on NPM repo
```sh
npm adduser
npm publish --access public
```
## ๐ License
[GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html)