Caleb Hailey @calebhailey.com

customElements.apply()

A (naive?) approach to workaround customElements.define() "same tag name" errors.

Web components have completely changed the way I write vanilla JavaScript. It turns out classes are quite useful for encapsulation (who knew?)! Complementary approaches like HTML web components (and CSS web components) have reduced the amount of JavaScript I'm writing by about 95%. These ideas from @adactio.com and @hawkticehurst.com together with a steady stream of web component content from @gomakethings.com have revived my interest in modern web development.

But recently I started running into a little speed bump. I've been exploring HTML templating workflows that enable me to compose sections and entire pages from a collection of layout "partials" (ala server side includes). This sometimes causes multiple <script> tags to be added for a given web component, resulting in redundant calls to customElements.define() and a "cannot define multiple custom elements with the same tag name" error.

NotSupportedError: Cannot define multiple custom elements with the same tag name.

My initial reaction to this was to move all <script src> tags into the <head>, but this inevitably resulted in failing to load a component's JavaScript on certain pages (dependency management fail). Then I read @jakelazaroff.com's excellent Define a custom element blog post and it opened my eyes to the many different approaches to managing custom elements. But I also felt like my challenge might have been slightly different than the problem Jake was solving. So I tried something different that has been working really well for me.

It started with reviewing the CustomElementsRegistry reference documentation and discovering that get() and getName() methods already existed. All that was needed was an idempotent method for registering custom elements. It turns out this was trivial to implement.

1CustomElementRegistry.prototype.apply = function(tag, component) {
2    if (!(!!component.constructor && component.toString().substring(0,5) == "class")) { return }; // guard
3    if (this.get(tag)) { return }; // lookup by tag name
4    if (this.getName(component)) { return }; // lookup by component class
5    try { this.define(tag, component) } catch(err) {
6        console.debug(`component ${component.name} already registered as <${tag}>`);
7    };
8};

This is already working so well for me that it's just about the only JavaScript I keep in main.js for new projects. I'm replacing calls to customElements.define with customElements.apply in all of my web component JavaScript.

1customElements.apply("my-element", class MyElement extends HTMLElement {
2    // this is where the magic happens 🪄
3});

In doing so, I can add <script src='/js/my-component.js'> tags in any layout partial without worrying about "same tag name" errors.

Do you find this helpful? Or is this a horrible idea? 😅 Send me an email and let me know what you think: hello [at] calebhailey.com

Join the mailing list

Get this and other posts delivered to your inbox. No more than one email per week, no fewer than one email per month.