Search code examples
javascriptecmascript-6web-component

Javascript Web components Setter not called for nested components


Suppose I have the following child element:

class ChildElement extends HTMLElement {
  constructor() {
    super();
    this._value = 0;
  }

  set value(newValue) {
    this._value = newValue;
    this.render();
    console.log("Value was updated")
  }

  get value() {
    return this._value
  }

  connectedCallback() {
    this.render();
  }

  render() {
    this.innerHTML = `<p>This element has value <strong>${this._value}</strong></p>`
  }
}

customElements.define("child-element", ChildElement);

_value is set to an integer here for simplicity, but in general I can have a more complex object, which is why I prefer to work with properties instead of attributes.

For simplicity, its sole goal is to displays its _value property, initially set to zero. The idea is that, every time value is changed (e.g., from another component), it is re-rendered just like a data binding.

Apart from using the <child-element></child-element>, I know I can initialize this element like:

// Initialization with default value 0
const child = document.createElement("child-element");
document.body.appendChild(child); 

// Initialization with different value 5
const child = document.createElement("child-element");
child.value = 5;
document.body.appendChild(child); 

and every time I change its value with child.value = newValue it gets properly re-rendered.

However, say I have a parent element:

class ParentElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    this.innerHTML = `<child-element></child-element>`
    this.querySelector("child-element").value = 5;  // setter is not called
  }
}

customElements.define("parent-element", ParentElement);

(I know it looks useless here but in principle it can be used to create multiple children from an array of values, which can be updated and be used to re-render them, for example)

The problem is that the setter of the child is never called and its HTML is never updated. Even if I manually query the child from the parent nothing happens:

const parent = document.querySelector("parent-element");
const child = parent.querySelector("child-element");
child.value = 10;  // does not trigger the `setter`

If I inspect child I can see that child.value is now 10, but child._value is still 0.

I tried replacing the line this.innerHTML = <child-element></child-element> with a manual createElement("child-element") but still no luck.

However, I noticed that, if I remove the this.querySelector("child-element").value = 10; line from the connectedCallback, the setter starts begin called and the previous code works.

So, does anyone know what is going on?


Solution

  • The only possibility this can happen is that your web component is possibly in a limbo state i.e. unknown tags stage. This happens when your code that's responsible for calling setters on ChildElement is being executed before the component is registered in a registry.

    For example, if you have following sequence of code, then setter would not be called:

    function main() {
      
      document.body.innerHTML = '<parent-element></parent-element>';
      const parent = document.querySelector('parent-element');
      const child = parent.querySelector('child-element');
      child.value = 10;
    
      // Child element defined after invoking the setter...
      customElements.define('child-element', ChildElement);
    }
    
    main();
    

    Custom elements go through something called as upgrade process. So it is not an error when you call the element's setter before it is defined or upgraded. Thus, when you say:

    If I inspect child I can see that child.value is now 10, but child._value is still 0.

    It simply means that it created an element of type HTMLElement and you simply added a property called value with 10 on it which overrides value setter or getter that you create in the ChildElement class.