Home > front end >  Changing shadowRoot.innerHTML stop inherited behavior for attributes and events
Changing shadowRoot.innerHTML stop inherited behavior for attributes and events

Time:08-29

  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 >
            <button >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 ></ake-class2>
    </body>
</html>

CodePudding user response:

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

  • Related