Search code examples
javascripttypescriptcreate-react-appweb-component

The setters and getters are not called in a custom web component


I have the following web component:

I originally tested this locally, (in a create-react-app) didn't work. I then put it into codesandbox (react template) where it doesn't work either.

class CounterWebComponent extends HTMLElement {
  constructor() {
    console.log("ctor");
    super();
  }

  _count: number = 0;

  set count(value: number) {
    console.log("CounterWebComponent set", value);
    this._count = value;
  }

  get count() {
    console.log("CounterWebComponent get", this._count);
    return this._count;
  }

  connectedCallback() {
    console.log("CounterWebComponent connected");
    this.innerHTML = "<h1>fff</h1>";
  }
  disconnectedCallback() {
    console.log("CounterWebComponent disconnected");
  }
}

if (!customElements.get("counter-wc-custom"))
  customElements.define("counter-wc-custom", CounterWebComponent);

In index.html I'm trying to set the count:

    <counter-wc-custom id="counter1"></counter-wc-custom>
    <script>
      const counter2 = document.getElementById("counter1");
      counter2.count = 23;
      alert(counter2.count);
      alert(counter2._count); //using alert because console.log inside index.html is not working for some reason in codesandbox
      console.log("count", counter2.count); 
      console.log("_count", counter2._count);

      counter2.addEventListener("count", (e) => console.log(e));
    </script>

If I use it like this it works:

const a = document.createElement("counter-wc-custom");
a.count = 2;
console.log("count", a.count);
console.log("_count", a._count);

EDIT: https://codesandbox.io/s/react-web-component-custom-wrapper-xh2n9s (I forgot the link like an idiot)

I assume I configured something somewhere incorrectly but I have no idea what. Any help is appreciated.


Solution

  • This has to do with timing; specifically, when you set the property versus when you define the custom element. Let's say we do:

    <hello-world></hello-world>
    <script>
    const helloWorld = document.querySelector('hello-world');
    
    helloWorld.message = 'Foo!'
    
    customElements.define('hello-world', class extends HTMLElement {
        #message = 'Default message.'
        get message(){ return this.#message; }
        set message(value){
            this.#message = value;
            console.log('Message: ', this.#message);
        }
    });
    
    helloWorld.message = 'Bar?'
    </script>
    

    In this example, we set the message property on the helloWorld instance before the instance is defined. Then when we define the custom element, the getters and setters are defined on the element's prototype; but, the helloWorld instance now already has an "own" property message. This means even setting the message to Bar? after definition doesn't trigger the setter, because it's still just using the property initially defined on the instance, completely ignoring the getters and setters.

    If we now delete helloWorld.message, we remove that initially defined property, and then doing helloWorld.message = 'Baz!' will finally trigger the setter.

    You can actually check that this is the case by getting a reference to the custom element instance in question and seeing what the result is of Object.getOwnPropertyDescriptors(elementInstance). Any properties included in the result of that call are "overwriting" getters and setters you may have defined on the prototype, and so are likely unwanted.

    In the example I gave above, the order things run in is (intentionally) obvious. In codesandbox, it might be a little less obvious; looking at the actual page it outputs it seems like it is indeed running your .count = 23 before it defines the component (and the above check indeed reveals count to be among the ownPropertyDescriptors).

    The way I see it, there are a two main ways to solve this issue:

    1. Make sure your custom element definitions run before doing setting properties on instances, either by rearranging your included scripts or through customElements.whenDefined(...).
    2. In case your custom element needs to support this, adjust your component to read any own property descriptors it can find on the instance and do the delete-set thing I mentioned above in the custom element constructor.