The Observer API

A web-native object observability API!
Observe and intercept operations on any type of JavaScript objects and arrays, using a notably lightweight and predictable utility-first reactivity API!
Motivation
Table of Contents
Download Options
Use as an npm package:
npm i @webqit/observer
import Observer from '@webqit/observer';;
Use as a script:
<script src="https://unpkg.com/@webqit/observer/dist/main.js"></script>
const Observer = window.webqit.Observer;
An Overview
Note
This is documentation for Observer@2.x
. (Looking for Observer@1.x
?)
Method: Observer.observe()
Observe mutations on any object or array!
Observer.observe( obj, callback[, options = {} ]);
Observer.observe( obj, props, callback[, options = {} ]);
Concept: Observers
Observe arbitrary objects and arrays:
const obj = {};
const abortController = Observer.observe( obj, handleChanges );
const arr = [];
const abortController = Observer.observe( arr, handleChanges );
Now changes will be delivered synchronously - as they happen. (The sync design is discussed shortly.)
function handleChanges( mutations ) {
mutations.forEach( mutation => {
console.log( mutation.type, mutation.key, mutation.value, mutation.oldValue );
} );
}
--> Stop observing at any time by calling abort()
on the returned abortController...
abortController.abort();
...or by using an Abort Signal instance:
const abortController = new AbortController;
Observer.observe( obj, mutations => {
}, { signal: abortController.signal } );
abortController.abort();
Concept: Mutations
Programmatically mutate properties of an object using the Reflect-like set of operators; each operation will be reported by observers:
Observer.set( obj, 'prop0', 'value0' );
Observer.defineProperty( obj, 'prop1', { get: () => 'value1' } );
Observer.deleteProperty( obj, 'prop2' );
Observer.set( arr, 0, 'item0' );
Observer.deleteProperty( arr, 0 );
Beware non-reactive operations:
delete obj.prop0;
obj.prop3 = 'value3';
arr.push( 'item3' );
arr.pop();
--> Enable reactivity on specific properties with literal object accessors - using the Observer.accessorize()
method:
Observer.accessorize( obj );
Observer.accessorize( obj, [ 'prop0', 'prop1', 'prop2' ] );
obj.prop0 = 'value0';
obj.prop1 = 'value1';
obj.prop2 = 'value2';
Observer.accessorize( arr );
Observer.accessorize( arr, [ 0, 1, 2 ] );
arr[ 0 ] = 'item0';
arr[ 1 ] = 'item1';
arr[ 2 ] = 'item2';
arr.unshift( 'new-item0' );
arr.shift();
Beware non-reactive operations:
delete obj.prop0;
obj.prop3 = 'value3';
arr.push( 'item0' );
arr.pop();
--> Enable reactivity on arbitray properties with Proxies - using the Observer.proxy()
method:
const _obj = Observer.proxy( obj );
_obj.prop1 = 'value1';
_obj.prop4 = 'value4';
_obj.prop8 = 'value8';
delete _obj.prop0;
const _arr = Observer.proxy( arr );
_arr[ 0 ] = 'item0';
_arr[ 1 ] = 'item1';
_arr[ 2 ] = 'item2';
_arr.push( 'item3' );
And no problem if you end up nesting the approaches.
Observer.accessorize( obj, [ 'prop0', 'prop1', 'prop2', ] );
obj.prop1 = 'value1';
let _obj = Observer.proxy( obj );
_obj.prop1 = 'value1';
Observer.set( _obj, 'prop1', 'value1' );
--> "Restore" accessorized properties to their normal state by using the unaccessorize()
method:
Observer.unaccessorize( obj, [ 'prop1', 'prop6', 'prop10' ] );
--> "Reproduce" original objects from Proxies obtained via Observer.proxy()
by using the unproxy()
method:
obj = Observer.unproxy( _obj );
Concept: Batch Mutations
Make multiple mutations at a go, and they'll be correctly delivered in batch to observers!
Observer.set( obj, {
prop0: 'value0',
prop1: 'value1',
prop2: 'value2',
} );
Observer.defineProperties( obj, {
prop0: { value: 'value0' },
prop1: { value: 'value1' },
prop2: { get: () => 'value2' },
} );
Observer.deleteProperties( obj, [ 'prop0', 'prop1', 'prop2' ] );
Observer.set( arr, {
'0': 'item0',
'1': 'item1',
'2': 'item2',
} );
Object.proxy( arr ).push( 'item3', 'item4', 'item5', );
Object.proxy( arr ).unshift( 'new-item0' );
Object.proxy( arr ).splice( 0 );
--> Use the Observer.batch()
to batch multiple arbitrary mutations - whether related or not:
Observer.batch( arr, async () => {
Observer.set( arr, 0, 'item0' );
await somePromise();
Observer.set( arr, 2, 'item2' );
} );
Method calls on a proxied instance - e.g. Object.proxy( arr ).splice( 0 )
- also follow this strategy.
Concept: Custom Details
Pass some custom detail - an arbitrary value - to observers via a params.detail
property.
Observer.set( obj, {
prop2: 'value2',
prop3: 'value3',
}, { detail: 'Certain detail' } );
Observers recieve this value on their mutation.detail
property.
Observer.observe( obj, 'prop1', mutation => {
console.log( 'A mutation has been made with detail:' + mutation.detail );
} );
Concept: Diffing
Receive notifications only for mutations that actually change property state, and ignore those that don't.
Observer.observe( obj, handleChanges, { diff: true } );
Observer.set( obj, 'prop0', 'value' );
Observer.set( obj, 'prop0', 'value' );
Method: Observer.intercept()
Intercept operations on any object or array before they happen!
Observer.intercept( obj, prop, handler[, options = {} ]);
Observer.intercept( obj, traps[, options = {} ]);
Concept: Traps
Extend standard operations on an object - Observer.set()
, Observer.deleteProperty()
, etc - with custom traps using the Observer.intercept()
method!
Below, we intercept all "set" operations for an HTTP URL then transform it to an HTTPS URL.
const setTrap = ( operation, previous, next ) => {
if ( operation.key === 'url' && operation.value.startsWith( 'http:' ) ) {
operation.value = operation.value.replace( 'http:', 'https:' );
}
return next();
};
Observer.intercept( obj, 'set', setTrap );
Now, only the first of the following will fly as-is.
Observer.set( obj, 'url', 'https://webqit.io' );
Observer.set( obj, 'url', 'http://webqit.io' );
And below, we intercept all "get" operations for a certain value to trigger a network fetch behind the scenes.
const getTrap = ( operation, previous, next ) => {
if ( operation.key === 'token' ) {
return next( fetch( tokenUrl ) );
}
return next();
};
Observer.intercept( obj, 'get', getTrap );
And all of that can go into one "traps" object:
Observer.intercept( obj, {
get: getTrap,
set: setTrap,
deleteProperty: deletePropertyTrap,
defineProperty: definePropertyTrap,
ownKeys: ownKeysTrap,
has: hasTrap,
} );
The End?
Certainly not! But this rundown should be a good start. Next:
Issues
To report bugs or request features, please submit an issue.
License
MIT.