Search code examples
javascriptclassinheritanceshadow-domcustom-element

Changing shadowRoot.innerHTML stop inherited behavior for attributes and events


  1. Below ake-class2 inherits from/extends ake-class1.
  2. Adding <select> element to ake-class2.shadowRoot.
  3. console.log this.clickme button to make sure it's inherited correctly.
  4. clickme button doesn't work without adding again lines after comment These 3 lines in ake-class2.

I couldn't understand why this behavior happen. why this happpens ?

<html>
    <head>
        <title>AKE Front</title>
        <script>
            class1_html = `
            <div class="container">
            <button class="clickme">Click Me</button>
            </div>
            `
            class2_html = `
            <select></select>
            `
            /*--------------------------------------------------------------------------------*/
            class AKEclass1 extends HTMLElement  { //custom-component class
                constructor() {
                    super(); // always call super() first in the constructor.
                    //const root = this.createShadowRoot(); //chrome only - deprecated
                    const root = this.attachShadow({mode: 'open'}); //By calling attachShadow with mode: 'open', we are telling our element to save a reference to the shadow root on the element.shadowRoot property
                    this.shadowRoot.innerHTML = class1_html;
                    // These 3 lines
                    this.container = this.shadowRoot.querySelector("div.container");
                    this.clickme = this.container.querySelector("button.clickme");
                    this.clickme.addEventListener("click", this.clickMe.bind(this));
                }
                clickMe() {
                    alert("Hello !");
                }
            }
            customElements.define('ake-class1', AKEclass1);
            /*--------------------------------------------------------------------------------*/
            class AKEclass2 extends AKEclass1 { //custom-component class
                constructor() {
                    super(); // always call super() first in the constructor.
                    this.shadowRoot.innerHTML += class2_html;
                    // These 3 lines
                    //this.container = this.shadowRoot.querySelector("div.container");
                    //this.clickme = this.container.querySelector("button.clickme");
                    //this.clickme.addEventListener("click", this.clickMe.bind(this));
                }
            }
            customElements.define('ake-class2', AKEclass2);
            /*--------------------------------------------------------------------------------*/
        </script>
    </head>
    <body>
        <ake-class2 class="ake_window"></ake-class2>
    </body>
</html>


Solution

  • As mentioned in the comments .innerHTML += is the culprit.

    What it does:

    • Create a NEW string by concatening .innerHTML + NEWString

    • delete the innerHTML DOM tree
      and then Garbage Collection (GC) kicks in:

      • Delete all existing DOM elements, thus remove all connected listeners
    • set the NEW String as innerHTML

    Some 'gurus' say this makes innerHTML evil, I say you need to understand what it does.


    In the SO snippet below you see the listener being connected twice, but only executed once when clicked

    <script>
      class BaseClass extends HTMLElement { 
        constructor() {
          super().attachShadow({mode:'open'})
                 .innerHTML = `<button>Click ${this.nodeName}</button>`;
          this.listen();// but removed by GC
        }
        listen(){
          console.log("add listener on", this.nodeName);
          this.shadowRoot
              .querySelector("button")
              .onclick = (evt) => this.clicked(evt); 
        }
        clicked(evt){
            console.log("clicked", this.nodeName)
        }
      }
      //customElements.define('element-1', BaseClass);
      customElements.define('element-2', class extends BaseClass {
        connectedCallback(){
          this.shadowRoot.innerHTML += ` with concatenated HTML`;
          this.listen();
        }
      });
    </script>
    <element-2></element-2>

    Notes:

    • Using the inline onclick handler, it only allows for one handler where addEventListener can add more (you can use it here if you like)

    • No need for oldskool .bind(this) by defining lexical scope with a arrow function, not a function reference

    • all can be chained because

      • super() sets AND returns the this scope

      • attachShadow sets AND returns this.shadowRoot