Search code examples
javascriptecmascript-6web-componentes6-classes6-proxy

Custom elements with proxied has, get & set


My goal is to create a class ObservableElement extending HTMLElement such that it can be used to define custom elements, e g:

customElements.define('an-observable-elem', class extends ObservableElement {
  construct() {
    super()
    ...
  }
  ...
})

The thing about any element based on ObservableElement is that it should have some particular behaviours wrt to properties.

First, 'whatever' in myElem should always be true. In other words, I'd like a proxy trap for has on the instance, which just always returns true.

Second, setting and getting any props should work. But whenever a prop which is not explicitly defined on the element or in the prototype chain is set, I want to emit a custom event with the name set${propname} and detail: theValue.

It seems there must be a way using es6-proxies. Naively, I first tried:

class ObservableElement extends HTMLElement {
  constructor () {
    super()
    const vals = {}
    return Proxy(this, {
      has: _ => true,
      get: name => {
        if (name in this) return this[name]
        if (name in vals) return vals[name]
        return null
      },
      set: (name, value) => {
        if (name in this) {
          this[name] = value
          return
        }
        if (vals[name] === value) return
        vals[name] = value
        this.dispatchEvent(new CustomEvent(`set${name}`, {detail: value}))
      }
    })
  }
}

But of course that didn't work. Returning the proxy from the constructor did not change the this value in extending classes' constructors. I fumbled around with all sorts of combinations of proxying construct on the class, Object.setPrototypeOf(...) et c to no avail.

I'd much appreciate it if anyone who understands how these things can fit together to achieve what I want would explain it to me. Thanks!


Solution

  • What you're trying to do is not supported by the W3C Web Components specification.

    According to §2.4 of the W3C Web Components specification, step 10.6.2:

    If observedAttributesIterable is not undefined, then set observedAttributes to the result of converting observedAttributesIterable to a sequence<DOMString>. Rethrow any exceptions from the conversion.

    This occurs during the customElements.define() execution, which means that you'd have to define an iterable of every conceivable string when the class is defined in order to intercept all attribute changes.

    If the component does not find the changed property in that sequence, it does not call the attributeChangedCallback().

    Similarly it is impossible to return a proxy from the constructor because of the restriction in §2.2:

    A return statement must not appear anywhere inside the constructor body, unless it is a simple early-return (return or return this).