Reputation: 969
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.
Upvotes: 0
Views: 1734
Reputation: 21173
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>
Upvotes: 2
Reputation: 20970
You are missing a certain level of abstraction. You are trying to emulate the Vue/React way of doing things where whenever the props change, the render()
function is repeatedly called. But in these frameworks, render function doesn't do any DOM manipulation. It is simply working on Virtual DOM. Virtual DOM is simply a tree of JS object and thus very fast. And, that's the abstraction we are talking about.
In your case, it is best if you rely on some abstraction like LitElement or Stencil, etc. - any web component creation framework. They would take care of handling such surgical updates. It also takes care of scheduling scenarios where multiple properties are changed in a single event loop but render is still called exactly once.
Having said that, if you want to do this by yourself, one important rule: Never read or write to DOM from the web component constructor function. Access to DOM is after the connectedCallback
lifecycle event.
Here is the sample that you can try and expand:
import { ChildElement } from './child-element.js';
class MyElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: "open" });
this.attached = false;
}
connectedCallback() {
// Check to ensure that initialization is done exactly once.
if (this.attached) {
return;
}
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// Shadow DOM
this.render();
this.attached = true;
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
span {color: ${this.color}}
</style>
<p>The color is: <span id="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.shadowRoot.querySelector('#span').textContent = this._color;
this.shadowRoot.querySelector('style').textContent = `
span { color: ${this._color}; }
`;
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.shadowRoot.querySelector('#span').innerHTML = this._myArray;
}
render() {
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
window.customElements.define('child-element', ChildElement);
The above example is a classical vanilla JS with imperative way of updating the DOM. As said earlier to have reactive way of updating the DOM, you should build your own abstraction or rely on library provided abstraction.
Upvotes: 0