Skip to content

Instantly share code, notes, and snippets.

@ViniciusFXavier
Last active April 12, 2021 16:54
Show Gist options
  • Save ViniciusFXavier/bb28d68fd1def5d496457c74373452a5 to your computer and use it in GitHub Desktop.
Save ViniciusFXavier/bb28d68fd1def5d496457c74373452a5 to your computer and use it in GitHub Desktop.
Detect on change
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
// 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