Last active
February 20, 2019 05:05
-
-
Save wmcmurray/87c3b45ccd38e290ab4519788f82fb50 to your computer and use it in GitHub Desktop.
Watch changes on objects using Proxy and Reflect (with an API very similar to VueJS watch mechanism)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import _get from 'lodash/get' | |
import _set from 'lodash/set' | |
import _cloneDeep from 'lodash/cloneDeep' | |
import _isArray from 'lodash/isArray' | |
/** | |
* A system to watch for changes on an object (with an API very simillar to VueJS watch mechanism) | |
* @param {Object} obj The object to watch changes on | |
* @return {Object} Return the proxied object, which you can modify like a normal object | |
* @TODO never tested with an array instead of object, should it work ? | |
* @TODO never tested the unwatch | |
* @TODO implement some sanitization of data observed | |
* @TODO do some performance benchmarks | |
*/ | |
const Observable = function(obj) { | |
var cbs = {}; | |
const watch = function(path, cb, options) { | |
// reorder arguments in case path was not defined | |
if(typeof path === 'function' && arguments.length <= 2){ | |
options = cb; | |
cb = path; | |
path = ''; | |
} | |
if(typeof cb === 'function'){ | |
if(typeof options === 'object'){ | |
if(typeof options.immediate === 'boolean' && options.immediate){ | |
var cObj = _cloneDeep(obj); | |
if(path){ | |
cb(_get(cObj, path), undefined, path); | |
} else { | |
cb(cObj, undefined, path); | |
} | |
} | |
if(typeof options.deep === 'boolean' && options.deep){ | |
cb._watchMeDeepPlease = true; | |
} | |
} | |
if(typeof cbs[path] === 'undefined'){ | |
cbs[path] = []; | |
} | |
cbs[path].push(cb); | |
} | |
} | |
const unwatch = function(path, cb) { | |
if(typeof cbs[path] !== 'undefined'){ | |
var index = cbs[path].indexOf(cb); | |
if(index !== -1) { | |
cbs[path].splice(index, 1); | |
} | |
} | |
} | |
const onChange = function(path, nv, ov) { | |
var parts = path.split('.'); | |
var total = parts.length; | |
var nObj = _cloneDeep(obj); | |
var oObj = _set(_cloneDeep(obj), path, ov); | |
var loopPath, ccbs, ccb; | |
// "bubble up" the path to trigger watchers from bottom > up | |
for(var i = 0; i <= total; i++){ | |
loopPath = parts.join('.'); | |
if(typeof cbs[loopPath] !== 'undefined'){ | |
ccbs = cbs[loopPath]; | |
for(var j in ccbs){ | |
ccb = ccbs[j]; | |
if(i === 0){ | |
// where change originated | |
ccb(nv, ov, loopPath); | |
} else if(typeof ccb._watchMeDeepPlease === 'boolean' && ccb._watchMeDeepPlease) { | |
if(i !== total) { | |
// changes in between | |
ccb(_get(nObj, loopPath), _get(oObj, loopPath), loopPath); | |
} else { | |
// the last item in hierarchy (the object itself) | |
ccb(nObj, oObj, loopPath); | |
} | |
} | |
} | |
} | |
parts.pop(); | |
} | |
} | |
const makeHandler = function(basepath){ | |
const getPath = function(property) { | |
return !basepath ? property : [basepath, property].join('.'); | |
} | |
return { | |
get(target, property, receiver) { | |
if(property === 'watch' && typeof target['watch'] === 'undefined'){ | |
return watch; | |
} | |
if(property === 'unwatch' && typeof target['unwatch'] === 'undefined'){ | |
return unwatch; | |
} | |
if(_isArray(target) && property === 'splice'){ | |
return function (...args) { | |
var ov = target; | |
target[property].apply(target, args); | |
var nv = target; | |
onChange(getPath(property), nv, ov); | |
} | |
} | |
if((typeof target[property] === 'object' || typeof target[property] === 'function') && target[property] !== null) { | |
return new Proxy(target[property], makeHandler( getPath(property) )); | |
} | |
return Reflect.get(target, property, receiver); | |
}, | |
defineProperty(target, property, descriptor) { | |
var path = getPath(property); | |
var ov = target[property]; | |
var nv = descriptor.value; | |
Reflect.defineProperty(target, property, descriptor); | |
onChange(path, nv, ov); | |
return target; | |
}, | |
deleteProperty(target, property) { | |
var path = getPath(property); | |
var ov = target[property]; | |
var nv = undefined; | |
Reflect.deleteProperty(target, property); | |
onChange(path, nv, ov); | |
return target; | |
}, | |
} | |
}; | |
return new Proxy(obj, makeHandler('')); | |
}; | |
export default Observable |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// some tests / examples | |
var test = Observable({ | |
a: 1, | |
b: { | |
c: 2, | |
d: { | |
e: 3 | |
} | |
} | |
}); | |
test.watch((nv, ov) => { | |
console.log('all-changed', nv, ov); | |
}); | |
test.watch((nv, ov) => { | |
console.log('all-changed-deep', nv, ov); | |
}, {deep: true}); | |
test.watch((nv, ov) => { | |
console.log('all-changed-deep-immediate', nv, ov); | |
}, {deep: true, immediate: true}); | |
test.watch('a', (nv, ov) => { | |
console.log('a-changed', nv, ov); | |
}); | |
test.watch('b', (nv, ov) => { | |
console.log('b-changed', nv, ov); | |
}); | |
test.watch('b.c', (nv, ov) => { | |
console.log('b.c-changed', nv, ov); | |
}); | |
test.watch('b.d', (nv, ov) => { | |
console.log('b.d-changed', nv, ov); | |
}); | |
test.watch('b.d.e', (nv, ov) => { | |
console.log('b.d.e-changed', nv, ov); | |
}); | |
setTimeout(() => { | |
test.a = 5; | |
test.b.c = 20; | |
test.b.d.e = 50; | |
}, 400); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment