Giter Site home page Giter Site logo

on-change's Introduction

on-change

Watch an object or array for changes

It works recursively, so it will even detect if you modify a deep property like obj.a.b[0].c = true.

Uses the Proxy API.

Install

npm install on-change

Usage

import onChange from 'on-change';

const object = {
	foo: false,
	a: {
		b: [
			{
				c: false
			}
		]
	}
};

let index = 0;
const watchedObject = onChange(object, function (path, value, previousValue, applyData) {
	console.log('Object changed:', ++index);
	console.log('this:', this);
	console.log('path:', path);
	console.log('value:', value);
	console.log('previousValue:', previousValue);
	console.log('applyData:', applyData);
});

watchedObject.foo = true;
//=> 'Object changed: 1'
//=> 'this: {
//   	foo: true,
//   	a: {
//   		b: [
//   			{
//   				c: false
//   			}
//   		]
//   	}
//   }'
//=> 'path: "foo"'
//=> 'value: true'
//=> 'previousValue: false'
//=> 'applyData: undefined'

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'
//=> 'applyData: undefined'

watchedObject.a.b.push(3);
//=> 'Object changed: 3'
//=> 'this: {
//   	foo: true,
//   	a: {
//   		b: [
//   			{
//   				c: true
//   			},
//   			3
//   		]
//   	}
//   }'
//=> 'path: "a.b"'
//=> 'value: [{c: true}, 3]'
//=> 'previousValue: [{c: true}]'
//=> 'applyData: {
//       name: "push",
//       args: [3],
//       result: 2,
//   }'

// Access the original object
onChange.target(watchedObject).foo = false;
// Callback isn't called

// Unsubscribe
onChange.unsubscribe(watchedObject);
watchedObject.foo = 'bar';
// Callback isn't called

API

onChange(object, onChange, options?)

Returns a version of object that is watched. It's the exact same object, just with some Proxy traps.

object

Type: object

Object to watch for changes.

onChange

Type: Function

Function that gets called anytime the object changes.

The function receives four arguments:

  1. A path to the value that was changed. A change to c in the above example would return a.b.0.c.
  2. The new value at the path.
  3. The previous value at the path. Changes in WeakSets and WeakMaps will return undefined.
  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.

The context (this) is set to the original object passed to onChange (with Proxy).

options

Type: object

Options for altering the behavior of onChange.

isShallow

Type: boolean
Default: false

Deep changes will not trigger the callback. Only changes to the immediate properties of the original object.

equals

Type: Function
Default: Object.is

The function receives two arguments to be compared for equality. Should return true if the two values are determined to be equal. Useful if you only need a more loose form of equality.

ignoreSymbols

Type: boolean
Default: false

Setting properties as Symbol won't trigger the callback.

ignoreKeys

Type: Array<string | symbol>
Default: undefined

Setting properties in this array won't trigger the callback.

ignoreUnderscores

Type: boolean
Default: false

Setting properties with an underscore as the first character won't trigger the callback.

pathAsArray

Type: boolean
Default: false

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.

ignoreDetached

Type: boolean
Default: false

Ignore changes to objects that become detached from the watched object.

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. 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.

onChange.target(object)

Returns the original unwatched object.

object

Type: object

Object that is already being watched for changes.

onChange.unsubscribe(object)

Cancels all future callbacks on a watched object and returns the original unwatched object.

object

Type: object

Object that is already being watched for changes.

Use-case

I had some code that was like:

const foo = {
	a: 0,
	b: 0
};

// …

foo.a = 3;
save(foo);

// …

foo.b = 7;
save(foo);


// …

foo.a = 10;
save(foo);

Now it can be simplified to:

const foo = onChange({
	a: 0,
	b: 0
}, () => save(foo));

// …

foo.a = 3;

// …

foo.b = 7;

// …

foo.a = 10;

Related

  • known - Allow only access to known object properties (Uses Proxy too)
  • negative-array - Negative array index support array[-1] (Uses Proxy too)
  • atama - State manager (Uses Proxy too)
  • introspected - Never-ending Proxy with multiple observers (Uses Proxy too)

Maintainers

on-change's People

Contributors

albertosantini avatar batram avatar bendingbender avatar brendon1982 avatar cbbfcd avatar codenamezjames avatar darrenpaulwright avatar franciscop avatar glundgren93 avatar hpx7 avatar kpruden avatar mikaello avatar mjstahl avatar mooyoul avatar remyguillaume avatar richienb avatar seldszar avatar sindresorhus avatar sorodrigo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

on-change's Issues

Don't trigger the callback if the operation fails

Here:

if (!ignore && !equals(previous, value)) {

We are not accounting for the return value of Reflect.set when deciding whether to trigger the callback or not, but if Reflect.set returns false then the set operation failed (maybe the object wasn't writable, maybe the setter threw an error etc.) so nothing actually changed and the callback shouldn't be called.

I think the defineProperty and deleteProperty traps are also affected by this issue.

Spread Operator not working for Sets ([...someSet])

const onChange = require('on-change');

const unwatchedObject = {
    someObject: {foo: 'bar'},
    someArray: [1, 2, 3],
    someSet: new Set([1, 2, 3])
};

const watchedObject = onChange(unwatchedObject, () => {
    console.log('watchedObject changed');
});

console.log(watchedObject.someArray);
console.log([...watchedObject.someArray]);

console.log(unwatchedObject.someSet);
console.log([...unwatchedObject.someSet]);

console.log(watchedObject.someSet);
console.log([...watchedObject.someSet]);

Output:

[ 1, 2, 3 ]
[ 1, 2, 3 ]
Set(3) { 1, 2, 3 }
[ 1, 2, 3 ]
Set(3) { 1, 2, 3 }
/Users/simon/Dropbox/Coding/node/sandbox/on-change-sandbox/node_modules/on-change/index.js:135
			const result = Reflect.apply(
			                       ^

TypeError: Method Set.prototype.values called on incompatible receiver [object Object]
    at Proxy.values (<anonymous>)
    at Object.apply (/Users/simon/Dropbox/Coding/node/sandbox/on-change-sandbox/node_modules/on-change/index.js:135:27)
    at Object.<anonymous> (/Users/simon/Dropbox/Coding/node/sandbox/on-change-sandbox/spreadOperatorError.js:20:31)
    at Module._compile (node:internal/modules/cjs/loader:1091:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1120:10)
    at Module.load (node:internal/modules/cjs/loader:971:32)
    at Function.Module._load (node:internal/modules/cjs/loader:812:14)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
    at node:internal/main/run_main_module:17:47

Performance

I am curious about the performance of this vs. calling save explicitly. Is there a benchmark?

Safari + Arrays


window.blarg = function() {
  
  let a = on_change({}, (o)=> {
    console.log("Changed!");
  });
  
  a.stuff = [];
  a.stuff.push(1);
  a.stuff.push(2);
  a.stuff.push(3);
  
  console.log(a.stuff);   // Proxy {0: 1}   
};

So essentially, we seem to end up with an object with one element in it instead of an array with three elements.

Callback triggered for changes on inheriting objects

I see some unexpected behavior and was wondering if may be I'm streching beyond on-change intended use case.

Lets I have an objects (lets call it obj) for which I created a proxy using on-change.

Now I have a flow that creates new object (lets call it derived) using the proxy as prototype.

When I set some property on obj I expect callback to be called and indeed it is.

When I set some new property on derived that does not exist on obj, here again callback is called.
Since I don't modify watched object, but rather object that has watched object as prototype, I expect that callback won't be called, but it is.

From what I can see, on-change does not take this scenario into account or may be that is by design ?

Would love to hear your opinion on this.

P.S. What I'm trying to do is to protect objects from been modified, but still allow objects that have them as prototypes be modified to mask values on prototype.

Thanks.

Not just pass the name to the listener but also the arguments

Hi there,

I am very very hapy about the last update. With the additional information, which method was used on the array, you can do much more things with this lib. But it turned out that it can be very hard to determine what happened exactly to an array. For example splice. Currently I am doing much things to determine which elements are added and which are removed. Example:

if (name === "splice") {
    const calcPrevVal = [];
    const calcChangedVal = []
    let startIndex = 0;
    let endIndex = 0
    let startFound = false;
    let endFound = false
    for (let index = 0; index < Math.max(changedVal.length, prevVal.length); index++) {
        const indexToUseFromBehind = prevVal.length >= changedVal.length ? prevVal.length - (1 + index) : changedVal.length - (1 + index);
        const lastPrevVal = prevVal.length - (1 + index) >= 0 ? prevVal[prevVal.length - (1 + index)] : undefined;
        const lastChangedVal = changedVal.length - (1 + index) >= 0 ? changedVal[changedVal.length - (1 + index)] : undefined
        if (!endFound) {
            if (lastChangedVal !== lastPrevVal) {
                endIndex = indexToUseFromBehind;
                endFound = true;
            } else {
                calcChangedVal[indexToUseFromBehind] = lastChangedVal;
                calcPrevVal[indexToUseFromBehind] = lastPrevVal;
            }
        
        if (!startFound) {
            if (prevVal[index] !== changedVal[index]) {
                startIndex = index;
                startFound = true;
            } else {
                calcChangedVal[index] = changedVal[index];
                calcPrevVal[index] = prevVal[index];
            }
        
        if (startFound && endFound) {
            for (let index = startIndex; index <= endIndex + (changedVal.length - prevVal.length); index++) {
                calcChangedVal[index] = changedVal[index];
            }
            for (let index = startIndex; index <= endIndex + (prevVal.length - changedVal.length) + (prevVal.length >= changedVal.length ? 1 : 0); index++) {
                calcPrevVal[index] = prevVal[index];
            }
            for (let index = startIndex; index < endIndex + 1; index++) {
                const prevElement = calcPrevVal[index];
                const changedElement = calcChangedVal[index];
                if (prevElement) {
                    Object.assign(removedElements, { [index]: prevElement });
                    if (changedElement) Object.assign(addedElements, { [index]: changedElement });
                } else Object.assign(addedElements, { [index]: changedElement });
            }
            break;
        }
    }
}

You see? Very ugly...
If there would be an argumentslist passed to the listener, this would be much easyer =)
I am using this lib together with two-way-data-binding and I want to notify the components what happened to the data. So I have to determine which element was added to the array and which was removed from. I Could use something like diff-object-deep, but this is not exactly what I am searching for because it would say that an element was updated when the first element was deleted and another element takes its place

Able to use in a Vanilla JS environment

I Would like to upload this package to a cdn of a sort, but for that to be possible it is needed to purely js (without require and such. Is there a way to sort of "bundle it" so it would be accessible through Vanilla js and therefore upload-able to a cdn?

Not detecting underlying field changes in javascript setter

If I have a setter function on an object that modifies a 'private' field, on-change will only pick up a change on the setter field, not the underlying 'private' field. Take a look at this example.

I have a setter called 'something' that modifies a field called '_something'. propChanged gets called for 'something' but not for '_something'. I would think it should get called for both.

someArray.map triggers change

Just calling map on an array triggers apply and onChange with value and previousValue being equal even when inner items does not change at all.

TypeScript definition invalid for pathAsArray: true

The TypeScript definition in index.d.ts does not include an correct overload for the onChange callback when the options parameter includes pathAsArray: true.

The signature in that case should be:

	onChange: (
		this: ObjectType,
		path: string[],

This leads to TypeScript complaining.

TypeScript example:

const object = {}
const watchedObject = onChange(object, function (path: string[]) {
  console.log('change:', path);
}, {
  pathAsArray: true
})

Leads to the following message:

Argument of type '(this: {}, path: string[]) => void' is not assignable to parameter of type '(this: {}, path: string, value: unknown, previousValue: unknown, name: string) => void'.
  Types of parameters 'path' and 'path' are incompatible.
    Type 'string' is not assignable to type 'string[]'.ts(2345)

inconsistent arguments are passed to the listener

Hi again... 🙈

I have recognized that different arguments are passed to the listener when I modify an array with a method like splice or assign a value with a key array[key] = value.

in case of the method, the whole object is passed to the listener and in case of the assignment only the assigned value is passed to the listener.

The behavior should always be the same. In my opinion it is perfect to always get the whole object before and after. In this case it is always possible to compare those objects.

Maybe this could be optional? Benefit: downwards compatibility.

Improve "apply" trap performance

I'm kind of rewriting this library to better suit my needs and I think I've found an interesting performance optimization.

Some methods of builtin objects can't mutate the object in question, like for instance calling forEach on an Array will never mutate the array, so we can avoid cloning, performing potentially expensive equality checks comparisons etc.

This is assuming the prototype of Array doesn't get messed up with, but IMHO if somebody changes it in a way that non-mutating methods become mutating it's on them.

Listen on original object change instead of return a new one

I got an object that its internal value is changed by another library (I'm writing a binding from it to React), and internal value change won't trigger React rerender. So I need to monitor the original object's internal change and trigger rerender by hand.

isShallow: false can result in proxies getting wrapped with proxies

I've been experiencing an issue when using onChange with isShallow: false. In my code I end up doing something like this obj1 === obj2 which will return false, however when I look closely they are the same object under the hood, it's just that one of them is wrapped in an additional proxy. I was able to reproduce this issue in a codesandbox here. You'll need to open your browser console, as the built in console does not show the proxies.

From what I've gathered, when getProxy() in cache.js gets called, the target is already a proxy and will end up getting wrapped in another proxy. There may be other ways to end up in this scenario other than what I've provided, but, I think a solid solution would prevent this from ever happening again.

Edit: here is more simple example of this problem happening

Array change within object loses callback details

test.ts

const onChange = require("on-change");

let i: number = 0;
const obj = {
    a: "",
    b: ["a", "b", "c"]
};

const watchedSettings = onChange(obj, function(
    path: any,
    value: any,
    previousValue: any
) {
    console.log("Object changed:", ++i);
    console.log("this:", this);
    console.log("path:", path);
    console.log("value:", value);
    console.log("previousValue:", previousValue);
});

watchedSettings.a = "test";
watchedSettings.b.push("d");

The output from this is:

~>tsc -p . && node test.js
Object changed: 1
this: { a: 'test', b: [ 'a', 'b', 'c' ] }
path: a
value: test
previousValue:
Object changed: 2
this: undefined
path: undefined
value: undefined
previousValue: undefined

The change to "a" works, but the update to the array "b" in the second returns "undefined" for all fields in the callback. It detected the change, but lost all of the details.

node: 11.12.0
on-change: 1.2.0
typescript: 3.4.1

Unsubscribe

Hope I can unsubscribe to change, so this can be used in React hook, for example, useEffct, which ask you to return an unsubscription function.

Protection against non-writable/non-configurable properties

For more context, see my question on SO.

Problem:

Some methods catch the get trap when trying to look up non-configurable and/or non-writable properties (for instance, Array#concat). This breaks because the returned value from a property lookup violates the first proxy object invariant:

The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable own data property.

My cursory understanding of this means that the prototype returned by the proxied object does not match that of the original object, so it breaks this rule.

Steps to reproduce:

I don't know every method this fails for, but if you use any of the "functional" array methods on a property of something wrapped in on-change, you'll see the error. These methods fail when looking up the target's prototype.

const onChange = require('on-change')

const proxy = onChange({ items: ['foo', 'bar'] }, () => {
  console.log('changed')
})

proxy.items = proxy.items.concat('baz')

/*
  TypeError: 'get' on proxy: property 'prototype' is a read-only and non-configurable data property
  on the proxy target but the proxy did not return its actual value (expected '[object Array]' but got
  '[object Object]')
*/

(Potential) solution:

Add a check to see if the property descriptor is configurable and writable, and if not, return an unwrapped version of the property:

get (target, property, receiver) {
  const desc = Object.getOwnPropertyDescriptor(target, property)
  const value = Reflect.get(target, property, receiver)

  if (desc && !desc.writable && !desc.configurable) return value

  try {
    return new Proxy(target[property], handler)
  } catch (error) {
    return value
  }
}

TypeError: Cannot set property 'example' of undefined

Hi. OS: Windows 10, Node: v10.15.3

Execute this.

const onChange = require('on-change');

let object = {
    example: 1,
};

const proxy = onChange(object, (path, value, previousValue) => {});

proxy.exec = proxyInFunc => proxyInFunc.example = 2;
proxy.exec(proxy);

And got this.

C:\Users\Storm\Dropbox\minipark\node_modules\on-change\index.js:76
                        item[property] = previous;
                                       ^

TypeError: Cannot set property 'example' of undefined
    at handleChange (C:\Users\Storm\Dropbox\minipark\node_modules\on-change\index.js:76:19)
    at Object.set (C:\Users\Storm\Dropbox\minipark\node_modules\on-change\index.js:156:5)
    at Proxy.proxyInEval (eval at <anonymous> (C:\Users\Storm\Dropbox\minipark\test.js:9:14), <anonymous>:1:36)
    at Object.apply (C:\Users\Storm\Dropbox\minipark\node_modules\on-change\index.js:201:28)
    at Object.<anonymous> (C:\Users\Storm\Dropbox\minipark\test.js:10:7)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)

Detection of arrays modified in size

Hi,

I just wondered if it's intended behavior that arrays getting added a value isn't detected as change.
Or am I doing something wrong?

Also the first of the two values in my equals callback is always undefined.

Thanks :D

Splice not working

watchedObject.splice(watchedObject.findIndex(x => x== jid), 1);

 watchedObject.splice(watchedObject.findIndex(x => x== jid), 1);
                  ^

TypeError: 'get' on proxy: property 'prototype' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '[object Array]' but got '[object Object]')

Looping a handler function in onChange when using map function.

Hello!

Please tell me why there is endless execution if I use the immutable map function. She does not change the object of state. Is this a bug or is there an explanation for this? If I remove the map function, then the handler is called once, as it should be. Here is the code that demonstrates this behavior:

const onChange = require('on-change');

const object = {
  arr: [],
  foo: true,
};

const watchedObject = onChange(object, function (path) {
  console.log(path);
  // the call of the immutable map function
  // leads to an endless execution of the handler function
  this.arr.map((el) => {
  });
});

watchedObject.arr.unshift('value');

// => 'arr'
// => 'arr'
// => 'arr'
// => 'arr'
...
it happens endlessly

Improve array handling

It works fine, but could be optimized. There are certain actions that cause many onChange() calls even though only one is desirable.

  • array.pop() causes two calls. One to delete the element and one to defineProperty the new .length. This could be made into one onChange() call. Same with .unshift(), .shift(), .push() too.
  • array.sort() causes a call for each time it moves an element. Here too we're only interested in one call. Same applies to .reverse(), and probably .splice() too.

defineProperty doesn't check for equality

I think there's a problem here:

const result = Reflect.defineProperty(target, property, descriptor);

Each defineProperty call is basically counted as a change, even if the property descriptor is exactly the same, I think the current and the next descriptor need to be compared before triggering a change.

Class Property

How do I watch for a class property that is changed by that class' method?

Changes not detected on internal variables

I'm using the popular https://github.com/jhlywa/chess.js but can't seem detect changes on mutations of the Chess object

Repro:

import onChange from "on-change";
import { Chess } from "chess.js";

const chess = new Chess();
const watchedChess = onChange(chess, () => {
  console.log("change detected");
});

watchedChess.foo = "bar"; // change detected
watchedChess.move({ from: "e2", to: "e4" }); // no change detected, even though this function mutates internal state

I suspect the issue is related to the internal chess state representation, which is not exposed to library consumers. Do you see any way/workaround to make this work?

Stop watching object once it is removed

I've been trying to build a tiny library for app-server configs that can dynamically update sub-components when a part of the config changes. While testing this, I ran into an issue where the object being monitored by onChange would react to changes even though that part of the object was already replaced.

Here's a pen that reproduces this issue. Here's a small snippet to reproduce it that you can just paste into a .js file and run

const onChange = require('on-change')

const config = {
  server: {
    dev: {
      host: 'dev-host',
      port: 30
    }
  }
}

const proxy = onChange(config, (path, v, o) => {
  console.log(`path=${path} from=${JSON.stringify(o)} to=${JSON.stringify(v)}`)
})

const old = proxy.server.dev
proxy.server.dev = { host: 'new-dev-host', port: 60 }
// Now, change the old object
old.host = 'bad'

Omit reIndexing of arrays when methods like unshift or splice are used

When on-change observes an array which is modified with unshift (or other methods which are able to insert or delete items somewhare alse than the end of the array) the handler will be fired for each re-index. similar to #56

Example:

const myObservedArray = onChange([1, 2, 3, 4, 5], (path, newVal, oldVal) => {
    console.log(path, newVal, oldVal);
});
myObservedArray.unshift(6);

The output will be:

path newVal oldVal comment
5 5 undefined because unshift starts first to move every item one index further and overwrites existing values with the beginning at the end
4 4 5
3 3 4
2 2 3
1 1 2
0 6 1 the origin unshift

There should be an option to avoid this behavior and only yield the origin change to the listener function. Maybe you could add an aditional parameter to the function, named "action" or something like that, to be able to determine what happened really.

esmodule version

hi, it would be convenient for me if there were a version that used esmodules, instead of commonjs.

onChange returns object with value before the change

const onChange = require('on-change');

const foo = onChange({
	a: 0,
	b: 0
}, () => save(foo));


foo.a = 3;
//foo = { a: 0, b: 0 }

foo.b = 7;
//foo = { a: 3, b: 0 }

foo.a = 10;
//foo = { a: 3, b: 7 }

function save(foo) {
	console.log("foo =", foo);
};

Will this work with a polyfill?

I'm curious if it will work with some polyfill, polyfilling proxy is almost impossible only this polyfill is somehow doing the minimal job of a proxy

Is it possible to rely on methods like get and set to make this module cover more browsers or it will kill the purpose on it?

proxy is revoked when proxy is assigned to itself even when the property is ignored

Hi again,

when I have a model, let's call ist ActiveUser, which is wrapped into a proxy with on-change and with a property "editingModel" which is an ignored key, the Proxy of ActiveUser on the property "editingModel" will be revoked when ActiveUser is assigned to editingModel.

Example:

// Expected
Proxy(ActiveUser {
    editingModel: Proxy(ActiveUser{}), // This is an ignored key which should be able to hold the same proxy
    anotherProperty: ...
})

// Currently
Proxy(ActiveUser{
    editingModel: ActiveUser{}, // Same Object as the Proxy target but not a proxy anymore
    anotherProperty: ...
})

I understand why this behavior of revoking the proxy when it is assigned to itself exists, but is it necessarry to have this behavior on ignored keys or keys which are not wraped into a proxy?

Sure you want to avoid loops but I think on-change schould be able to deal with circular structures without removing proxies.
Currently this makes my bachelor thesis a little bit mor complicated. ;)

Enhancement: The callback is not called when class methods set properties

class Counter {
  constructor () {
    setInterval(this.onTick.bind(this), 1000)
  }
  onTick () {
    // the setting of hours, minutes, seconds does not call the callback
    const now = new Date()
    this.hours = now.getHours()
    this.minutes = now.getMinutes()
    this.seconds = now.getSeconds()
  }
}
var counter = onchange(new Counter(), (property, value) => {
  console.log(property, value)
})

The callback will be called if I do:

counter.title = 'CLOCK'

This makes sense, because this referenced in onTick is not the Proxy but the instance of Counter. Not completely sure what the correct solution is here. Use of the constructor trap?

Error / invalid behavior when using dates

Failing test (in main):

// Support dates
object.bar.a.c[2] = new Date(1546300800000);
t.deepEqual(object.bar.a.c[2], new Date(1546300800000));
t.is(object.bar.a.c[2].valueOf(), 1546300800000);
t.is(callCount, 9);
 1 test failed

  main

  /code/github/on-change/index.js:125

   124:
   125:         const result = Reflect.apply(target, thisArg, argumentsList);
   126:

  Error thrown in test:

  TypeError {
    message: 'this is not a Date object.',
  }

  Object.apply (index.js:125:28)
  unboxComplex (node_modules/concordance/lib/describe.js:78:27)
  describeComplex (node_modules/concordance/lib/describe.js:112:17)
  curriedComplex (node_modules/concordance/lib/describe.js:143:12)
  describe (node_modules/concordance/lib/describe.js:170:10)
  Object.compare (node_modules/concordance/lib/compare.js:98:12)
  deepEqual (test.js:65:4)

Incompatible receiver error with Set/Map

Repro:

const foo = { x: new Set() };
const bar = onChange(foo, () => {});
console.log(bar.x.has("baz")); // => TypeError: Method Set.prototype.has called on incompatible receiver [object Object]

Same things happens with Map as well

Giving information about the method name for arrays

This is not a real issue but it's an enhancement will add more to this amazing library.
The case is when I do any operation on an array using this library like push, pop, splice, unshift, it gives me the new information and old information normally but what I need actually is the operation name that it's done over the data to detect the behaviour should I take

Tracking classes

Howdy! I just wanted to start by saying thank you for this awesome plugin. It works like a dream for most everything I need. However I stumbled across an edge-case and I was wondering how possible it would be to implement something like this.

Right now, I have a class that has a class instance as a variable within it, for example:

class Image {
  constructor(attributes = {}) {
    this.id = attributes.id || null;
    this.path = attributes.path || null;
  }
}

class User {
  constructor(attributes = {}) {
    this.id = attributes.id || null;
    this.name = attributes.name || null;
    this.email = attributes.email || null;
    this.image = new Image();
  }

  setImage(image) {
    this.image = image;
  }
}

And then I have code written so that I can track changes around it. If I set the image directly, the changes are picked up just fine:

import onChange from "on-change";

const user = new User();

const watchedUser = onChange(object, function (path, value, previousValue) {
	console.log('this:', this); // populated correctly
	console.log('path:', path); // "image"
});

watchedUser.image = new Image({ id: 2, path: "some/path/1.jpg" });

However if I set the image using the setter method, things blow up:

import onChange from "on-change";

const user = new User();

const watchedUser = onChange(object, function (path, value, previousValue) {
	console.log('this:', this); // undefined
	console.log('path:', path); // ""
});

watchedUser.setImage(new Image({ id: 2, path: "some/path/1.jpg" }));

Is there any plan to support something like this? Once again, thanks for the awesome tool. It has been super helpful.

Strange behavior when using onChange in a constructor

I've made a small example to show this:
https://codesandbox.io/s/vigorous-volhard-f1ox5

I can declare a new Address object and it ends up being a proxy because it extends AbstractProxiedObject which uses onChange, however, if I assign it to the property 'address' on a Person in the Person constructor, it's no longer a proxy.

I was able to trace this back to line 112 of index.js
If I return 'receiver' instead of 'target' in the 'get' function, my example works just fine.
So the question becomes, should we be returning 'receiver' in 'get'? Or is there something i'm doing wrong in my code?

It's also worth mentioning that if I remove 'extends AbstractProxiedObject' from the Person object, my example works just fine as well - which is a bit of a head scratcher... are the person and address objects interfering with each other somehow?

Any incite into this problem would be much appreciated.

Second property change on context errors because applyPrevious is `null`

So this simple example of an object mutating itself, fails with the Error:
TypeError: Cannot set property 'count' of null

let count = 0;

const counterObj = {
  onTick() {
    this.count = count++;
  },
};

var counter = onChange(counterObj, (property, value) => {
  console.log(property, value);
});

counter.onTick();
counter.onTick();

I think the problem is in here https://github.com/sindresorhus/on-change/blob/master/index.js#L188-L212

...but I am having trouble following what is going on there.

It seems like applyPrevious is not handled correctly, and is resulting in null being set it item on https://github.com/sindresorhus/on-change/blob/master/index.js#L65

Which results in the error.

Just saw this #32
Probably related, but just in case I will open this issue

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.