Search code examples
javascriptcustom-elementes6-proxy

Unable to trap accessor calls on customElements using Proxy?


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?


Solution

  • 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