Last active
April 12, 2021 16:54
-
-
Save ViniciusFXavier/bb28d68fd1def5d496457c74373452a5 to your computer and use it in GitHub Desktop.
Detect on change
This file contains hidden or 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
const object = { | |
foo: false, | |
a: { | |
b: [ | |
{ | |
c: false | |
} | |
] | |
} | |
}; | |
let i = 0; | |
const watchedObject = onChange(object, function (path, value, previousValue) { | |
console.log('Object changed:', ++i); | |
console.log('this:', this); | |
console.log('path:', path); | |
console.log('value:', value); | |
console.log('previousValue:', previousValue); | |
}); | |
watchedObject.foo = true; | |
//=> 'Object changed: 1' | |
//=> 'this: { | |
// foo: true, | |
// a: { | |
// b: [ | |
// { | |
// c: false | |
// } | |
// ] | |
// } | |
// }' | |
//=> 'path: "foo"' | |
//=> 'value: true' | |
//=> 'previousValue: false' | |
watchedObject.a.b[0].c = true; | |
//=> 'Object changed: 2' | |
//=> 'this: { | |
// foo: true, | |
// a: { | |
// b: [ | |
// { | |
// c: true | |
// } | |
// ] | |
// } | |
// }' | |
//=> 'path: "a.b.0.c"' | |
//=> 'value: true' | |
//=> 'previousValue: false' | |
// Access the original object | |
onChange.target(watchedObject).foo = false; | |
// Callback isn't called | |
// Unsubscribe | |
onChange.unsubscribe(watchedObject); | |
watchedObject.foo = 'bar'; | |
// Callback isn't called |
This file contains hidden or 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
// Browser version from: https://github.com/sindresorhus/on-change/ | |
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } | |
var PATH_SEPARATOR = '.'; | |
var TARGET = Symbol('target'); | |
var UNSUBSCRIBE = Symbol('unsubscribe'); | |
var isPrimitive = function isPrimitive(value) { | |
return value === null || _typeof(value) !== 'object' && typeof value !== 'function'; | |
}; | |
var isBuiltinWithoutMutableMethods = function isBuiltinWithoutMutableMethods(value) { | |
return value instanceof RegExp || value instanceof Number; | |
}; | |
var isBuiltinWithMutableMethods = function isBuiltinWithMutableMethods(value) { | |
return value instanceof Date; | |
}; | |
var isSameDescriptor = function isSameDescriptor(a, b) { | |
return a !== undefined && b !== undefined && Object.is(a.value, b.value) && (a.writable || false) === (b.writable || false) && (a.enumerable || false) === (b.enumerable || false) && (a.configurable || false) === (b.configurable || false); | |
}; | |
var concatPath = function concatPath(path, property) { | |
if (property && property.toString) { | |
if (path) { | |
path += PATH_SEPARATOR; | |
} | |
path += property.toString(); | |
} | |
return path; | |
}; | |
var walkPath = function walkPath(path, callback) { | |
var index; | |
while (path) { | |
index = path.indexOf(PATH_SEPARATOR); | |
if (index === -1) { | |
index = path.length; | |
} | |
callback(path.slice(0, index)); | |
path = path.slice(index + 1); | |
} | |
}; | |
var shallowClone = function shallowClone(value) { | |
if (Array.isArray(value)) { | |
return value.slice(); | |
} | |
return Object.assign({}, value); | |
}; | |
var onChange = function onChange(object, _onChange) { | |
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | |
var proxyTarget = Symbol('ProxyTarget'); | |
var inApply = false; | |
var changed = false; | |
var applyPath; | |
var applyPrevious; | |
var isUnsubscribed = false; | |
var equals = options.equals || Object.is; | |
var propCache = new WeakMap(); | |
var pathCache = new WeakMap(); | |
var proxyCache = new WeakMap(); | |
var handleChange = function handleChange(path, property, previous, value) { | |
if (isUnsubscribed) { | |
return; | |
} | |
if (!inApply) { | |
_onChange(concatPath(path, property), value, previous); | |
return; | |
} | |
if (inApply && applyPrevious && previous !== undefined && value !== undefined && property !== 'length') { | |
var item = applyPrevious; | |
if (path !== applyPath) { | |
path = path.replace(applyPath, '').slice(1); | |
walkPath(path, function (key) { | |
item[key] = shallowClone(item[key]); | |
item = item[key]; | |
}); | |
} | |
item[property] = previous; | |
} | |
changed = true; | |
}; | |
var getOwnPropertyDescriptor = function getOwnPropertyDescriptor(target, property) { | |
var props = propCache !== null && propCache.get(target); | |
if (props) { | |
props = props.get(property); | |
} | |
if (props) { | |
return props; | |
} | |
props = new Map(); | |
propCache.set(target, props); | |
var prop = props.get(property); | |
if (!prop) { | |
prop = Reflect.getOwnPropertyDescriptor(target, property); | |
props.set(property, prop); | |
} | |
return prop; | |
}; | |
var invalidateCachedDescriptor = function invalidateCachedDescriptor(target, property) { | |
var props = propCache ? propCache.get(target) : undefined; | |
if (props) { | |
props["delete"](property); | |
} | |
}; | |
var buildProxy = function buildProxy(value, path) { | |
if (isUnsubscribed) { | |
return value; | |
} | |
pathCache.set(value, path); | |
var proxy = proxyCache.get(value); | |
if (proxy === undefined) { | |
proxy = new Proxy(value, handler); | |
proxyCache.set(value, proxy); | |
} | |
return proxy; | |
}; | |
var unsubscribe = function unsubscribe(target) { | |
isUnsubscribed = true; | |
propCache = null; | |
pathCache = null; | |
proxyCache = null; | |
return target; | |
}; | |
var ignoreProperty = function ignoreProperty(property) { | |
return isUnsubscribed || options.ignoreSymbols === true && _typeof(property) === 'symbol' || options.ignoreUnderscores === true && property.charAt(0) === '_' || options.ignoreKeys !== undefined && options.ignoreKeys.includes(property); | |
}; | |
var handler = { | |
get: function get(target, property, receiver) { | |
if (property === proxyTarget || property === TARGET) { | |
return target; | |
} | |
if (property === UNSUBSCRIBE && pathCache !== null && pathCache.get(target) === '') { | |
return unsubscribe(target); | |
} | |
var value = Reflect.get(target, property, receiver); | |
if (isPrimitive(value) || isBuiltinWithoutMutableMethods(value) || property === 'constructor' || options.isShallow === true || ignoreProperty(property)) { | |
return value; | |
} // Preserve invariants | |
var descriptor = getOwnPropertyDescriptor(target, property); | |
if (descriptor && !descriptor.configurable) { | |
if (descriptor.set && !descriptor.get) { | |
return undefined; | |
} | |
if (descriptor.writable === false) { | |
return value; | |
} | |
} | |
return buildProxy(value, concatPath(pathCache.get(target), property)); | |
}, | |
set: function set(target, property, value, receiver) { | |
if (value && value[proxyTarget] !== undefined) { | |
value = value[proxyTarget]; | |
} | |
var ignore = ignoreProperty(property); | |
var previous = ignore ? null : Reflect.get(target, property, receiver); | |
var isChanged = !(property in target) || !equals(previous, value); | |
var result = true; | |
if (isChanged) { | |
result = Reflect.set(target[proxyTarget] || target, property, value); | |
if (!ignore && result) { | |
handleChange(pathCache.get(target), property, previous, value); | |
} | |
} | |
return result; | |
}, | |
defineProperty: function defineProperty(target, property, descriptor) { | |
var result = true; | |
if (!isSameDescriptor(descriptor, getOwnPropertyDescriptor(target, property))) { | |
result = Reflect.defineProperty(target, property, descriptor); | |
if (result && !ignoreProperty(property) && !isSameDescriptor()) { | |
invalidateCachedDescriptor(target, property); | |
handleChange(pathCache.get(target), property, undefined, descriptor.value); | |
} | |
} | |
return result; | |
}, | |
deleteProperty: function deleteProperty(target, property) { | |
if (!Reflect.has(target, property)) { | |
return true; | |
} | |
var ignore = ignoreProperty(property); | |
var previous = ignore ? null : Reflect.get(target, property); | |
var result = Reflect.deleteProperty(target, property); | |
if (!ignore && result) { | |
invalidateCachedDescriptor(target, property); | |
handleChange(pathCache.get(target), property, previous); | |
} | |
return result; | |
}, | |
apply: function apply(target, thisArg, argumentsList) { | |
var compare = isBuiltinWithMutableMethods(thisArg); | |
if (compare) { | |
thisArg = thisArg[proxyTarget]; | |
} | |
if (!inApply) { | |
inApply = true; | |
if (compare) { | |
applyPrevious = thisArg.valueOf(); | |
} | |
if (Array.isArray(thisArg) || toString.call(thisArg) === '[object Object]') { | |
applyPrevious = shallowClone(thisArg[proxyTarget]); | |
} | |
applyPath = pathCache.get(target); | |
applyPath = applyPath.slice(0, Math.max(applyPath.lastIndexOf(PATH_SEPARATOR), 0)); | |
var result = Reflect.apply(target, thisArg, argumentsList); | |
inApply = false; | |
if (changed || compare && !equals(applyPrevious, thisArg.valueOf())) { | |
handleChange(applyPath, '', applyPrevious, thisArg[proxyTarget] || thisArg); | |
applyPrevious = null; | |
changed = false; | |
} | |
return result; | |
} | |
return Reflect.apply(target, thisArg, argumentsList); | |
} | |
}; | |
var proxy = buildProxy(object, ''); | |
_onChange = _onChange.bind(proxy); | |
return proxy; | |
}; | |
onChange.target = function (proxy) { | |
return proxy[TARGET] || proxy; | |
}; | |
onChange.unsubscribe = function (proxy) { | |
return proxy[UNSUBSCRIBE] || proxy; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment