4

Imagine this trivial custom element:

<my-el data-cute-number="7" id="foo"></my-el>

document.getElementById('foo').dataset.cuteNumber, as expected, returns the String "7". I would like to create a proxy for accessing dataset properties that does the casting to Number for me because I'm using the property alot in the component code and would like to avoid repeatedly having to cast it manually every time I access it. I also do not want to create an additional getter for a new property (e.g. get cuteNumber() { return Number(this.dataset.cuteNumber); }) on the component itself since I will have to do all the synchronisation manually then (since I'd also need a setter), make sure I avoid infinite update loops, etc.

As I understand proxies, this is exactly where a proxy will help me.

customElements.define('my-el', class extends HTMLElement {
  constructor() {
    super();
    const proxy = new Proxy(this.dataset, {
      get: function(context, prop, receiver) {
        console.log(`Proxy getter executing for ${prop}`);
        switch (prop) {
          case 'cuteNumber':
            return Number(context[prop]);
            break;
          default: 
            return context[prop];
        }
      }
    });
  }
})

console.log(typeof document.getElementById('foo').dataset.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>

This is where I'm stuck.

Accessing the dataset currently does not trigger the proxy (the inner console.log doesn't show up).

Can anyone point me in the right direction? Is it even possible to proxy the dataset of an element?

connexo
  • 53,704
  • 14
  • 91
  • 128

1 Answers1

4

Creating a proxy does not mutate the target object, it doesn't become a proxy. The proxy is a new object that wraps around the target. In your code, you are just throwing away the proxy and never use it - the .dataset property is unaffected. You'll want to either overwrite it or create a new property:

customElements.define('my-el', class extends HTMLElement {
  get dataset() {
//^^^^^^^^^^^^^
    return new Proxy(super.dataset, {
//  ^^^^^^
      get: function(target, prop, receiver) {
        console.log(`Proxy getter executing for ${prop}`);
        if (prop == 'cuteNumber')
          return Number(target.cuteNumber);
        return Reflect.get(target, prop, receiver);
      }
    });
  }
});

console.log(typeof document.getElementById('foo').dataset.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>
customElements.define('my-el', class extends HTMLElement {
  constructor() {
    super();
    this.numberdata = new Proxy(this.dataset, {
//  ^^^^^^^^^^^^^^^^^
      get: function(target, prop, receiver) {
        return Number(target[prop]);
      }
    });
  }
});

console.log(typeof document.getElementById('foo').numberdata.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • So it doesn't work for `dataset` because it is read-only, correct? – connexo Mar 26 '19 at 12:12
  • I guess you can create an own property on the instance by using `Object.defineProperty`, ignoring the setter. But there's a better solution... See my edit. – Bergi Mar 26 '19 at 12:13
  • I was hoping for a solution that wouldn't modify the way I need to access the dataset, at all. Having this intermediate object that does the proxying doesn't feel any better than creating mirrored properties on the element itself. – connexo Mar 26 '19 at 12:14
  • Your edit is looking good, except is it necessary to create a new Proxy on every read access to `dataset`? – connexo Mar 26 '19 at 13:11
  • @connexo You could also cache them in a `WeakMap` or on a (symbol?) property of the element, instantiating it either in the constructor (like in the second snippet) or lazily on first access. I don't what the overhead of proxy objects is, it might not matter maybe (unless you want to ensure that `element.dataset === element.dataset` holds)? You might want to put the `handler` object literal in a static `const` outside of the class though. – Bergi Mar 26 '19 at 14:15