Search code examples
javascriptweb-componentnative-web-component

Dynamically add elements inside web component


I would like to create a web component that contains a list of elements that can be added to. For example if I had an initial template like:

const template = document.createElement("template");
template.innerHTML = `<input type="text"></input><button>add div</button>`;

class MyElement extends HTMLElement {
  constructor() {
    super();
    this._shadowRoot = this.attachShadow({ mode: "open" });
    this._shadowRoot.appendChild(template.content.cloneNode(true));
    const button = this._shadowRoot.querySelector("button");
    button.addEventListener("click", this.addDiv);
  }
  addDiv(e) {
    // ...
  }
}
customElements.define("my-element", MyElement);

and each time the button is clicked, a <div> is added which contains the text from the input field, thereby creating something like:

<input type="text"></input><button>add div</button>
<div>first text from input added</div>
<div>second text from input added</div>
...

Solution

  • In your case, you cannot use insertAjacentHTML() on the Shadow DOM shadowRoot property because the Shadow Root doesn't implement the Element interface.

    Use bind( this )

    A better solution is to use appendChild() on the shadowRoot property. However, you'll need to add a special bind() operation on the click event callback.

    In an event callback, this indeed references the element that triggered the event, not the object which the callback is defined in.

    In order to get the reference to the custom element (in order to access the input element in the Shadow DOM shadowRoot, call bind(this) inside addEventListener().

    button.addEventListener( "click", this.addDiv.bind( this ) )
    

    See a full example below:

    const template = document.createElement("template");
    template.innerHTML = `<input type="text"></input><button>add div</button>`;
    
    class MyElement extends HTMLElement {
      constructor() {
        super();
        this._shadowRoot = this.attachShadow({ mode: "open" });
        this._shadowRoot.appendChild(template.content.cloneNode(true));
        const button = this._shadowRoot.querySelector("button");
        button.addEventListener("click", this.addDiv.bind( this ) );
      }
      addDiv(e) {
        var div = document.createElement( 'div' )
        div.textContent = this.shadowRoot.querySelector( 'input' ).value
        this.shadowRoot.appendChild( div )
      }
    }
    customElements.define("my-element", MyElement);
    <my-element></my-element>


    Use arrow function

    Another solution whould be to use an arrow function. With arrow function, this is not redefined, so you don't need to use bind().

    class MyElement extends HTMLElement {
      constructor() {
        super()
        const sh = this.attachShadow( { mode: "open" } )
        sh.innerHTML = `<input type="text"></input><button>add div</button>`
        const button = sh.querySelector( "button" )
        button.onclick = ev => {
          let div = document.createElement( "div" )
          div.textContent = sh.querySelector( "input" ).value
          sh.appendChild( div )
        }
      }
    }
    customElements.define( "my-element", MyElement )
    <my-element></my-element>