Unable to trap accessor calls on customElements using Proxy? - javascript

I'm registering some custom elements using customElements.define and would like to automatically set up traps on member accessors so that I can emit events when they change
class State extends HTMLElement {
public someValue = 1;
public constructor() {
super();
console.log('State constructor');
}
}
const oProxy = new Proxy(State, {
get(target, prop: string) {
console.log(`GET trap ${prop}`);
return Reflect.get(target, prop);
},
set(target, prop: string, value: any) {
console.log(`SET trap ${prop}`);
return Reflect.set(target, prop, value);
}
});
customElements.define('my-state', oProxy);
const oStateEl = document.querySelector('my-state');
console.log(oStateEl.someValue);
console.log(oStateEl.someValue = 2);
console.log(oStateEl.someValue);
My browser doesn't seem to have a problem with the above code and I can see some trap output as the element is set up
GET trap prototype
GET trap disabledFeatures
GET trap formAssociated
GET trap prototype
But when I manually get/set values the traps aren't triggered.
Is this even possible?

What I ended up doing was moving all member variable values to a private object and dynamically defining a getter/setter for each as soon as the custom element was mounted on the DOM like so...
//
class State extends HTMLElement {
protected _data: object = {}
public connectedCallback() {
// Loop over member vars
Object.getOwnPropertyNames(this).forEach(sPropertyKey => {
// Ignore private
if(sPropertyKey.startsWith('_')) {
return;
}
// Copy member var to data object
Reflect.set(this._data, sPropertyKey, Reflect.get(this, sPropertyKey));
// Remove member var
Reflect.deleteProperty(this, sPropertyKey);
// Define getter/setter to access data object
Object.defineProperty(this, sPropertyKey, {
set: function(mValue: any) {
console.log(`setting ${sPropertyKey}`);
Reflect.set(this._data, sPropertyKey, mValue);
},
get: function() {
return this._data[sPropertyKey];
}
});
});
}
}
//
class SubState extends State {
public foobar = 'foobar_val';
public flipflop = 'flipflop_val';
public SubStateMethod() { }
}
//
window.customElements.define('sub-state', SubState);
//
const oState = document.querySelector('sub-state') as SubState;
oState.foobar = 'foobar_new_val';
That way I can still get/set values on the object as normal, typescript is happy that the member variables exist, and I can trigger custom events when members are accessed - all while allowing custom elements to exist within the markup at DOM ready

Related

How to access to a class's property without an instance?

Currently, I have a class of this sort:
class MyClass {
constructor(c) {
this.a = "a";
this.b = "b";
}
myMethod() {
return c;
}
}
As you can see c would be a property that I want to be returned when I execute a method on an instance, but it is not in the object instance.
Private properties would not work, because if I stringify the object, the property is also in the string, and I don't want it there.
Is there any way to achieve this? Not necessarily a complete solution but some hints would be enough.
Private properties would not work, because if I stringify the object, the property is also in the string
No it's not? This works fine:
class MyClass {
#c;
constructor(c) {
this.a="a";
this.b="b";
this.#c=c;
}
myMethod() {
return this.#c;
}
}
const obj = new MyClass('hi');
console.log(JSON.stringify(obj));
console.log(obj.myMethod());
An alternative would be to create the method in the constructor, as a closure over the c variable:
class MyClass {
constructor(c) {
this.a="a";
this.b="b";
this.myMethod = () => {
return c;
};
}
}
const obj = new MyClass('hi');
console.log(JSON.stringify(obj));
console.log(obj.myMethod());
Further alternatives that work with normal properties and prevent inclusion in the JSON.stringify result are to make the c property non-enumerable or to define a custom toJSON method.
In my real world class I added the properties:
class MyClass{
//other props
preQ: string;
postQ: string;
constructor(data: InputData = { cli: cli.flags }) {
Object.defineProperties(this, {
preQ: { enumerable: false },
postQ: { enumerable: false },
});
// other stuff
}
}
As indicated to in the first comment by Pointy. The properties are not present in the JSON.stringify result.
Which are properties that I do not want to be sent to the server.
Use static Keyword :
MDN Documentation :
Static properties cannot be directly accessed on instances of the
class. Instead, they're accessed on the class itself.
Static methods are often utility functions, such as functions to
create or clone objects, whereas static properties are useful for
caches, fixed-configuration, or any other data you don't need to be
replicated across instances.
Example
class Student {
name: string;
static age: number;
constructor(age: number) {
this.name = 'Jhon';
Student.age = age;
}
static getAge = () => Student.age;
}
const student = new Student(20);
const json = JSON.stringify(student); // {"name":"Jhon"}
console.log(json);
console.log(Student.getAge()); // 20
Your code:
class MyClass {
a: string;
b: string;
static c: string;
constructor(c:string) {
this.a = 'a';
this.b = 'b';
MyClass.c = c;
}
myMethod() {
return MyClass.c;
}
}
const obj = new MyClass('hi');
console.log(JSON.stringify(obj)); // {"a":"a","b":"b"}
console.log(obj.myMethod()); // hi

Weird issue about JavaScript Proxy and getter functions

The two test cases blow both pass. I simply don't understand the behavior. It seems that JavaScript Proxy cannot trap property getting inside a getter function.
test('JS Proxy normal method', () => {
class Store {
hidden = false;
visible() {
return !this.hidden;
}
}
const accessList: PropertyKey[] = [];
const proxy = new Proxy<Store>(new Store(), {
get: (target: any, propertyKey: PropertyKey) => {
accessList.push(propertyKey);
return Reflect.get(target, propertyKey);
},
});
expect(proxy.visible()).toBe(true);
expect(accessList).toEqual(['visible', 'hidden']);
});
test('JS Proxy getter method', () => {
class Store {
hidden = false;
get visible() {
return !this.hidden;
}
}
const accessList: PropertyKey[] = [];
const proxy = new Proxy<Store>(new Store(), {
get: (target: any, propertyKey: PropertyKey) => {
accessList.push(propertyKey);
return Reflect.get(target, propertyKey);
},
});
expect(proxy.visible).toBe(true);
expect(accessList).toEqual(['visible']);
});
You're missing the receiver of the property access. The property might be defined on a different object than it is accessed on, and your Reflect.get call needs to take that into account. In particular, the receiver you get as a argument of the get trap is the proxy itself, and that's also the object you want to evaluate the getter against, so that its this value refers to the proxy. However, Reflect.get(target, propertyKey) is the same as target[propertyKey], where the this value in the getter is set to the target and the .hidden property access can't be detected by your proxy.

nested properties on a class instance

I need to build a structure (i need to do it for jestjs, in order to stub a module) that allows me to access a method in the following way:
// I make an instance of a class
const myTest = new Test()
// I access a method in this way
const result = myTest.prop1.prop2.prop3.getAll()
A basic class would look like
class Test2 {
constructor(){}
//getter
getAll(){
return 'Salutes'
}
}
And I can access getAll()
const myTest2 = new Test()
const result = myTest.getAll()
//in this way result contains a string (*salutes*)
So how can I add more properties in the middle? I found something, but in typescript... I need to do it in javascript
Assuming you need exactly the structure you've specified, in Test you'd have to create the object and have any methods you need be arrow functions or bound functions (if they need to access information from the Test instance; your example didn't so it wouldn't matter whether they had the right this). For instance:
class Test {
constructor() {
this.prop1 = {
prop2: {
prop3: {
getAll: () => {
return "Salutes";
},
getAll2() { // Also works
return "Salutes";
},
getAll3: function() { // Also works
return "Salutes";
}
}
}
}
}
}
const myTest = new Test();
const result = myTest.prop1.prop2.prop3.getAll();
console.log(result);
console.log(myTest.prop1.prop2.prop3.getAll2());
console.log(myTest.prop1.prop2.prop3.getAll3());
Example of using information on the Test instance (it's the same except for the constructor parameter and message property):
class Test {
constructor(message) {
this.message = message;
this.prop1 = {
prop2: {
prop3: {
getAll: () => {
// `this` refers to the `Test` instance because
// this is an arrow function
return this.message;
}
// `getAll2` and `getAll3` wouldn't work because
// they would get `this` based on how they're called
// (it would be the `this.prop1.prop2.prop3` object).
}
}
}
}
}
// I make an instance of a class
const myTest = new Test("Salutes encore!");
// I access a method in this way
const result = myTest.prop1.prop2.prop3.getAll();
console.log(result);
If you want to achieve it without modifying the Test2 class, you can use Proxy class and define custom handler for get method:
class Test2 {
constructor(){}
getAll(){
return 'Salutes'
}
}
let instance = new Test2();
const handler = {
get: function(target, prop, receiver) {
if (['prop1', 'prop2', 'prop3'].includes(prop)) {
return receiver;
}
return Reflect.get(...arguments);
}
};
const proxy = new Proxy(instance, handler);
console.log(proxy.prop1.prop2.prop3.getAll());
console.log(proxy.prop7);
console.log(proxy.getAll());

is there any chance to get the outer scope in Proxy?

I am Proxing an object in a custom class and I'd like to get access to the methods and properties of that same class within my Proxy Object. Is it possible?
I thought there is a way to bind the context but it didn't work for me neight with apply, call nor bind.
Any suggestions would be appreciated!
class MyClass {
constructor() {
this.state = new Proxy(this.initialState, {
set(target, prop, value) {
// some logic goes here
}
})
}
methodIneedToReach() {}
}
I need it to structure the code and prevent the mess.
Either store the value of this in a variable called that for example and use that.methodIneedToReach inside the set method, or, better yet, use an arrow function for set. Since arrow functions don't have their own this keyword, they will use the surrounding one which is, in this case, the instance of your class:
class MyClass {
constructor() {
this.state = new Proxy(this.initialState, {
set: (target, prop, value) => { // set is an arrow function
this.methodIneedToReach(); // works because 'this' inside here points to your class
}
})
}
methodIneedToReach() {}
}
Demo:
class MyClass {
constructor() {
this.initialState = { message: "Hello World!" };
this.state = new Proxy(this.initialState, {
set: (target, prop, value) => {
this.methodIneedToReach();
}
})
}
methodIneedToReach(value) {
console.log("'methodIneedToReach' is called");
}
}
let inst = new MyClass();
inst.state.message = "Bye world!";

Javascript / Typescript - get object property visibility and type

My problem:
I need to differentiate between the private, public and getter (get X()) properties of a typescript class.
My Project:
I have an Angular project, that has a model design pattern. Aka. an user model would look like this
class UserModel extends BaseModel {
private _id: number;
get id() { return this._id; }
set id( _id: number ) { this._id = _id; }
}
To send these models to the backend, I just JSON.stringify() them, which if the user id is set as 13, returns an object like this
{
_id: 13
}
Now I need to modify the toJSON() function on the UserModel, so that instead of returning the private properties of the object, I will return the get X() variables only. The output should look like this.
{
id: 13
}
I made this simple function, to retrieve all properties of an object, but this gives me the private properties and the get properties both.
abstract class BaseModel {
public propsToObj() : {[key: string]: any} {
let ret: any = {};
for (var prop in this) {
ret[prop] = this[prop];
}
return ret;
}
}
and the toJSON function looks like this
class UserModel extends BaseModel {
private _id: number;
get id() { return this._id; }
set id( _id: number ) { this._id = _id; }
toJSON() {
return this.propsToObj();
}
}
The outcome of stringify-ing the UserModel looks like this
{
_id: 13,
id: 13
}
In conclusion, I need to know the visibility and type (getter or regular variable?) of properties on a class, how would I achieve this?
your propsToObj() is working wrong, it gets just all properties, you need to change it so it will get only getters, for example you can use this
abstract class BaseModel {
public propsToObj() : {[key: string]: any} {
let ret: any = {};
for (const prop in this) {
const descriptor = Object.getOwnPropertyDescriptor(this.constructor.prototype, prop);
if (descriptor && typeof descriptor.get === 'function') {
ret[prop] = this[prop];
}
}
return ret;
}
}
Object.getOwnPropertyDescriptor will get descriptor of a property and from which you can check if there is get function in descriptor, if it is then your property is getter, if not it is regular property, you can read more about descriptors here MDN(descriptors)
The last question you asked
I need to know the visibility and type of properties on a class, how
would I achieve this?
As I know you can't get the visibility of a property, as for the type if you want to know data type of a property you can use typeof for it.
EXAMPLE in propsToObj() method:
public propsToObj() : {[key: string]: any} {
let ret: any = {};
for (const prop in this) {
const descriptor = Object.getOwnPropertyDescriptor(this.constructor.prototype, prop);
if (descriptor && typeof descriptor.get === 'function') {
ret[prop] = this[prop];
console.log(typeof ret[prop]); // just exaple how you can know type you can save it with property too if you need it
}
}
return ret;
}

Categories