How to create a proxy for HTMLELement - javascript

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>

Related

Proxy object - access target from within a getter property?

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);

How to observe property value changes of a third party object?

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; }

Javascript variable as function name

const self = {
element: document.querySelector(selector),
html: () => self.element,
on: (event, callback) => {
self.element.addEventListener(event, callback);
},
style: {
alignContent: (property) => {
return (property === null) ? self.element.style.alignContent : self.element.style.alignContent = property;
}
}
}
I am trying to make it so I have quick access to all CSS style properties with jQuery like selectors it should work as: select('h1').style.alignContent('center'), but the problem is that I would have to make a seperate function for each style property in order for this method to work, is there a way to solve this problem without duplicating a lot of code?
//Duplication example
color: (property) => {
return (property === null) ? self.element.style.color : self.element.style.color = property;
}
One way to do this is with a Proxy (mdn):
let elemWrapper = selector => {
let element = document.querySelector(selector);
return {
element,
html: () => element,
on: (event, callback) => {
element.addEventListener(event, callback);
},
style: new Proxy({}, {
get: (obj, prop) => {
// The user called a function named "prop"
// We need to return a function that sets the style property named "prop"
return cssValue => element.style[prop] = cssValue;
}
})
};
};
let bodyElem = elemWrapper('body');
bodyElem.style.backgroundColor('cyan');
Here to prove the concept I've set the body element's background colour using a dynamically named function.
The big downside to this approach is the poor performance of Proxies (an excellent read on Proxy performance is available here).
This means it may be quicker to simply compile a list of all css property names, and define a function for each (never using Proxies). The following code compiles all css property names, to serve as a starting point:
console.log(Object.keys(document.body.style));
You can use a Proxy to intercept all attempts to get a property.
let selector = '#test';
const self = {
element: document.querySelector(selector),
html: () => self.element,
on: (event, callback) => {
self.element.addEventListener(event, callback);
},
style: new Proxy(Object.create(null), {
get(target, prop, receiver) {
if (self.element.style.hasOwnProperty(prop)) {
return val => {
if (val != null) {
self.element.style[prop] = val;
} else {
return self.element.style[prop];
}
}
}
throw Error("No such property exists: " + prop);
}
})
};
self.style.color('red')
console.log("Color:", self.style.color());
<div id="test">
This is a test
</div>
You can also wrap this into a general function like so:
const getEnhancedElement = arg => {
const element = /Element/.test(Object.prototype.toString.call(arg)) ? arg
: document.querySelector(arg);//accept a HTMLElement or a selector
return {
element,
html: () => element,
on: (event, callback) => {
element.addEventListener(event, callback);
},
style: new Proxy(Object.create(null), {
get(target, prop) {
if (element.style.hasOwnProperty(prop)) {
return val => {
if (val != null) {//set value
element.style[prop] = val;
} else {//get value
return element.style[prop];
}
}
}
throw Error("No such property exists: " + prop);
}
})
};
};
let test = getEnhancedElement("#test");
test.style.color('red')
console.log("Color:", test.style.color());
test.style.textAlign('center');
<div id="test">
This is a test
</div>
I would have something like this:
style: {
chnageStyle: (propertyName, propertyVal) => {
return (propertyName === null) ? self.element.style[propertyName] : self.element.style[propertyName] = propertyVal;
}
}
Then you can call this:
style.changeStyle('alignContent','center');
style.changeStyle('color','orange');

Javascript ES6 Proxy

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;

Is it possible to Proxy an extended class in javascript

I'm trying to Proxy an inheritance structure from within a node module and allow the client to instantiate a new Class A. Currently when trying to access class B's parent methods I get a.parentMethod is not a function
handler.js ->
module.exports = {
get(target, key, receiver) {
return target.getAttribute(key)
},
set(target, key, value, receiver) {
return target.setAttribute(key, value)
}
}
A.js ->
const handler = require('handler')
class B {
constructor(data) {
this.data = data
}
parentMethod() {
... do stuff
}
}
class A extends B {
constructor(data){
super(data)
}
}
module.exports = function(data) {
return new Proxy(new A(data), handler)
}
////
const A = require('A')
var a = new A
a.parentMethod()
Where am I going wrong with this structure? I'm new to Proxy!
Thanks
EDIT -
Further context:
I'm trying to keep sets of properties in sync based on a valueSchema I have defined. When I set Artwork.title I need Artwork['Artwork Title'] to be updated with the same value. Likewise when I retrieve Artwork.title I get the value of Artwork['Artwork Title']. Hopefully this helps a bit. I'm stuck at the above error so I can't be sure what I've written actually works yet! I'm trying to debug why the function can't be found first...
class Instance {
constructor(data) {
this._valueAttributes = {}
}
setAttribute(key, value) {
if (this._isValueAttribute(key)) {
return this._getSetValueAttribute(key, value)
}
throw Error('Cannot set invalid property '+key+' on instance.')
}
getAttribute(key) {
if (this._isValueAttribute(key)) {
return this._getSetValueAttribute(key)
}
}
_getSetValueAttribute(key, value) {
let schemaKey = this._getSchemaKey(key)
if (_.isFunction(schemaKey)) {
return alias(data)
}
if (value === undefined) {
return this._valueAttributes[schemaKey]
}
return this._valueAttributes[schemaKey] = value
}
_isValueAttribute(key) {
return _.keys(this._valueSchema).indexOf(key) === -1
}
}
class Artwork extends Instance {
constructor() {
this._valueSchema = {
medium: 'Artwork Medium',
title: 'Artwork Title'
}
}
}
///
a = new Artwork
a.title = 'thing'
a['Artwork Medium'] = 'medium';
I need
a.title == a['Artwork Title']
a['Artwork Medium'] == a.medium
It's very likely I've royally screwed it all up. I've assumed that I can access __valueSchema on the child from the parent. Is this not possible?

Categories