Paul Kinlan was kind enough to share his experience efficiently packaging a stand-alone custom element. His thoughts happen to chime with my own efforts over the last few months, so let’s compare notes. (This is a bit of a rush job to avoid procrastinating; feel free to comment below if anything requires clarification.)

Paul writes

My main goal with this project was to encapsulate everything inside a single ES6 Class

This is also where I’d started: The compelling thing about custom elements is that you start out with declarative markup, which effectively expands to more complex DOM structures if and when the client-side JavaScript binding kicks in. (While I have strong feelings about what should constitute that initial markup, I’ll spare you the progressive enhancement encomium here as it seems not entirely germane.)

<blog-post ...>...</blog-post>
class BlogPost {
    connectedCallback() {
        // render template, turning initial markup into an augmented DOM
    }

    ...
}
// bind implementation to markup
customElements.define("blog-post", BlogPost);

Since we’re talking about the augmented version here, it makes perfect sense to embed the corresponding template in our component’s JavaScript file.

Note that I’m assuming we want some sort of declarative template rather than a series of imperative DOM operations. The latter very quickly becomes unwieldy, hard to grok and maintain. Yet, as expressed by Nick Kotenberg:

Client-side templating is overly complicated, ultimately what you actually want is a function you can call from your JS that puts your data in a template. Why should I have to send a bunch of strings with Mustaches {{}} or other silly stuff for the client to parse? Ultimately, all I want is a function that I can call with some variable to render the string I want.

(cf. last year’s components are like functions assertion)

So while for development we want declarative syntax, at runtime all we need are lightweight functions to generate the respective DOM nodes. Thus in simplified terms

<article class="blog-post">
    <h3>$title</h3>
    <p>$desc</p>
</article>

becomes something like

(title, desc) => `<article class="${encode("blog-post")}">
    <h3>${encode(title)}</h3>
    <p>${encode(desc)}</p>
</article>`;

Well, turns out that transformation is exactly what JSX, transpiler of React notoriety, provides: It turns HTML-like syntax into createElement invocations, thus

createElement("article", { class: "blog-post" },
        createElement("h3", null, title),
        createElement("p", null, desc));

JSX leaves it up to you what that createElement function looks like internally, so I wrote a tiny wrapper mapping this to native DOM operations. (In fact, I’m also working on a variant which writes to an HTTP response stream for server-side rendering.)

While I’m not hugely enthusiastic about JSX per se, it’s actually fairly decent and powerful — particularly with regard to composition. In contrast to most templating engines I’d considered, this straightforward transformation means developers/designers can declaratively describe DOM structures without incurring runtime overhead.[1] (Conceptually, one could easily imagine replacing JSX with an alternative precompiler.)

The crux is that we can simply import our template like any other JavaScript file:

// components/blog_post/index.js
import BlogPost from "./element";

customElements.define("blog-post", BlogPost);
// components/blog_post/element.js
import render from "./template";

class BlogPost {
    connectedCallback() {
        ...
        let ui = render(title, desc);
        this.appendChild(ui);
    }

    ...
}
// components/blog_post/template.jsx
import { createElement } from "uitil/dom/create";

export default (title, desc) => <article class="blog-post">
    <h3>{title}</h3>
    <p>{desc}</p>
</article>;

When combining these modules for distribution (I typically use Rollup with Babel), we can simply rely on established JavaScript tooling for minification and other optimizations.

One of the few drawbacks I’ve identified so far is that customizing the template at runtime is tricky, so if you need to adjust the markup, you probably wanna go directly to the source and build your own version (e.g. by subclassing).

I hope the description and rationale here make some sense, perhaps it even tickled some pathways to result in a more refined approach? Let me know in the comments.

  1. e.g. Handlebars' precompiled templates still require a sizable runtime library while Pug uses string concatenation rather than DOM operations  ↩