object-observer
object-observer
provides a deep observation of a changes performed on an object/array graph.
Main aspects and features:
Observable
configuration; more details hereobserver
configuration; more details hereObservable
s
Array
mutation methods supported: pop
, push
, shift
, unshift
, reverse
, sort
, fill
, splice
, copyWithin
TypedArray
mutation methods supported: reverse
, sort
, fill
, set
, copyWithin
Map
, WeakMap
, Set
, WeakSet
(set
, delete
) etc are not observed (see this issue for more details)Date
Supported: 71+ | 65+ | 79+ | 12.1 | 12.0.0+
Performance report can be found here.
Changelog is here.
For a preview/playground you are welcome to:
Use regular npm install @gullerya/object-observer --save-prod
to use the library from your local environment.
ES module:
import { Observable } from '@gullerya/object-observer';
CJS flavor:
const { Observable } = require('@gullerya/object-observer');
Huge thanks to seidelmartin providing the CJS build while greatly improving the build code overall along the way!
CDN (most suggested, when possible):
import { Observable } from 'https://libs.gullerya.com/object-observer/x.y.z/object-observer.min.js';
Replace the
x.y.z
with the desired version, one of the listed in the changelog.
CDN features:
Full details about CDN usage and example are found here.
Library implements Observable
API as it is defined here.
There is also a ‘DOM-like’ API flavor - constructable ObjectObserver
.
This API is resonating with DOM’s MutationObserver
, ResizeObserver
etc from the syntax perspective.
Under the hood it uses the same Observable
mechanics.
Read docs about this API flavor here.
object-observer
is cross-instance operable.
Observables created by different instances of the library will still be detected correctly as such and handled correctly by any of the instances.
Security policy is described here. If/when any concern raised, please follow the process.
const
order = { type: 'book', pid: 102, ammount: 5, remark: 'remove me' },
observableOrder = Observable.from(order);
Observable.observe(observableOrder, changes => {
changes.forEach(change => {
console.log(change);
});
});
observableOrder.ammount = 7;
// { type: 'update', path: ['ammount'], value: 7, oldValue: 5, object: observableOrder }
observableOrder.address = {
street: 'Str 75',
apt: 29
};
// { type: "insert", path: ['address'], value: { ... }, object: observableOrder }
observableOrder.address.apt = 30;
// { type: "update", path: ['address','apt'], value: 30, oldValue: 29, object: observableOrder.address }
delete observableOrder.remark;
// { type: "delete", path: ['remark'], oldValue: 'remove me', object: observableOrder }
Object.assign(observableOrder, { amount: 1, remark: 'less is more' }, { async: true });
// - by default the changes below would be delivered in a separate callback
// - due to async use, they are delivered as a batch in a single callback
// { type: 'update', path: ['ammount'], value: 1, oldValue: 7, object: observableOrder }
// { type: 'insert', path: ['remark'], value: 'less is more', object: observableOrder }
let a = [ 1, 2, 3, 4, 5 ],
observableA = Observable.from(a);
Observable.observe(observableA, changes => {
changes.forEach(change => {
console.log(change);
});
});
// observableA = [ 1, 2, 3, 4, 5 ]
observableA.pop();
// { type: 'delete', path: [4], value: undefined, oldValue: 5, object: observableA }
// now observableA = [ 1, 2, 3, 4 ]
// following operation will cause a single callback to the observer with an array of 2 changes in it)
observableA.push('a', 'b');
// { type: 'insert', path: [4], value: 'a', oldValue: undefined, object: observableA }
// { type: 'insert', path: [5], value: 'b', oldValue: undefined, object: observableA }
// now observableA = [1, 2, 3, 4, 'a', 'b']
observableA.shift();
// { type: 'delete', path: [0] value: undefined, oldValue: 1, object: observableA }
// now observableA = [ 2, 3, 4, 'a', 'b' ]
// following operation will cause a single callback to the observer with an array of 2 changes in it)
observableA.unshift('x', 'y');
// { type: 'insert', path: [0], value: 'x', oldValue: undefined, object: observableA }
// { type: 'insert', path: [1], value: 'y', oldValue: undefined, object: observableA }
// now observableA = [ 2, 3, 4, 'a', 'b' ]
observableA.reverse();
// { type: 'reverse', path: [], object: observableA } (see below and exampe of this event for nested array)
// now observableA = [ 'b', 'a', 4, 3, 2 ]
observableA.sort();
// { type: 'shuffle', path: [], object: observableA } (see below and exampe of this event for nested array)
// observableA = [ 2, 3, 4, 'a', 'b' ]
observableA.fill(0, 0, 1);
// { type: 'update', path: [0], value: 0, oldValue: 2, object: observableA }
// observableA = [ 0, 3, 4, 'a', 'b' ]
// the following operation will cause a single callback to the observer with an array of 2 changes in it)
observableA.splice(0, 1, 'x', 'y');
// { type: 'update', path: [0], value: 'x', oldValue: 0, object: observableA }
// { type: 'insert', path: [1], value: 'y', oldValue: undefined, object: observableA }
let customer = { orders: [ ... ] },
oCustomer = Observable.from(customer);
// sorting the orders array, pay attention to the path in the event
oCustomer.orders.sort();
// { type: 'shuffle', path: ['orders'], object: oCustomer.orders }
oCustomer.orders.reverse();
// { type: 'reverse', path: ['orders'], object: oCustomer.orders }
Arrays notes: Some of array operations are effectively moving/reindexing the whole array (shift, unshift, splice, reverse, sort). In cases of massive changes touching presumably the whole array I took a pessimistic approach with a special non-detailed events: ‘reverse’ for
reverse
, ‘shuffle’ forsort
. The rest of these methods I’m handling in an optimistic way delivering the changes that are directly related to the method invocation, while leaving out the implicit outcomes like reindexing of the rest of the Array.
object-observer
allows to filter the events delivered to each callback/listener by an optional configuration object passed to the observe
API.
In the examples below assume that
callback = changes => {...}
.
let user = {
firstName: 'Aya',
lastName: 'Guller',
address: {
city: 'of mountaineers',
street: 'of the top ridges',
block: 123,
extra: {
data: {}
}
}
},
oUser = Observable.from(user);
// path
//
// going to observe ONLY the changes of 'firstName'
Observable.observe(oUser, callback, {path: 'firstName'});
// going to observe ONLY the changes of 'address.city'
Observable.observe(oUser, callback, {path: 'address.city'});
// pathsOf
//
// going to observe the changes of 'address' own properties ('city', 'block') but not else
Observable.observe(oUser, callback, {pathsOf: 'address'});
// here we'll be notified on changes of
// address.city
// address.extra
// pathsFrom
//
// going to observe the changes from 'address' and deeper
Observable.observe(oUser, callback, {pathsFrom: 'address'});
// here we'll be notified on changes of
// address
// address.city
// address.extra
// address.extra.data