Westbrook Johnson

thinks he might

write more:

someday,

here.

In my day-to-day work, I tend to write custom elements using the LitElement base class. This is great because it reduces boilerplate, focuses me on the functionality I'm delivering, and enforces at least a baseline normalization of patterns with my coworkers. However, I at least put a passing attempt at writing vanilla web components when bringing together explainers and conversation around non-Lit contexts; see articles on the styling of basic custom elements or the internals of custom elements. That process often leaves me to think out loud about the patterns that might be useful for leveraging when building many/shared/distributed custom elements with vanilla JS, here's one...

Static blocks in JS classes

First published: 2022-11-26

Classes in JS have had static properties and methods for some time. Place a static property on a class, and only access that value through the class:

class Awesome {
static amount = '100%';
}

console.log(Awesome.amout); // 100%

console.log(new Awesome().amount); // undefined;

Static blocks take this one step further by allowing you a define time mechanism by which to functionally work on those values. So, much in the way a constructor would be called once every time a new instance of the class is created, these static blocks would be called once every time the class was defined.

class Awesome {
constructor() {
console.log('construct');
}
static {
console.log('static block');
}
}

// static block
new Awesome(); // construct
new Awesome(); // construct
new Awesome(); // construct
new Awesome(); // construct
new Awesome(); // construct

I'll be honest with you in saying that the above examples leave me a bit nonplused. Even the examples on MDN seem to border right on the edge of frivolity when taking into account the way I tend to work with JS classes. However, there is one pattern this does remind me of; the way vanilla custom element look to share a single base template from which they can clone their shadow root.

const template = document.createElement("template");
template.innerHTML = /*html*/`
<slot></slot>
`
;

class CustomElement extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: "open"
});
this.shadowRoot.appendChild(
template.content.cloneNode(true)
);
}
}

customElements.define('custom-element', CustomElement);

You see that template variable hanging of what's likely a JS module like a class in and of itself, but requiring some initialization hand-holding? Well, what if we placed that in a static block?

class CustomElement extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: "open"
});
this.shadowRoot.appendChild(
this.template.content.cloneNode(true)
);
}
static template;
static {
this.template = document.createElement("template");
this.template.innerHTML = /*html*/`
<slot></slot>
`
;
}
}

customElements.define('custom-element', CustomElement);

Technically, you could go a few steps further, if you liked this pattern.

class CustomElement extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: "open"
});
this.shadowRoot.appendChild(
this.template()
);
}
static template;
static {
const template = document.createElement("template");
const template.innerHTML = /*html*/`
<slot></slot>
`
;
this.template = () => template.content.cloneNode(true);
}
}

customElements.define('custom-element', CustomElement);

The way this refocuses all that is the custom element back into the custom element's class definition is kind of nice. It also opens up some new areas of code reusability to explore by making somewhat equal paths to consuming the custom element definition as a custom element and as a DOM snippet. You can see a version of this style of reuse in a project by Rody Davis that he's called lit-modules. Fully exploring what they might actually open up to a developer is a pattern exploration for another day, but it does feel that as soon as static blocks get full cross-browser support that there may be something interesting to pursue there.


Editors note: some of the APIs outlined in the examples above are not yet available cross-browser. While these are the target APIs that browsers have agreed to, in order to support cross-browser delivery of this article, live demo code is modified from the example to work fully cross-browser.