spyo
Version:
Observe object
196 lines (172 loc) • 4.99 kB
JavaScript
const extend = require('defaulty');
const clone = require('clone');
const arrayme = require('arrayme');
/**
* @class
*/
class Spyo {
/**
* Create instance
* @param obj {Object|Function} object that you want watch
* @param [opts] {Object} configuration object
* @param [opts.autoWatch=true] {boolean} auto watch
* @param [opts.checkMs=50] {number} interval in milliseconds for every check
* @param [opts.provider=null] {Function} optional function called on every check that returns new state
* @param [opts.exclude=null] {String|Array} exclude a property or more from check
* @param [opts.autoReset=false] {boolean} reset changes detected after check
*/
constructor(obj, opts = {}) {
if (typeof obj === 'function') {
opts.provider = obj;
obj = obj();
}
if (!Spyo.isIterable(obj))
throw new TypeError('An object, array or provider function is required');
this.opts = extend.copy(opts, {
autoWatch: true,
checkMs: 50,
provider: null,
exclude: null,
autoReset: false
});
this.opts.exclude = arrayme(this.opts.exclude);
this.obj = obj;
this.objCopy = clone(this.obj);
this._onChange = () => {
};
this._intervalObject = null;
this._lastState = null;
if (this.opts.autoWatch)
this.watch();
}
/**
* Refresh data source object
* @param obj
*/
refresh(obj) {
if (!Spyo.isIterable(obj))
throw new TypeError('An object or an array is required');
this.obj = obj;
}
/**
* Check if it's different and call `onChange` callback
* @returns {Spyo}
*/
check() {
if (typeof this.opts.provider === 'function')
this.refresh(this.opts.provider());
let state = this.isChanged();
if (state !== this._lastState) {
this._lastState = state;
this._onChange.call(this, state, this);
}
if (this.opts.autoReset)
this.reset();
return this;
}
/**
* Start watching
* @returns {Spyo}
*/
watch() {
this._intervalObject = setInterval(() => {
this.check();
}, this.opts.checkMs);
return this;
}
/**
* Stop watching
* @param [reset] {boolean} reset changes detected
* @returns {Spyo}
*/
unwatch(reset) {
clearInterval(this._intervalObject);
this._intervalObject = null;
if (reset)
this.reset();
return this;
}
/**
* Detect if check is active
* @returns {boolean}
*/
isWatching() {
return Boolean(this._intervalObject);
}
/**
* Fired when object is isChanged
* @param callback
* @returns {Spyo}
*/
onChange(callback) {
this._onChange = callback;
return this;
}
/**
* Check if object is changed
* @returns {boolean}
*/
isChanged() {
return !Spyo.isEqual(
this.obj,
this.objCopy,
this.opts.exclude
);
}
/**
* Reset changes detected
* @returns {Spyo}
*/
reset() {
this._lastState = null;
this.objCopy = clone(this.obj);
return this;
}
/**
* Check if two object are equals (deep check)
* @param a {object} first object
* @param b {object} second object
* @param exclude {Array} exclude properties from check
* @returns {boolean}
*/
static isEqual(a, b, exclude = []) {
if (Spyo.isIterable(a) && Spyo.isIterable(b)) {
if (Object.keys(a).length !== Object.keys(b).length)
return false;
for (let prop in a) {
if (exclude.indexOf(prop) === -1)
if (!(a.hasOwnProperty(prop) && b.hasOwnProperty(prop)) || !Spyo.isEqual(a[prop], b[prop], exclude)) {
return false;
}
}
return true;
} else {
return Object.is(a, b);
}
}
/**
* Check if is an array or an plain object
* @param obj
* @returns {boolean}
*/
static isIterable(obj) {
return Spyo.isObject(obj) || Spyo.isArray(obj);
}
/**
* Check for plain object
* @param obj {*}
* @returns {boolean}
*/
static isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
/**
* Check for array
* @param obj {*}
* @returns {boolean}
*/
static isArray(obj) {
return Array.isArray(obj);
}
}
module.exports = Spyo;