How to make a web component return a Proxy (and be extended)

110 views Asked by At

The goal is to have a base class A extending HTMLElement that customizes getters and setters. Then class B would extend class A and do stuff.

The way to do this is by wrapping class A with a proxy (not the instance, but the class) so B can extend A.

I tried to return a Proxy in the constructor, but I get custom element constructors must call super() first and must not return a different object

<!DOCTYPE html>

<body>
    <script>
        window.onerror = function (error, url, line) {
            document.getElementById('error').innerHTML = document.getElementById('error').innerHTML + '<br/>' + error;
        };
    </script>
    <div id="error">Console errors here:<br /></div>
    <my-b-element></my-b-element>
    <script type="module">
        class A extends HTMLElement {
            constructor() {
                super();
                return new Proxy(this, {
                    get(target, name, receiver) {
                        let rv = Reflect.get(target, name, receiver);
                        console.log(`get ${name} = ${rv}`);
                        // do something with rv
                        return rv;
                    },
                    set(target, name, value, receiver) {
                        if (!Reflect.has(target, name)) {
                            console.log(`Setting non-existent property '${name}', initial value: ${value}`);
                        }
                        return Reflect.set(target, name, value, receiver);
                    }
                });
            }
        }
        class B extends A {
            constructor() {
                super();
            }
        }
        customElements.define("my-b-element", B);
        document.querySelector('my-b-element').nonExistentProperty = 'value1';
    </script>
</body>

</html>

1

There are 1 answers

0
daniel p On

In case it helps anyone, here's how it's done without any proxy.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script>
    class Reactive extends HTMLElement {
        #data = {};
        connectedCallback() {
            this.propertyObserver();
        }
        propertyObserver() {
        const properties = Object.getOwnPropertyDescriptors(this);
        // defines the new object properties including the getters and setters
        for (let key in properties) {
          const descriptor = properties[key];
          this.#data[key] = descriptor.value;
          descriptor.get = () => this.#data[key];
          descriptor.set = (value) => {
            const result = this.trap(key, value);
            this.#data[key] = typeof result === 'undefined' ? value : result;
          }
          delete descriptor.value;
          delete descriptor.writable;
        }
        Object.defineProperties(this, properties);
      }
      trap() {
        // placeholder in case the child doesn't implement it
      }
    }

    class Child extends Reactive {
      a = 1;
      b = 2;

      constructor () {
        super();
      }
      connectedCallback() {
        super.connectedCallback();
      }

      trap(key, value) {
        // You can return a value to override the value that is set
        console.log(`LOG new ${key}:  ${value} old: ${this[key]}`);
      }

    }
    customElements.define("x-element", Child);
  </script>
</head>
<body>
<x-element></x-element>
<script>
  document.querySelector('x-element').a = 20;
</script>
</body>
</html>