@webqit/realdom
v2.1.32
Published
Low-level DOM APIs.
Downloads
65
Maintainers
Readme
realdom
A small, low-level utility for working with the (real) DOM in realtime!
Documentation
Info This is (Early stage) documentation for
v2.x
. (Looking forv1.x
?)
- Download Options
- The Ready-State API
- The Realtime Mutations API
- The Render Scheduling API
- Issues
- License
Download Options
Use as an npm package:
npm i @webqit/realdom
// 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:
<script src="https://unpkg.com/@webqit/realdom/dist/main.js"></script>
// 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.
Method: realdom.ready()
Know when the document is ready.
// Signature 1
ready([ callback = undefined ]);
// 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.
// 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.
// 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 - 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.
// Binding to the document's "complete" ready state
ready( 'complete', () => console.log( 'Document "ready state" is now "complete"' ) );
// 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()
) or in realtime (you letting the DOM call you... as you would using the MutationObserver API).
Method: realdom.realtime( context ).observe()
Observe the (real) DOM in realtime!
// Signature 1
realtime( context ).observe( callback[, options = {} ]);
// Signature 2
realtime( context ).observe( targets, callback[, options = {} ]);
Concept: Scope
Report all direct children additions and removals to/from the given element - the context:
// Observing all direct children mutations
realtime( document.body ).observe( handleChanges );
--> Observe entire subtree of the given element using the options.subtree
flag:
// Observing all subtree mutations
realtime( document.body ).observe( handleChanges, { subtree: true } );
Info You'd normally always need this flag when the
document
object is the context.
Concept: Targets
Pass in a CSS selector to match elements in realtime; e.g. "p" for all <p>
elements:
// Observing only "p" elements mutations
realtime( document.body ).observe( 'p', handleChanges, { subtree: true } );
...whether "p" elements added via markup:
// and whether or not it's deeply nested as par { subtree: true }:
document.body.innerHTML = '<div><p></p></div>';
...or "p" elements added programmatically:
// 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.
// 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. Theoptions.subtree
parameter is how you specify the resolution context for your queries.
--> Observe element instances as targets too. (E.g. a "p" instance.)
// 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:
// 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:
// either via an overwrite... (indirect overwrite in this case)...
document.body.innerHTML = '';
// 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:
// 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:
// Requiring only the "entrants" list
realtime( document.body ).observe( handleChanges, { generation: 'entrants' } );
// 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:
// 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:
// 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:
// The following "p" element suddenly matches and is reported (record.entrants)
document.querySelector( 'p' ).setAttribute( 'draggable', 'true' );
// 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:
// Requiring that event details be added
realtime( document.body ).observe( 'p[draggable="true"]', handleChanges, { eventDetails: true } );
// Inspecting record.event
function handleChanges( record ) {
console.log( record.event );
}
You get an array in the format: [ HTMLBodyElement, 'appendChild' ]
- for mutations that happen programatically:
// 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>
<head>
<script>
realdom.realtime( document ).observe( 'meta[foo]', handleChanges, { subtree: true } );
</script>
<meta name="foo" content="bar">
<!--
At this point in the document parsing, the meta element is now reported, and record.event is: "parse"
-->
<script>
const meta2 = document.createElement( 'meta' );
meta2.name = 'foo';
meta2.content = 'baz';
document.head.appendChild( meta2 );
</script>
<!--
At this point in the document parsing, the meta2 element is now reported, and record.event is: [ HTMLHeadElement, 'appendChild' ]
-->
</head>
</html>
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 that you can use to abort your mutation listener at any time:
// Providing an AbortSignal
const abortController = new AbortController;
realtime( document.body ).observe( 'p', handleChanges, { signal: abortController.signal } );
// 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.
// 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 } );
// 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:
// 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 } );
// 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 API. This means that there's a small lag between when mutations happen and when they are delivered.
// Observing with "asynchronous" timing
let deliveredElement;
realtime( document.body ).observe( 'p', record => {
deliveredElement = record.entrants[ 0 ];
} );
// Confirming the "async" delivery
const pElement = document.createElement( 'p' );
document.body.appendChild( pElement );
console.log( pElement.isConnected ); // true
console.log( deliveredElement ); // undefined
// Estimating delivery timing
setTimeout( () => {
console.log( deliveredElement ); // HTMLParagraphElement
}, 0 );
--> Use the options.timing = "sync"
parameter to observe mutations synchronously:
// Opting in to "synchronous" timing
let deliveredElement;
realtime( document.body ).observe( 'p', record => {
deliveredElement = record.entrants[ 0 ];
}, { timing: 'sync' } );
// 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 <script>
elements before they're parsed and executed if you could intercept them.
--> Use the options.timing = "intercept"
parameter to observe mutations before they actually happen:
// Trying the "intercept" timing
realtime( document.body ).observe( 'script', handleScripts, { timing: 'intercept' } );
// Making the mutation
const scriptElement = document.createElement( 'script' );
document.body.appendChild( scriptElement );
// 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 API, interception also works at parse time for mutation listeners created early enough:
<html>
<head>
<script>
realdom.realtime( document ).observe( 'script[rewriteme]', handleScripts, { timing: 'intercept' } );
</script>
<script rewriteme>
alert( 'Hello world!' );
</script>
<!--
At this point in the document parsing, the script[rewriteme] element is now reported, and rewritten
But note that this time, element was just already in the DOM, only yet to be handled. So...
console.log( deliveredElement.isConnected ); // true
console.log( deliveredElement.parentNode ); // HTMLHeadElement
console.log( record.event ); // "parse"
-->
</head>
</html>
Method: realdom.realtime( context ).query()
Work with the (real) DOM both on-demand (as you would with querySelectorAll()
), and in realtime (as you would with realtime().observe()
).
// Signature 1
realtime( context ).query([ callback = undefined[, options = {} ]]);
// Signature 2
realtime( context ).query( targets[, callback = undefined[, options = {} ]]);
// Signature 3
const records = realtime( context ).query([ options = {} ]);
Concept: Scope
Get all direct children of the given element delivered:
// Getting all children delivered to a callback
realtime( document.body ).query( handleResult );
// Retreiving all children
const records = realtime( document.body ).query();
// Deligating to handler
records.forEach( record => handleResult( record ) );
--> Use the realtime().children()
alias:
// Delivering, using the .children() alias
realtime( document.body ).children( handleResult );
// Retreiving, using the .children() alias
const records = realtime( document.body ).children();
// Deligating to handler
records.forEach( record => handleResult( record ) );
Info Using the
options.subtree
flag without specifying targets produces no result.
Concept: Targets
Pass in a CSS selector to match elements: e.g. "p" for all <p>
elements:
// Matching just "p" children
realtime( document.body ).query( 'p', handleResult );
// Using the .children() alias
realtime( document.body ).children( 'p', handleResult );
// Retreiving, using the .children() alias
const records = realtime( document.body ).children( 'p' );
// Deligating to handler
records.forEach( record => handleResult( record ) );
--> Use the options.subtree
flag to query the entire subtree.
// Using the options.subtree flag
realtime( document.body ).query( 'p', handleResult, { subtree: true } );
--> Use the realtime().subtree()
alias:
// Using the .subtree() alias
realtime( document.body ).subtree( 'p', handleResult );
// Retreiving, using the .subtree() alias
const records = realtime( document.body ).subtree( 'p' );
// Deligating to handler
records.forEach( record => handleResult( record ) );
--> 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. Xpath expressions must be enclosed in parentheses.
// Query all "comment" nodes having a certain content
realtime( document.body ).query( '(comment()[contains(., "hello world")])', handleResult, { subtree: true } );
Info: Note that Xpath expressions must not be prefixed with the direct children
/
or descendant//
qualifiers as that is controlled internally. Theoptions.subtree
parameter is how you specify the resolution context for your queries.
Concept: Records
Handle query result records in the same way as mutation records:
// Handling query result
function handleResult( record ) {
// record.entrants is the list of matched nodes
for ( const matchedNode of record.entrants ) {
console.log( 'matched:', matchedNode );
}
console.log( record.exits ); // Always an empty array
console.log( record.event ); // Always the keyword: "query"
}
Info Setting the
options.generation
parameter toexits
effectively defies the logic, thus no query actually happens.
--> Use the record.target
property to access the query context for the record:
// Inspecting record.target
function handleResult( record ) {
console.log( record.target ); // HTMLBodyElement
}
Info Elements are organized into records by common parent. Thus, an expression like
realtime().subtree( 'p' )
on the below will produce 2 records, with 2 entrants per record:<html> <body> <section> <p></p> <p></p> </section> <p></p> <p></p> </body> </html>
Concept: Realtime Queries
Get both current and future elements delivered to the same handler function:
// Matching all current "p[draggable]" elements
realtime( document ).query( 'p[draggable]', handleDraggables, { subtree: true } );
// Subscribe to future matching elements
realtime( document ).observe( 'p[draggable]', handleDraggables, { subtree: true } );
--> Use the options.live
flag to achieve the same:
// Matching all current "p[draggable]" elements and staying subscribed
realtime( document ).query( 'p[draggable]', handleDraggables, { subtree: true, live: true } );
--> Use equivalent APIs the same way:
// Using the .subtree() alias
realtime( document ).subtree( 'p[draggable]', handleDraggables, { live: true } );
// Using the .children() alias
realtime( document ).children( 'p[draggable]', handleDraggables, { live: true } );
--> Use the options.live
flag in conjunction with other concepts:
// Using the options.live flag
const abortController = new AbortController;
realtime( document ).children( 'p[draggable]', handleDraggables, {
live: true,
signal: abortController.signal,
lifecycleSignals: true,
staticSensitivity: true,
timing: 'sync',
eventDetails: true,
} );
Method: realdom.realtime( context, 'attr' ).observe()
Observe DOM attributes in realtime!
// Signature 1
realtime( context, 'attr' ).observe( callback[, options = {} ]);
// Signature 2
realtime( context, 'attr' ).observe( targets, callback[, options = {} ]);
Concept: Scope
Report all attribute changes on the given element:
// Observing all attributes
realtime( document.body, 'attr' ).observe( handleChanges );
--> Observe all attribute changes across the entire subtree of the given context using the options.subtree
flag:
// Observing entire subtree
realtime( document.body, 'attr' ).observe( handleChanges, { subtree: true } );
Info You'd normally always need this flag when the
document
object is the context.
Concept: Targets
Observe specific attributes on the given context:
// Observing the "draggable" attribute
realtime( element, 'attr' ).observe( [ 'draggable' ], handleChanges );
Concept: Records
Handle mutation records - an array of attribute-change records:
// Handling mutation records
function handleChanges( records ) {
for ( const record of records ) {
console.log( record.name );
}
}
--> Use the options.oldValue
and options.newValue
flags to require attribute's old and new values respectively:
// Requiring attribute's old and new value
realtime( element, 'attr' ).observe( [ 'draggable' ], handleChanges, { newValue: true, oldValue: true } );
// Inspecting attribute's old and new value
function handleChanges( records ) {
for ( const record of records ) {
console.log( record.name, record.value, record.oldValue );
}
}
--> Use the record.target
property to access the mutation target - the element on which mutation happened:
// Inspecting record.target
function handleChanges( records ) {
console.log( records[ 0 ].target );
}
--> Where exactly one attribute is being observed and is passed as a string instead of an array, mutation records are delivered in singular form instead of as an array:
// Passing the observed attribute as a bare string instead of an array
realtime( element, 'attr' ).observe( 'draggable', handleChanges );
// Receiving records in singular form instead of as an array
function handleChanges( record ) {
console.log( record.name );
}
Concept: Atomic Delivery
Get records for multiple attributes delivered atomically - in whole - whenever any of the attributes change:
// Observing multiple attributes atomically
realtime( element, 'attr' ).observe( [ 'attr1', 'attr2', 'attr3' ], handleChanges, { atomic: true } );
// Receiving all 3 records anytime any one of them changes
function handleChanges( records ) {
const [ attr1, attr2, attr3 ] = records;
}
Concept: Event Details
Use the option.eventDetails
flag to require the actual DOM operation that happened under the hood:
// Requiring that event details be added
realtime( element, 'attr' ).observe( [ 'draggable' ], handleChanges, { eventDetails: true } );
// Inspecting record.event
function handleChanges( records ) {
console.log( records[ 0 ].event );
}
You get an array in the format: [ HTMLInputElement, 'toggleAttribute' ]
- for mutations that happen programatically:
// Running an operation
element.toggleAttribute( 'required' );
You get the keyword: mutation
- for mutations that happen in other ways; e.g in when the user directly alters an element's attribute from the browser console.
Concept: Abort Signals
Pass in an Abort Signal that you can use to abort your mutation listener at any time:
// Providing an AbortSignal
const abortController = new AbortController;
realtime( element, 'attr' ).observe( [ 'required' ], { signal: abortController.signal } );
// Abort at any time
abortController.abort();
Concept: Life Cycle Signals
When dealing with nested event listeners (see above), use the options.lifecycleSignals
parameter to opt in to receiving auto-generated signals for tying nested listeners:
// Managing nested lifecycles using automatic lifecycle signals
const parentAbortController = new AbortController;
realtime( element, 'attr' ).observe( [ 'draggable' ], ( records, flags ) => {
if ( records[ 0 ].value === 'true' ) {
element.addEventListener( 'drag', handleDrag, { signal: flags.signal } );
}
}, { newValue: true, signal: parentAbortController.signal, lifecycleSignals: true } );
// Abort parent at any time
parentAbortController.abort();
// The latest flags.signal instance is also automatically aborted
Concept: Timing
For when timing is critical (see above), use the options.timing = "sync"
parameter to observe mutations synchronously:
// Opting in to "synchronous" timing
realtime( element, 'attr' ).observe( [ 'draggable' ], records => {
// Handle records
}, { timing: 'sync' } );
--> Use the options.timing = "intercept"
parameter to observe attribute mutations before they actually happen:
// Opting in to "synchronous" timing
realtime( img, 'attr' ).observe( [ 'src' ], records => {
// Handle records
}, { timing: 'intercept' } );
Method: realdom.realtime( context, 'attr' ).get()
Work with attributes both on-demand and in realtime (as you would with realtime( element, 'attr' ).observe()
).
// Signature 1
realtime( context, 'attr' ).get([ callback = undefined[, options = {} ]]);
// Signature 2
realtime( context, 'attr' ).get( targets[, callback = undefined[, options = {} ]]);
// Signature 3
const records = realtime( context, 'attr' ).get([ options = {} ]);
Concept: Scope
Get all attributes on the given element:
// Getting all attributes delivered to a handler
realtime( document.body, 'attr' ).get( handleChanges );
// Retreiving all attributes
const records = realtime( document.body, 'attr' ).get();
// Deligating to handler
handleChanges( records );
Concept: Targets
Get specific attributes on the given context:
// Getting the "draggable" attributes delivered to a handler
realtime( element, 'attr' ).get( [ 'draggable' ], handleChanges );
// Retreiving the "draggable" attributes
const records = realtime( document.body, 'attr' ).get( [ 'draggable' ] );
// Deligating to handler
handleChanges( records );
Concept: Realtime Attributes
Get both current and future attributes delivered to the same handler function:
// Getting the "draggable" attributes delivered to a handler
realtime( element, 'attr' ).get( [ 'draggable' ], handleChanges );
// Observing future "draggable" attribute changes
realtime( element, 'attr' ).observe( [ 'draggable' ], handleChanges );
--> Use the options.live
flag to achieve the same:
// Getting current and future state of the "draggable" attributes delivered to a handler
realtime( element, 'attr' ).get( [ 'draggable' ], handleChanges, { live: true } );
--> Use the options.live
flag in conjunction with other concepts:
// Using the options.live flag with other flags
const abortController = new AbortController;
realtime( element, 'attr' ).get( [ 'attr1', 'attr2', 'attr3' ], handleChanges, {
newValue: true,
oldValue: true,
atomic: true,
live: true,
signal: abortController.signal,
lifecycleSignals: true,
timing: 'sync',
eventDetailsL true,
} );
Method: realdom.realtime( context ).attr()
An alias for realtime( context, 'attr' ).get()
.
Implementation Notes
The
realtime
API's unique timing capabilities is based on literal interception of DOM APIs. And here is the complete list of them:Node
:insertBefore
,replaceChild
,removeChild
,appendChild
,textContent
,nodeValue
.Element
:insertAdjacentElement
,insertAdjacentHTML
,setHTML
,replaceChildren
,replaceWith
,remove
.before
,after
,append
,prepend
,toggleAttribute
,removeAttribute
,setAttribute
.HTMLElement
:outerText
,innerText
.
You may need to consider this caveat on your specific usecase.
The Render Scheduling API
Eliminate layout thrashing by scheduling DOM read/write operations. (Compare fastdom)
schedule( 'read', () => {
console.log( 'reading phase of the UI' );
} );
schedule( 'write', () => {
console.log( 'writing phase of the UI' );
} );
schedule( 'read', () => {
console.log( 'reading phase of the UI' );
} );
schedule( 'write', () => {
console.log( 'writing phase of the UI' );
} );
reading phase of the UI
reading phase of the UI
writing phase of the UI
writing phase of the UI
Concept
The schedule
API works as a regulatory layer between your app/library and the DOM. It lets you think of the DOM in terms of a "reading" phase and a "writing" phase, and lets you hook into this cycle when working with the DOM: schedule( 'read', ... )
for doing "read" operations, and schedule( 'write', ... )
for doing "write" operations. Batching DOM operations this way lets us avoid unnecessary document reflows and dramatically speeds up layout performance.
Each read/write operation is added to a corresponding read/write queue. The queues are emptied (reads, then writes) at the turn of the next frame using
window.requestAnimationFrame
.
Method: realdom.schedule( 'read', ... )
// Signature
schedule( 'read', readCallback[, inPromiseMode = false ]);
Schedules a job for the "read" phase. Can return a promise that resolves when job eventually executes; you ask for a promise by supplying true
as a second argument.
const promise = schedule( 'read', () => {
const width = element.clientWidth;
}, true/*give back a promise*/ );
Method: realdom.schedule( 'write', ... )
// Signature
schedule( 'write', writeCallback[, inPromiseMode = false ]);
Schedules a job for the "write" phase. Can return a promise that resolves when job eventually executes; you ask for a promise by supplying true
as a second argument.
const promise = schedule( 'write', () => {
element.style.width = width + 'px';
}, true/*give back a promise*/ );
Method: realdom.schedule( 'cycle', ... )
// Signature
schedule( 'cycle', readCallback, writeCallback );
Puts your read/write operations in a cycle that keeps in sync with the UI's read/write cycle.
schedule( 'cycle',
// onread
() => {
// Do a read operation
const width = element.clientWidth;
// Now if we return anything other than undefined, the "onwrite" block is executed
return width; // recieved by the "onwrite" callback on its first parameter
},
// onwrite
( width, carried ) => {
// Do a write operation
element.style.width = width + 'px';
// Now if we return anything other than undefined, the cycle repeats starting with the "onread" block
return newCarry; // recieved by the "onwrite" block again on its second parameter: "carried"
}
);
Issues
To report bugs or request features, please submit an issue.
License
MIT.