Using this example: How to watch for array changes? i tried to adapt to watch for object chabges:
(function() {
if (!("Proxy" in window)) {
console.warn("Your browser doesn't support Proxies.");
return;
}
var ob = {"a":["a", "b", "c", "d"]};
// a proxy for our ob
var proxy = new Proxy(ob, {
apply: function(target, thisArg, argumentsList) {
return thisArg[target].apply(this, argumentList);
},
deleteProperty: function(target, property) {
console.log("Deleted %s", property);
return true;
},
set: function(target, property, value, receiver) {
target[property] = value;
console.log("Set %s to %o", property, value);
return true;
}
});
proxy.a.push("z");
console.log("Current state of array: %o", ob);
})();
But the push does not trigger the console.log in set. Only if i change:
var proxy = new Proxy(ob.a, {...
proxy.push("z");
Can anyone help me? Thank you
Related
I have a simple proxy object:
const myProxy = new Proxy(
/* myTarget */
{
test: 2,
},
/* myHandler */
{
get(target, prop, proxy)
{
if (prop in this && this[prop] instanceof Function)
return this[prop](target); //getter
return target[prop]; //value
},
myGetter(target)
{
return ++target.test
}
}
);
console.log("test before", myProxy.test);
console.log("myGetter", myProxy.myGetter);
console.log("test after", myProxy.test);
Is there a way make myGetter property as a true getter?
My attempt failed, because I don't know how to access myTarget from within the getter:
const myProxy = new Proxy(
/* myTarget */
{
test: 2,
},
/* myHandler */
{
get(target, prop, proxy)
{
if (prop in this)
return this[prop]; //getter
return target[prop]; //value
},
get myGetter()
{
return ++target.test; //target undefined
}
}
);
console.log("test before", myProxy.test);
console.log("blah", myProxy.myGetter);
console.log("test after", myProxy.test);
The reason why I need this is because I have multiple functions inside the handler and I need to determine which one needs to be executed within handler itself (myGetter), or should be returned as function reference (myFunc), where it can also accept additional data as arguments, so I'm using prop instanceof Function to check if it's a function or not:
const myProxy = new Proxy(
/* myTarget */
{
test: 2,
},
/* myHandler */
{
get(target, prop, proxy)
{
if (prop in this)
{
if (this[prop] instanceof Function)
return (args) => this[prop](target, args); //function
return this[prop]; //getter
}
return target[prop]; //value
},
get myGetter()
{
return ++target.test; //target undefined
},
myFunc(target, value)
{
return target.test * value;
}
}
);
console.log("myFunc", myProxy.myFunc(3));
console.log("test before", myProxy.test);
console.log("myGetter", myProxy.myGetter);
console.log("test after", myProxy.test);
I could think of several work arounds, for example hard code names of functions:
const myProxy = new Proxy(
/* myTarget */
{
test: 2,
},
/* myHandler */
{
get(target, prop, proxy)
{
if (prop in this)
{
if (["myFunc"].includes(prop)) // function
return (args) => this[prop](target, args);
return this[prop](target); // getter
}
return target[prop];
},
myGetter(target)
{
return ++target.test
},
myFunc(target, value)
{
return target.test * value;
}
}
);
console.log("myFunc", myProxy.myFunc(3));
console.log("test before", myProxy.test);
console.log("myGetter", myProxy.myGetter);
console.log("test after", myProxy.test);
Or separate functions into groups:
const myProxy = new Proxy(
/* myTarget */
{
test: 2,
},
/* myHandler */
{
get(target, prop, proxy)
{
if (prop in this._myFuncs)
return (args) => this._myFuncs[prop](target, args);
if (prop in this._myGetters)
return this._myGetters[prop](target);
return target[prop];
},
_myGetters:
{
myGetter(target)
{
return ++target.test
},
},
_myFuncs:
{
myFunc(target, value)
{
return target.test * value;
},
}
}
);
console.log("myFunc", myProxy.myFunc(3));
console.log("test before", myProxy.test);
console.log("myGetter", myProxy.myGetter);
console.log("test after", myProxy.test);
For education purpose any suggestions how to make true getter to work in Proxy?
I don't think the handler object should have nonstandard properties on it - it'd make more sense for it to only have the traps that it's expected to have. To have collection of properties for traps, either hard-code those properties (which is inflexible and not so nice) or use a separate object structure for those properties. With a separate object in the proxy closure, checking whether the property exists on the object and then .calling it with target if it's a getter is easy.
const makeProxy = (target, customProperties) => {
return new Proxy(
target, {
get(target, prop, proxy) {
if (prop in customProperties) {
// Invoke getter if it exists on customProperties
const { get } = Object.getOwnPropertyDescriptor(customProperties, prop);
if (get) {
return get.call(target);
}
}
// Otherwise return original object value or getter
return target[prop];
},
}
);
};
const proxy = makeProxy({
test: 2,
}, {
get myGetter() {
return ++this.test
}
});
console.log("test before", proxy.test);
console.log("blah", proxy.myGetter);
console.log("test after", proxy.test);
I would like to observe whenever a property of a third party object is changed. I'm taking the approach of assigning a custom setter but my console.log below is never invoked. Why is that? Is there a better approach?
const foo = { a: 1, b: 2 };
Object.assign(foo, {
set user(user) {
foo.user = user;
console.log(">>>>>> user was changed", user);
},
});
// Desired behaviour
foo.user = "asdf"; // >>>>>> user was changed asdf
delete foo.user; // >>>>>> user was changed undefined
foo.user = "asdf1" // >>>>>> user was changed asdf1
Please note, I need to mutate foo I cannot wrap a proxy around foo and return that because it is a third party library which mutates .user internally
I've found a way, pretty hacky as it is
const foo = { a: 1, b: 2 };
let underlyingValue = foo.user
Object.defineProperty(foo, "user", {
get() {
return underlyingValue
},
set(user) {
underlyingValue = user;
console.log(">>>>>> user was changed", user);
},
enumerable: true
});
foo.user = "asdf";
console.log(foo)
I've made this into a generic function below 👇
/** Intercepts writes to any property of an object */
function observeProperty(obj, property, onChanged) {
const oldDescriptor = Object.getOwnPropertyDescriptor(obj, property);
let val = obj[property];
Object.defineProperty(obj, property, {
get() {
return val;
},
set(newVal) {
val = newVal;
onChanged(newVal);
},
enumerable: oldDescriptor?.enumerable,
configurable: oldDescriptor?.configurable,
});
}
// example usage 👇
const foo = { a: 1 };
observeProperty(foo, "a", (a) => {
console.log("a was changed to", a);
});
foo.a = 2; // a was changed to 2
Also available in typescript
🚨 Edit: This will break if the property is deleted eg delete foo.user. The observer will be removed and the callback will stop firing. You will need to re-attach it.
#david_adler ... when I commented ...
"Is the latter a special case or does the OP need a somehow more generic observation approach?"
... I thought of the most generic solution one could come up with in terms of changing/mutating an existing object entirely into an observable variant of itself.
Such a solution also would be more close to what the OP did ask for ...
"I would like to observe whenever a property of a third party object is changed"
Thus the next provided approach keeps the objects appearance and behavior and also does not introduce additional (e.g. Symbol based) keys.
function mutateIntoObservableZombie(obj, handlePropertyChange) {
const propertyMap = new Map;
function createAccessors(keyOrSymbol, initialValue, handler) {
return {
set (value) {
propertyMap.set(keyOrSymbol, value);
handler(keyOrSymbol, value, this);
return value;
},
get () {
return propertyMap.has(keyOrSymbol)
? propertyMap.get(keyOrSymbol)
: initialValue;
},
};
}
function wrapSet(keyOrSymbol, proceed, handler) {
return function set (value) {
handler(keyOrSymbol, value, this);
return proceed.call(this, value);
};
}
function createAndAssignObservableDescriptor([keyOrSymbol, descriptor]) {
const { value, get, set, writable, ...descr } = descriptor;
if (isFunction(set)) {
descr.get = get;
descr.set = wrapSet(keyOrSymbol, set, handlePropertyChange);
}
if (descriptor.hasOwnProperty('value')) {
Object.assign(descr, createAccessors(keyOrSymbol, value, handlePropertyChange));
}
Object.defineProperty(obj, keyOrSymbol, descr);
}
const isFunction = value => (typeof value === 'function');
if (isFunction(handlePropertyChange)) {
const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
const ownDescrSymbols = Object.getOwnPropertySymbols(ownDescriptors);
Object
.entries(ownDescriptors)
.forEach(createAndAssignObservableDescriptor);
ownDescrSymbols
.forEach(symbol =>
createAndAssignObservableDescriptor([symbol, ownDescriptors[symbol]])
);
}
return obj;
}
// third party object (closed/inaccessible code)
const foo = { a: 1, b: 2 };
// custom changes already.
foo.userName = '';
foo.userLoginName = '';
const userNick = Symbol('nickname');
foo[userNick] = null;
console.log('`foo` before descriptor change ...', { foo });
mutateIntoObservableZombie(foo, (key, value, target) => {
console.log('property change ...', { key, value, target });
});
console.log('`foo` after descriptor change ...', { foo });
foo.a = "foo bar";
foo.b = "baz biz";
console.log('`foo` after property change ...', { foo });
foo.userName = '****';
foo.userLoginName = '************#**********';
console.log('`foo` after property change ...', { foo });
foo[userNick] = 'superuser';
console.log('`foo` after symbol property change ...', { foo });
.as-console-wrapper { min-height: 100%!important; top: 0; }
Edit
Since the above approach already is implemented generic and modular it of cause easily can be refactored into a function which allows the exact definition of which property/ies, both string and symbol based, are going to be observed ...
function observePropertyChange(obj, keysAndSymbols, handlePropertyChange) {
const propertyMap = new Map;
function createAccessors(keyOrSymbol, initialValue, handler) {
return {
set (value) {
propertyMap.set(keyOrSymbol, value);
handler(keyOrSymbol, value, this);
return value;
},
get () {
return propertyMap.has(keyOrSymbol)
? propertyMap.get(keyOrSymbol)
: initialValue;
},
};
}
function wrapSet(keyOrSymbol, proceed, handler) {
return function set (value) {
handler(keyOrSymbol, value, this);
return proceed.call(this, value);
};
}
function createAndAssignObservableDescriptor(keyOrSymbol, descriptor) {
const { value, get, set, writable, ...descr } = descriptor;
if (isFunction(set)) {
descr.get = get;
descr.set = wrapSet(keyOrSymbol, set, handlePropertyChange);
}
if (descriptor.hasOwnProperty('value')) {
Object.assign(descr, createAccessors(keyOrSymbol, value, handlePropertyChange));
}
Object.defineProperty(obj, keyOrSymbol, descr);
}
const isString = value => (typeof value === 'string');
const isSymbol = value => (typeof value === 'symbol');
const isFunction = value => (typeof value === 'function');
if (isFunction(handlePropertyChange)) {
const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
const identifierList = (Array
.isArray(keysAndSymbols) && keysAndSymbols || [keysAndSymbols])
.filter(identifier => isString(identifier) || isSymbol(identifier));
identifierList
.forEach(keyOrSymbol =>
createAndAssignObservableDescriptor(keyOrSymbol, ownDescriptors[keyOrSymbol])
);
}
return obj;
}
// third party object (closed/inaccessible code)
const foo = { a: 1, b: 2 };
// custom changes already.
foo.userName = '';
foo.userLoginName = '';
const userNick = Symbol('nickname');
foo[userNick] = null;
console.log('`foo` before descriptor change ...', { foo });
observePropertyChange(
foo,
['b', 'userLoginName', userNick],
(key, value, target) => { console.log('property change ...', { key, value, target }); },
);
console.log('`foo` after descriptor change ...', { foo });
foo.a = "foo bar";
foo.b = "baz biz";
console.log('`foo` after property change ...', { foo });
foo.userName = '****';
foo.userLoginName = '************#**********';
console.log('`foo` after property change ...', { foo });
foo[userNick] = 'superuser';
console.log('`foo` after symbol property change ...', { foo });
.as-console-wrapper { min-height: 100%!important; top: 0; }
Question
How to create a proxy for browser native DOM object?
Background
I want to intercept the settings for the element style. So I create a proxy for the DOM object. However, it causes error when I use some function like getComputedStyle().
const setHandler = (target: any, prop: PropertyKey, value: any, _receiver?: any) => {
if (/*some condition*/) {
target[prop] = value
}
return true
}
const getHandler = (target: any, prop: PropertyKey, _receiver?: any) => {
return target[prop]
}
const style = new Proxy(el.style, {
get: getHandler,
set: setHandler
})
const classList = new Proxy(el.classList,{
get: getHandler,
set: setHandler
})
const proxy = new Proxy(el/*HTMLElement*/, {
get: (target, prop, _receiver) => {
if (prop === 'target') {
return target
}
if (prop === 'style') {
return style
}
if (prop === 'classList') {
return classList
}
return getHandler(target, prop, target)
},
set: setHandler
})
const style = getComputedStyle(el)
el is the native browser DOM object. In my code, there are many methods whose parameters are el, and these methods may modify el.
I want to prevent some of these methods from modifying el, so I am trying to proxy the el object.
But after the proxy, some methods for DOM objects can't be used on proxy objects (like getComputedStyle()).
Demo
I create a demo below.
(function() {
const node = document.querySelector('#demo')
const proxy = new Proxy(node, {
getPrototypeOf(target){
return Object.getPrototypeOf(target)
},
get(target, prop, receiver){
let value = target[prop]
if (typeof value === 'function') {
value = Function.prototype.bind.call(value, target)
}
return value
},
set(target, prop,value, receiver){
target[prop] = value
},
apply(target, args, newTarget) {
return Object.apply(target,args)
},
})
console.log(proxy)
console.log(Object.getPrototypeOf(proxy))
console.log(proxy.style)
// error: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
const style = getComputedStyle(proxy)
console.log(style)
})()
<div id='demo'>demo</div>
I need to create an object that stores another objects. Each property of the big object has two properties 'value' and 'callback'.
let bigObj = {
first: {
value: true,
callback: () => {}
},
second: {
value: false,
callback: () => {}
}, {...}
}
I want to be able to get and change the value property by using bigObj.first / bigObj.first = "false", and the callback.. through the classic method: bigObj.first.callback = () => {}.
Each time the property 'value' is changed, I want to call its callback function.
Here's what I did
var proxy = new Proxy({
first: {
value: true,
callback: () => {}
}
}, {
get(target, key) {
return key in target ? target[key].value : null;
},
set(target, key, value) {
target[key] ? target[key].value = value : target[key] = {value, callback: () => {}};
key !== 'callback' && target[key].callback();
return true;
}
});
The problem is that I can not change the callback property.
proxy.first.callback = () => console.log('new cb'); // won't do anything.
Do you have any ideas on how I could change the code so it would work?
Thank you.
The way you have it set up, proxy.first is returning a boolean. So then proxy.first.callback = ends up being false.callback = or true.callback =. These at least don't throw exceptions, but they're useless. If the value was an object instead of a boolean, you could make the value itself be a proxy, but you can't create a proxy with a non-object as the target.
Another option would be to have a special value with which you set first, that tells it to insert the callback. Below is an example, where if you pass in an object like {callback: () => {}}, then it will insert that as the callback. But anything else it will get set as the value.
var proxy = new Proxy({
first: {
value: true,
callback: () => {}
}
}, {
get(target, key) {
return key in target ? target[key].value : null;
},
set(target, key, value) {
if (value && value.callback) {
target[key] ? target[key].callback = value.callback : target[key] = {value: null, callback: value.callback};
return true;
} else {
target[key] ? target[key].value = value : target[key] = {value, callback: () => {}};
target[key].callback();
return true;
}
}
});
proxy.first = {callback: () => console.log('got a callback')};
proxy.first = false;
I'm fairly new to getters and setters and am looking for a way to listen for changes in an object to store the data immediately, without calling a Save() function everytime a value gets changed. This is how I do it right now:
var myObject = {
Data: {
enabled: true,
show: false
},
Save: function () {
//store myObject.Data to local storage
},
Load: function () {
//load data from local storage and assign it to myObject.Data
},
doSomething: function () {
myObject.Load();
if (myObject.Data.enabled) {
myObject.Data.show = true;
myObject.Save();
}
}
Now I would like to optimize this code so everytime a property in myObject.Data is changed, myObject.Save() is executed. The problem I'm experiencing is that it seems only possible to define a getter for a property that has just one value, but not for a property that is an object itself.
var myObj = {
_Data: {
a: 0,
b: 1,
c: 3
},
set Data (a) {
console.log(a);
}
};
myObj.Data.a = 2;
This obviously doesn't work since myObj.Data is not an object and doesn't have the same properties as myObj._Data.
Thanks in advance for any help.
You are likely interested in the Proxy object.
I used a very simple debounce function callHandler in order to avoid calling the onSet method dozens of times during array modifications. Otherwise, [1, 2, 3].splice(0, 1) would call the set handler once per item in the original array.
'use strict';
var myObject = {
Data: {
a: [1, 2, 3],
b: {c: ['test']}
},
Save: function() {
console.log('Save called');
},
}
function recursiveProxy(target, onSet) {
// For performance reasons, onSet will only be called one millesecond
// after the set handler has last been called.
var timeout;
function callHandler() {
clearTimeout(timeout);
timeout = setTimeout(onSet, 1);
}
var recursiveHandler = {
get: function(target, property) {
// If the property is something that could contain another object,
// we want to proxy it's properties as well.
if (typeof target[property] == 'object' && target[property] != null) {
return new Proxy(target[property], recursiveHandler);
}
return target[property];
},
set: function(target, property, value) {
console.log('Set called - queueing onSet');
callHandler();
target[property] = value;
return true;
}
}
return new Proxy(target, recursiveHandler);
}
myObject.Data = recursiveProxy(myObject.Data, myObject.Save);
myObject.Data.a.splice(0, 1);
myObject.Data.b.c[0] = 'test 2';
I believe you are looking for Defining a getter on existing objects using defineProperty
To append a getter to an existing object later at any time, use
Object.defineProperty().
var o = { a:0 }
Object.defineProperty(o, "b", { get: function () { return this.a + 1; } });
console.log(o.b) // Runs the getter, which yields a + 1 (which is 1)
For e.g:
var Data = {
enable: true,
show: false
};
Object.defineProperty(Data, 'doSomething', {
get: function() {
// get something;
},
set: function(something) {
// set something
}
});