Search code examples
javascriptweb-componentnative-web-component

How to make web component to redender specific elements upon property update


class MyElement extends HTMLElement {
    constructor() {
        super();
        // Props
        this._color = this.getAttribute("color");
        this._myArray = this.getAttribute("myArray");
        // data
        
        // Shadow DOM
        this._shadowRoot = this.attachShadow({ mode: "open" });
        this.render();
    }
    template() {
        const template = document.createElement("template");
        template.innerHTML = `
            <style>
                :host {
                    display: block;
                }
                span {color: ${this.color}}
            </style>
            <p>Notice the console displays three renders: the original, when color changes to blue after 2 secs, and when the array gets values</p>
            <p>The color is: <span>${this.color}</span></p>
            <p>The array is: ${this.myArray}</p>
            
        `;
        return template;
    }
    get color() {
        return this._color;
    }
    set color(value) {
        this._color = value;
        this.render(); 
    }
    get myArray() {
        return this._myArray;
    }
    set myArray(value) {
        this._myArray = value;
        this.render();
    }

    render() {
        // Debug only
        const props = Object.getOwnPropertyNames(this).map(prop => {
            return this[prop] 
        })
        console.log('Parent render; ', JSON.stringify(props));
        // end debug
        this._shadowRoot.innerHTML = '';
        this._shadowRoot.appendChild(this.template().content.cloneNode(true));
    }
}

window.customElements.define('my-element', MyElement);
<!DOCTYPE html>

<head>
    <script type="module" src="./src/my-element.js" type="module"></script>
    <!-- <script type="module" src="./src/child-element.js" type="module"></script> -->
</head>

<body>
    <p><span>Outside component</span> </p>
    <my-element color="green"></my-element>
    <script>
        setTimeout(() => {
            document.querySelector('my-element').color = 'blue';
            document.querySelector('my-element').myArray = [1, 2, 3];
        }, 2000);
    </script>
</body>

I have a native web component whose attributes and properties may change (using getters/setters). When they do, the whole component rerenders, including all children they may have.

I need to rerender only the elements in the template that are affected.

import {ChildElement} from './child-element.js';
class MyElement extends HTMLElement {
    constructor() {
        super();
        // Props
        this._color = this.getAttribute("color");
        this._myArray = this.getAttribute("myArray");
        // Shadow DOM
        this._shadowRoot = this.attachShadow({ mode: "open" });
        this.render();
    }
    template() {
        const template = document.createElement("template");
        template.innerHTML = `
            <style>
                span {color: ${this.color}}
            </style>
            <p>The color is: <span>${this.color}</span></p>
            <p>The array is: ${this.myArray}</p>
            <child-element></child-element>
        `;
        return template;
    }
    get color() {
        return this._color;
    }
    set color(value) {
        this._color = value;
        this.render(); // It rerenders the whole component
    }
    get myArray() {
        return this._myArray;
    }
    set myArray(value) {
        this._myArray = value;
        this.render();
    }
    render() {
        this._shadowRoot.innerHTML = '';
        this._shadowRoot.appendChild(this.template().content.cloneNode(true));
    }
}

window.customElements.define('my-element', MyElement);
window.customElements.define('child-element', ChildElement);

Because each setter calls render(), the whole component, including children unaffected by the updated property, rerenders.


Solution

  • Yes, if you go native you have to program all reactivity yourself.
    (but you are not loading any dependencies)

    Not complex, Your code can be simplified;

    and you probably want to introduce static get observedAttributes and the attributeChangedCallback to automatically listen for attribute changes

    customElements.define('my-element', class extends HTMLElement {
      constructor() {
        super().attachShadow({ mode: "open" }).innerHTML = `
          <style id="STYLE"></style>
          <p>The color is: <span id="COLOR"/></p>
          <p>The array is: <span id="ARRAY"/></p>`;
      }
      connectedCallback() {
        // runs on the OPENING tag, attributes can be read
        this.color   = this.getAttribute("color");
        this.myArray = this.getAttribute("myArray"); // a STRING!!
      }
      get color() {
        return this._color;
      }
      set color(value) {
        this.setDOM("COLOR" , this._color = value );
        this.setDOM("STYLE" , `span { color: ${value} }`);
      }
      get myArray() {
        return this._myArray;
      }
      set myArray(value) {
        this.setDOM("ARRAY" , this._myArray = value );
      }
      setDOM(id,html){
        this.shadowRoot.getElementById(id).innerHTML = html;
      }
    });
    <my-element color="green" myArray="[1,2,3]"></my-element>
    <my-element color="red" myArray="['foo','bar']"></my-element>