https://github.com/webqit/realdom
Low-level realtime DOM APIs
https://github.com/webqit/realdom
Last synced: 10 months ago
JSON representation
Low-level realtime DOM APIs
- Host: GitHub
- URL: https://github.com/webqit/realdom
- Owner: webqit
- License: mit
- Created: 2022-07-15T07:49:08.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2025-05-20T11:49:49.000Z (about 1 year ago)
- Last Synced: 2025-09-07T08:23:46.009Z (10 months ago)
- Language: JavaScript
- Size: 8.62 MB
- Stars: 10
- Watchers: 1
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# realdom
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href]
[![License][license-src]][license-href]
A small, low-level utility for working with the (real) DOM in realtime!
## Documentation
> **Info** This is *(Early stage)* documentation for `v2.x`. (Looking for [`v1.x`](https://github.com/webqit/realdom/tree/v1.0.3)?)
+ [Download Options](#download-options)
+ [The Ready-State API](#the-ready-state-api)
+ [Method: `realdom.ready()`](#method-realdomready)
+ [The Realtime Mutations API](#the-realtime-mutations-api)
+ [Method: `realdom.realtime( context ).observe()`](#method-realdomrealtime-context-observe)
+ [Concept: *Scope*](#concept-scope)
+ [Concept: *Targets*](#concept-targets)
+ [Concept: *Records*](#concept-records)
+ [Concept: *Static Sensitivity*](#concept-static-sensitivity)
+ [Concept: *Event Details*](#concept-event-details)
+ [Concept: *Abort Signals*](#concept-abort-signals)
+ [Concept: *Life Cycle Signals*](#concept-life-cycle-signals)
+ [Concept: *Timing*](#concept-timing)
+ [Method: `realdom.realtime( context ).query()`](#method-realdomrealtime-context-query)
+ [Concept: *Scope*](#concept-scope-1)
+ [Concept: *Targets*](#concept-targets-1)
+ [Concept: *Records*](#concept-records-1)
+ [Concept: *Realtime Queries*](#concept-realtime-queries)
+ [Method: `realdom.realtime( context, 'attr' ).observe()`](#method-realdomrealtime-context-attr-observe)
+ [Concept: *Scope*](#concept-scope-2)
+ [Concept: *Targets*](#concept-targets-2)
+ [Concept: *Records*](#concept-records-2)
+ [Concept: *Atomic Delivery*](#concept-atomic-delivery)
+ [Concept: *Event Details*](#concept-event-details-1)
+ [Concept: *Abort Signals*](#concept-abort-signals-1)
+ [Concept: *Life Cycle Signals*](#concept-life-cycle-signals-1)
+ [Concept: *Timing*](#concept-timing-1)
+ [Method: `realdom.realtime( context, 'attr' ).get()`](#method-realdomrealtime-context-attr-get)
+ [Concept: *Scope*](#concept-scope-3)
+ [Concept: *Targets*](#concept-targets-3)
+ [Concept: *Realtime Attributes*](#concept-realtime-attributes)
+ [Method: `realdom.realtime( context ).attr()`](#method-realdomrealtime-context-attr)
+ [Implementation Notes](#implementation-notes)
+ [The Render Scheduling API](#the-render-scheduling-api)
+ [Method: `realdom.schedule( 'read', ... )`](#method-realdomschedule-read--)
+ [Method: `realdom.schedule( 'write', ... )`](#method-realdomschedule-write--)
+ [Method: `realdom.schedule( 'cycle', ... )`](#method-realdomschedule-cycle--)
+ [Issues](#issues)
+ [License](#license)
## Download Options
**_Use as an npm package:_**
```bash
npm i @webqit/realdom
```
```js
// Import
import init from '@webqit/realdom';
// Initialize the lib
init.call( window );
// Obtain the APIs
const { ready, realtime, schedule } = window.webqit.realdom;
```
**_Use as a script:_**
```html
```
```js
// Obtain the APIs
const { ready, realtime, schedule } = window.webqit.realdom;
```
## The Ready-State API
Know when the document is ready! This is a simplistic API for working with the document's [ready state](https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState).
### Method: `realdom.ready()`
Know when the document is ready.
```js
// Signature 1
ready([ callback = undefined ]);
```
```js
// Signature 2
ready([ timing = 'interactive'[, callback = undefined ]]);
```
The `ready()` function takes a callback function to be called at a certain document-ready state. This function receives the `window` object.
```js
// Binding to the document's ready state
ready( window => console.log( `Document "ready state" is now "interactive"` ) );
```
*When no callback function is provided, a promise is returned.*
```js
// Awaiting the document's ready state
await ready();
console.log( `Document "ready state" is now "interactive"` );
```
**-->** Use the two-parameter syntax to specify the `timing` - i.e. *[ready state](https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState)* - at which to be called. This can be either of two values:
+ `interactive` - *(The default)* The point at which the document has finished loading and the document has been parsed but sub-resources such as scripts, images, stylesheets and frames are still loading.
+ `complete` - The point at which the document and all sub-resources have finished loading.
```js
// Binding to the document's "complete" ready state
ready( 'complete', () => console.log( 'Document "ready state" is now "complete"' ) );
```
```js
// Awaiting the document's "complete" ready state
await ready( 'complete' );
console.log( `Document "ready state" is now "complete"` );
```
## The Realtime Mutations API
React to realtime DOM operations! This is a set of succint and consistent methods for accessing the DOM - either on-demand (you calling the DOM... as you would using [`querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)) or in realtime (you letting the DOM call you... as you would using the [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) API).
### Method: `realdom.realtime( context ).observe()`
Observe the (real) DOM in realtime!
```js
// Signature 1
realtime( context ).observe( callback[, options = {} ]);
```
```js
// Signature 2
realtime( context ).observe( targets, callback[, options = {} ]);
```
### Concept: *Scope*
Report all *direct children* additions and removals to/from the given element - the context:
```js
// Observing all direct children mutations
realtime( document.body ).observe( handleChanges );
```
**-->** Observe entire *subtree* of the given element using the `options.subtree` flag:
```js
// Observing all subtree mutations
realtime( document.body ).observe( handleChanges, { subtree: true } );
```
> **Info** Given that only direct children are covered without the `options.subtree` flag, you'd normally always need this flag when the `document` object is the *context*.
**-->** Observe into Shadow Roots of the given element, however nested, using the `options.subtree = 'cross-roots'` flag:
```js
// Observing all subtree mutations
realtime( document.body ).observe( handleChanges, { subtree: 'cross-roots' } );
```
### Concept: *Targets*
Pass in a CSS selector to match elements in realtime; e.g. "p" for all `
` elements:
```js
// Observing only "p" elements mutations
realtime( document.body ).observe( 'p', handleChanges, { subtree: true } );
```
...whether "p" elements added via markup:
```js
// and whether or not it's deeply nested as par { subtree: true }:
document.body.innerHTML = '
';
```
...or "p" elements added programmatically:
```js
// and whether or not it's deeply nested as par { subtree: true }:
const p = document.createElement( 'p' );
const div = document.createElement( 'div' );
div.appendChild( p );
document.body.appendChild( div );
```
**-->** Pass in an Xpath query to express what can't be expressed in a CSS selector; e.g. for when you need to match text and comment nodes in realtime. Xpath expressions must be enclosed in parentheses.
```js
// Observing all "comment" nodes having a certain content
realtime( document.body ).observe( '(comment()[contains(., "hello world")])', handleChanges, { subtree: true } );
```
> **Info** Note that Xpath expressions must not be prefixed with the direct children `/` or descendant `//` qualifiers as that is controlled internally. The `options.subtree` parameter is how you specify the resolution context for your queries.
**-->** Observe element instances as targets too. (E.g. a "p" instance.)
```js
// observing an instance plus a selector
const pElement = document.createElement( 'p' );
realtime( document.body ).observe( [ pElement, orCssSelector ], handleChanges, { subtree: true } );
```
...both for when they're added:
```js
// and whether or not it's deeply nested as par { subtree: true }:
const div = document.createElement( 'div' );
div.appendChild( pElement );
document.body.appendChild( div );
```
...and when they're removed:
```js
// either via an overwrite... (indirect overwrite in this case)...
document.body.innerHTML = '';
```
```js
// or via some programmatic means... (indirect removal in this case)...
document.querySelector( 'div' ).remove();
```
### Concept: *Records*
Handle mutation records - each having an `entrants` and an `exits` array property, representing added and removed nodes respectively:
```js
// Handling changes
function handleChanges( record ) {
for ( const addedNode of record.entrants ) {
console.log( 'added:', addedNode );
}
for ( const removedNode of record.exits ) {
console.log( 'removed:', removedNode );
}
}
```
**-->** Use the `options.generation` parameter to require only either the `entrants` or `exits` list:
```js
// Requiring only the "entrants" list
realtime( document.body ).observe( handleChanges, { generation: 'entrants' } );
```
```js
// Handling just record.entrants
function handleChanges( record, context ) {
for ( const addedNode of record.entrants ) {
console.log( 'added:', addedNode );
}
console.log( record.exits ); // Empty array
}
```
**-->** Use the `record.target` property to access the *mutation target* - often, the parent element under which mutation happened:
```js
// Inspecting record.target
function handleChanges( record ) {
console.log( record.target ); // HTMLBodyElement
}
```
### Concept: *Static Sensitivity*
When targeting elements using *attribute selectors*, use the `options.staticSensitivity` flag to opt in to statically matching elements based on the attributes mentioned in the selector:
```js
// Adding the options.staticSensitivity flag
realtime( document.body ).observe( 'p[draggable="true"]', handleChanges, { staticSensitivity: true } );
```
*Now, "p" elements are matched for `[draggable="true"]` in their static state too:*
```js
// The following "p" element suddenly matches and is reported (record.entrants)
document.querySelector( 'p' ).setAttribute( 'draggable', 'true' );
```
```js
// The following "p" element suddenly doesn't match and is reported (record.exits)
document.querySelector( 'p' ).setAttribute( 'draggable', 'false' );
```
### Concept: *Event Details*
Use the `option.eventDetails` flag to require the actual DOM operation that happened under the hood:
```js
// Requiring that event details be added
realtime( document.body ).observe( 'p[draggable="true"]', handleChanges, { eventDetails: true } );
```
```js
// Inspecting record.event
function handleChanges( record ) {
console.log( record.event );
}
```
*You get an array in the format: `[ HTMLBodyElement, 'appendChild' ]` - for mutations that happen programatically:*
```js
// Running an operation
document.body.appendChild( pElement );
```
*You get the keyword: `parse` - for elements recorded directly from the HTML parser while the document loads. (This happens for mutation listeners created early in the document tree.):*
```html
realdom.realtime( document ).observe( 'meta[foo]', handleChanges, { subtree: true } );
const meta2 = document.createElement( 'meta' );
meta2.name = 'foo';
meta2.content = 'baz';
document.head.appendChild( meta2 );
```
*You get the keyword: `mutation` - for mutations that happen in other ways; e.g in when the user directly alters the DOM tree from the browser console.*
### Concept: *Abort Signals*
Pass in an [Abort Signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that you can use to abort your mutation listener at any time:
```js
// Providing an AbortSignal
const abortController = new AbortController;
realtime( document.body ).observe( 'p', handleChanges, { signal: abortController.signal } );
```
```js
// Abort at any time
abortController.abort();
```
### Concept: *Life Cycle Signals*
When dealing with nested event listeners - event handlers that themselves create event listeners, tying child listeners' lifecycle to parent's lifecycle can be cumbersome.
```js
// Managing nested lifecycles using multiple AbortSignals
const parentAbortController = new AbortController;
let recursionAbortController;
realtime( document.body ).observe( 'p', record => {
// Abort all nested listeners in "previous" recursion
recursionAbortController?.abort();
// Create a new AbortController for listeners in "this" recursion
recursionAbortController = new AbortController;
for( const addedNode of record.entrants ) {
addedNode.addEventListener( 'click', handleClick, { signal: recursionAbortController.signal } );
}
}, { signal: parentAbortController.signal } );
```
```js
// Abort parent at any time
parentAbortController.abort();
// Abort the latest instance of recursionAbortController
recursionAbortController?.abort();
```
**-->** Use the `options.lifecycleSignals` parameter to opt in to receiving auto-generated signals for tying nested listeners:
```js
// Managing nested lifecycles using automatic lifecycle signals
const parentAbortController = new AbortController;
realtime( document.body ).observe( 'p', ( record, flags ) => {
for( const addedNode of record.entrants ) {
addedNode.addEventListener( 'click', handleClick, { signal: flags.signal } );
}
}, { signal: parentAbortController.signal, lifecycleSignals: true } );
```
```js
// Abort parent at any time
parentAbortController.abort();
// The latest flags.signal instance is also automatically aborted
```
### Concept: *Timing*
For when timing is everything, meet the `options.timing` parameter!
*By default, mutation records are delivered at the "async" timing of the [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) API. This means that there's a small lag between when mutations happen and when they are delivered.*
```js
// Observing with "asynchronous" timing
let deliveredElement;
realtime( document.body ).observe( 'p', record => {
deliveredElement = record.entrants[ 0 ];
} );
```
```js
// Confirming the "async" delivery
const pElement = document.createElement( 'p' );
document.body.appendChild( pElement );
console.log( pElement.isConnected ); // true
console.log( deliveredElement ); // undefined
```
```js
// Estimating delivery timing
setTimeout( () => {
console.log( deliveredElement ); // HTMLParagraphElement
}, 0 );
```
**-->** Use the `options.timing = "sync"` parameter to observe mutations *synchronously*:
```js
// Opting in to "synchronous" timing
let deliveredElement;
realtime( document.body ).observe( 'p', record => {
deliveredElement = record.entrants[ 0 ];
}, { timing: 'sync' } );
```
```js
// Confirming the "sync" delivery
const pElement = document.createElement( 'p' );
document.body.appendChild( pElement );
console.log( pElement.isConnected ); // true
console.log( deliveredElement ); // HTMLParagraphElement
```
*There is also a rare case where a tool needs to extend the DOM in more low-level ways, and this time, needs to *intercept* certain mutations before they actually happen. For example, you could only really [rewrite `` elements](https://github.com/webqit/oohtml#scoped-js) before they're parsed and executed if you could *intercept* them.*
**-->** Use the `options.timing = "intercept"` parameter to observe mutations *before* they actually happen:
```js
// Trying the "intercept" timing
realtime( document.body ).observe( 'script', handleScripts, { timing: 'intercept' } );
```
```js
// Making the mutation
const scriptElement = document.createElement( 'script' );
document.body.appendChild( scriptElement );
```
```js
// Confirming the "intercpted" delivery
function handleScripts( record ) {
const deliveredElement = record.entrants[ 0 ];
// We're receiving an element that is only just about to be added to the DOM
console.log( deliveredElement.isConnected ); // false
console.log( deliveredElement.parentNode ); // null
console.log( record.event ); // [ HTMLBodyElement, 'appendChild' ]
// We can rewrite this script
deliveredElement.text = 'alert( "Tada!" )';
}
```
*And thanks to the [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) API, interception also works at parse time for mutation listeners created early enough:*
```html
<html>
<head>
<script>
realdom.realtime( document ).observe( 'script[rewriteme]', handleScripts, { timing: 'intercept' } );
alert( 'Hello world!' );
>
>
>
>
>
>
>
>