The Elm guide section about using custom elements for JavaScript interoperability has the following custom element example:
customElements.define('intl-date',
class extends HTMLElement {
// things required by Custom Elements
constructor() { super(); }
connectedCallback() { this.setTextContent(); }
attributeChangedCallback() { this.setTextContent(); }
static get observedAttributes() { return ['lang','year','month']; }
// Our function to set the textContent based on attributes.
setTextContent() {
const lang = this.getAttribute('lang');
const year = this.getAttribute('year');
const month = this.getAttribute('month');
this.textContent = localizeDate(lang, year, month);
}
}
);
I could not understand why my custom elements written this way were rendered multiple times ![]()
The reason is that attributeChangedCallback() is called for every observed attribute at initialization time before connectedCallback() finally attaches the custom element to the DOM.
In the example above, with 3 observed attributes, it means setTextContent() is called 4 times total! Just add a console.log() to believe it.
Here is the code I use now to prevent this redundant rendering:
customElements.define('intl-date',
class extends HTMLElement {
#isConnected = false
connectedCallback() {
this.#setTextContent();
this.#isConnected = true;
}
attributeChangedCallback() {
if (this.#isConnected) this.#setTextContent();
}
static observedAttributes = ['lang','year','month'];
#setTextContent() {
const lang = this.getAttribute('lang');
const year = this.getAttribute('year');
const month = this.getAttribute('month');
this.textContent = localizeDate(lang, year, month);
}
}
);
You’ll noticed a few other optimizations:
- I’m not using
constructor()along withsuper()as I don’t want to modify the constructor inherited fromHTMLElement. And it’s working fine without it. - I’m using a static field instead of a getter for
observedAttributes: after the release of Safari 14.1 in May 2021 static class fields are available in all major browsers. - I’m using #-prefixed private fields so they can be safely mangled by Terser or any other JS minifier.
Don’t you think the docs should be updated to prevent this weird multiple rendering?