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>
...
In your case, you cannot use insertAjacentHTML() on the Shadow DOM shadowRoot property because the Shadow Root doesn't implement the Element interface.
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>
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>
Thanks to Danny's answer I realised I don't need to create this._shadowRoot, so my question and Supersharp's answer can be simplified to the below. I've kept the template because it is good practice to create web components from templates because performance is better than using shadowRoot.innerHTML.
const template = document.createElement("template");
template.innerHTML = `<input type="text"></input><button>add div</button>`;
class MyElement extends HTMLElement {
constructor() {
super();
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) {
const div = document.createElement("div");
div.textContent = this.shadowRoot.querySelector("input").value;
this.shadowRoot.appendChild(div);
}
}
customElements.define("my-element", MyElement);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With