Comparing version 2.2.3 to 3.0.0
@@ -10,3 +10,3 @@ declare namespace onChange { | ||
``` | ||
import onChange = require('on-change'); | ||
const onChange = require('on-change'); | ||
@@ -40,3 +40,3 @@ const object = { | ||
``` | ||
import onChange = require('on-change'); | ||
const onChange = require('on-change'); | ||
@@ -97,3 +97,51 @@ const object = { | ||
ignoreDetached?: boolean; | ||
/** | ||
Trigger callbacks for each change within specified method calls or all method calls. | ||
@default false | ||
*/ | ||
details?: boolean | string[]; | ||
/** | ||
The function receives the same arguments and context as the [onChange callback](#onchange). The function is called whenever a change is attempted. Returning true will allow the change to be made and the onChange callback to execute, returning anything else will prevent the change from being made and the onChange callback will not trigger. | ||
@example | ||
``` | ||
const onChange = require('on-change'); | ||
const object = {a: 0}; | ||
let i = 0; | ||
const watchedObject = onChange(object, () => { | ||
console.log('Object changed:', ++i); | ||
}, {onValidate: () => false}); | ||
watchedObject.a = true; | ||
// watchedObject.a still equals 0 | ||
``` | ||
*/ | ||
onValidate?: ( | ||
this: unknown, | ||
path: string, | ||
value: unknown, | ||
previousValue: unknown, | ||
applyData: ApplyData | ||
) => boolean; | ||
} | ||
interface ApplyData { | ||
/** | ||
The name of the method that produced the change. | ||
*/ | ||
name: string; | ||
/** | ||
The arguments provided to the method that produced the change. | ||
*/ | ||
args: any[]; | ||
/** | ||
The result returned from the method that produced the change. | ||
*/ | ||
result: any; | ||
} | ||
} | ||
@@ -107,2 +155,3 @@ | ||
@param onChange - Function that gets called anytime the object changes. | ||
@param [options] - Options for altering the behavior of onChange. | ||
@returns A version of `object` that is watched. It's the exact same object, just with some `Proxy` traps. | ||
@@ -112,3 +161,3 @@ | ||
``` | ||
import onChange = require('on-change'); | ||
const onChange = require('on-change'); | ||
@@ -127,3 +176,3 @@ const object = { | ||
let i = 0; | ||
const watchedObject = onChange(object, function (path, value, previousValue, name) { | ||
const watchedObject = onChange(object, function (path, value, previousValue, applyData) { | ||
console.log('Object changed:', ++i); | ||
@@ -134,3 +183,3 @@ console.log('this:', this); | ||
console.log('previousValue:', previousValue); | ||
console.log('name:', name); | ||
console.log('applyData:', applyData); | ||
}); | ||
@@ -153,3 +202,3 @@ | ||
//=> 'previousValue: false' | ||
//=> 'name: undefined' | ||
//=> 'applyData: undefined' | ||
@@ -171,3 +220,3 @@ watchedObject.a.b[0].c = true; | ||
//=> 'previousValue: false' | ||
//=> 'name: undefined' | ||
//=> 'applyData: undefined' | ||
@@ -190,3 +239,7 @@ watchedObject.a.b.push(3); | ||
//=> 'previousValue: [{c: true}]' | ||
//=> 'name: "push"' | ||
//=> 'applyData: { | ||
// name: "push", | ||
// args: [3], | ||
// result: 2, | ||
// }' | ||
@@ -210,3 +263,3 @@ // Access the original object | ||
previousValue: unknown, | ||
name: string | ||
applyData: onChange.ApplyData | ||
) => void, | ||
@@ -224,3 +277,3 @@ options?: onChange.Options & {pathAsArray?: false} | ||
previousValue: unknown, | ||
name: string | ||
applyData: onChange.ApplyData | ||
) => void, | ||
@@ -227,0 +280,0 @@ options: onChange.Options & {pathAsArray: true} |
119
index.js
@@ -11,3 +11,3 @@ 'use strict'; | ||
const Cache = require('./lib/cache'); | ||
const SmartClone = require('./lib/smart-clone'); | ||
const SmartClone = require('./lib/smart-clone/smart-clone.js'); | ||
@@ -20,3 +20,4 @@ const defaultOptions = { | ||
ignoreUnderscores: false, | ||
ignoreDetached: false | ||
ignoreDetached: false, | ||
details: false | ||
}; | ||
@@ -30,7 +31,15 @@ | ||
const proxyTarget = Symbol('ProxyTarget'); | ||
const {equals, isShallow, ignoreDetached} = options; | ||
const {equals, isShallow, ignoreDetached, details} = options; | ||
const cache = new Cache(equals); | ||
const smartClone = new SmartClone(); | ||
const hasOnValidate = typeof options.onValidate === 'function'; | ||
const smartClone = new SmartClone(hasOnValidate); | ||
const handleChangeOnTarget = (target, property, previous, value) => { | ||
// eslint-disable-next-line max-params | ||
const validate = (target, property, value, previous, applyData) => { | ||
return !hasOnValidate || | ||
smartClone.isCloning || | ||
options.onValidate(path.concat(cache.getPath(target), property), value, previous, applyData) === true; | ||
}; | ||
const handleChangeOnTarget = (target, property, value, previous) => { | ||
if ( | ||
@@ -40,3 +49,3 @@ !ignoreProperty(cache, options, property) && | ||
) { | ||
handleChange(cache.getPath(target), property, previous, value); | ||
handleChange(cache.getPath(target), property, value, previous); | ||
} | ||
@@ -46,7 +55,7 @@ }; | ||
// eslint-disable-next-line max-params | ||
const handleChange = (changePath, property, previous, value, name) => { | ||
const handleChange = (changePath, property, value, previous, applyData) => { | ||
if (smartClone.isCloning) { | ||
smartClone.update(changePath, property, previous); | ||
} else { | ||
onChange(path.concat(changePath, property), value, previous, name); | ||
onChange(path.concat(changePath, property), value, previous, applyData); | ||
} | ||
@@ -56,7 +65,5 @@ }; | ||
const getProxyTarget = value => { | ||
if (value) { | ||
return value[proxyTarget] || value; | ||
} | ||
return value; | ||
return value ? | ||
(value[proxyTarget] || value) : | ||
value; | ||
}; | ||
@@ -112,13 +119,19 @@ | ||
const previous = reflectTarget[property]; | ||
const hasProperty = property in target; | ||
if (cache.setProperty(reflectTarget, property, value, receiver, previous)) { | ||
if (!equals(previous, value) || !hasProperty) { | ||
handleChangeOnTarget(target, property, previous, value); | ||
} | ||
if (equals(previous, value) && property in target) { | ||
return true; | ||
} | ||
const isValid = validate(target, property, value, previous); | ||
if ( | ||
isValid && | ||
cache.setProperty(reflectTarget, property, value, receiver, previous) | ||
) { | ||
handleChangeOnTarget(target, property, target[property], previous); | ||
return true; | ||
} | ||
return false; | ||
return !isValid; | ||
}, | ||
@@ -128,7 +141,10 @@ | ||
if (!cache.isSameDescriptor(descriptor, target, property)) { | ||
if (!cache.defineProperty(target, property, descriptor)) { | ||
return false; | ||
const previous = target[property]; | ||
if ( | ||
validate(target, property, descriptor.value, previous) && | ||
cache.defineProperty(target, property, descriptor, previous) | ||
) { | ||
handleChangeOnTarget(target, property, descriptor.value, previous); | ||
} | ||
handleChangeOnTarget(target, property, undefined, descriptor.value); | ||
} | ||
@@ -146,4 +162,7 @@ | ||
if (cache.deleteProperty(target, property, previous)) { | ||
handleChangeOnTarget(target, property, previous); | ||
if ( | ||
validate(target, property, undefined, previous) && | ||
cache.deleteProperty(target, property, previous) | ||
) { | ||
handleChangeOnTarget(target, property, undefined, previous); | ||
@@ -163,4 +182,8 @@ return true; | ||
if (SmartClone.isHandledType(thisProxyTarget)) { | ||
const applyPath = path.initial(cache.getPath(target)); | ||
if ( | ||
(details === false || | ||
(details !== true && !details.includes(target.name))) && | ||
SmartClone.isHandledType(thisProxyTarget) | ||
) { | ||
let applyPath = path.initial(cache.getPath(target)); | ||
const isHandledMethod = SmartClone.isHandledMethod(thisProxyTarget, target.name); | ||
@@ -170,3 +193,3 @@ | ||
const result = Reflect.apply( | ||
let result = Reflect.apply( | ||
target, | ||
@@ -179,10 +202,30 @@ smartClone.preferredThisArg(target, thisArg, thisProxyTarget), | ||
const isChanged = smartClone.isChanged(thisProxyTarget, equals, argumentsList); | ||
const clone = smartClone.stop(); | ||
const isChanged = smartClone.isChanged(thisProxyTarget, equals); | ||
const previous = smartClone.stop(); | ||
if (SmartClone.isHandledType(result) && isHandledMethod) { | ||
if (thisArg instanceof Map && target.name === 'get') { | ||
applyPath = path.concat(applyPath, argumentsList[0]); | ||
} | ||
result = cache.getProxy(result, applyPath, handler); | ||
} | ||
if (isChanged) { | ||
if (smartClone.isCloning) { | ||
handleChange(path.initial(applyPath), path.last(applyPath), clone, thisProxyTarget, target.name); | ||
const applyData = { | ||
name: target.name, | ||
args: argumentsList, | ||
result | ||
}; | ||
const changePath = smartClone.isCloning ? | ||
path.initial(applyPath) : | ||
applyPath; | ||
const property = smartClone.isCloning ? | ||
path.last(applyPath) : | ||
''; | ||
if (validate(path.get(object, changePath), property, thisProxyTarget, previous, applyData)) { | ||
handleChange(changePath, property, thisProxyTarget, previous, applyData); | ||
} else { | ||
handleChange(applyPath, '', clone, thisProxyTarget, target.name); | ||
smartClone.undo(thisProxyTarget); | ||
} | ||
@@ -198,5 +241,3 @@ } | ||
return (SmartClone.isHandledType(result) && isHandledMethod) ? | ||
cache.getProxy(result, applyPath, handler, proxyTarget) : | ||
result; | ||
return result; | ||
} | ||
@@ -211,8 +252,12 @@ | ||
if (hasOnValidate) { | ||
options.onValidate = options.onValidate.bind(proxy); | ||
} | ||
return proxy; | ||
}; | ||
onChange.target = proxy => proxy[TARGET] || proxy; | ||
onChange.target = proxy => (proxy && proxy[TARGET]) || proxy; | ||
onChange.unsubscribe = proxy => proxy[UNSUBSCRIBE] || proxy; | ||
module.exports = onChange; |
@@ -78,9 +78,3 @@ 'use strict'; | ||
isDetached(target, object) { | ||
path.walk(this.getPath(target), key => { | ||
if (object) { | ||
object = object[key]; | ||
} | ||
}); | ||
return !Object.is(target, object); | ||
return !Object.is(target, path.get(object, this.getPath(target))); | ||
} | ||
@@ -87,0 +81,0 @@ |
@@ -100,3 +100,12 @@ 'use strict'; | ||
} | ||
}, | ||
get(object, path) { | ||
this.walk(path, key => { | ||
if (object) { | ||
object = object[key]; | ||
} | ||
}); | ||
return object; | ||
} | ||
}; |
{ | ||
"name": "on-change", | ||
"version": "2.2.3", | ||
"version": "3.0.0", | ||
"description": "Watch an object or array for changes", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -32,3 +32,3 @@ # on-change | ||
let i = 0; | ||
const watchedObject = onChange(object, function (path, value, previousValue, name) { | ||
const watchedObject = onChange(object, function (path, value, previousValue, applyData) { | ||
console.log('Object changed:', ++i); | ||
@@ -39,3 +39,3 @@ console.log('this:', this); | ||
console.log('previousValue:', previousValue); | ||
console.log('name:', name); | ||
console.log('applyData:', applyData); | ||
}); | ||
@@ -58,3 +58,3 @@ | ||
//=> 'previousValue: false' | ||
//=> 'name: undefined' | ||
//=> 'applyData: undefined' | ||
@@ -76,3 +76,3 @@ watchedObject.a.b[0].c = true; | ||
//=> 'previousValue: false' | ||
//=> 'name: undefined' | ||
//=> 'applyData: undefined' | ||
@@ -95,3 +95,7 @@ watchedObject.a.b.push(3); | ||
//=> 'previousValue: [{c: true}]' | ||
//=> 'name: "push"' | ||
//=> 'applyData: { | ||
// name: "push", | ||
// args: [3], | ||
// result: 2, | ||
// }' | ||
@@ -130,3 +134,3 @@ // Access the original object | ||
3. The previous value at the path. Changes in `WeakSets` and `WeakMaps` will return `undefined`. | ||
4. The name of the method that produced the change. | ||
4. An object with the name of the method that produced the change, the args passed to the method, and the result of the method. | ||
@@ -139,2 +143,4 @@ The context (this) is set to the original object passed to `onChange` (with Proxy). | ||
Options for altering the behavior of onChange. | ||
##### isShallow | ||
@@ -180,3 +186,3 @@ | ||
The path will be provided as an array of keys instead of a delimited string. | ||
The path will be provided as an array of keys instead of a delimited string. Recommended when working with Sets, Maps, or property keys that are Symbols. | ||
@@ -190,2 +196,15 @@ ##### ignoreDetached | ||
##### details | ||
Type: `boolean|string[]`\ | ||
Default: `false` | ||
Trigger callbacks for each change within specified method calls or all method calls. | ||
##### onValidate | ||
Type: `Function` | ||
The function receives the same arguments and context as the [onChange callback](#onchange). The function is called whenever a change is attempted. Returning true will allow the change to be made and the onChange callback to execute, returning anything else will prevent the change from being made and the onChange callback will not trigger. | ||
<br/> | ||
@@ -192,0 +211,0 @@ |
39368
31
1110
282