New Feb 14, 2025

?nodefine — a pattern to skip Custom Element definitions

Top Front-end Bloggers All from Zach Leatherman View ?nodefine — a pattern to skip Custom Element definitions on zachleat.com

The following code is a minimum viable custom element:

class Nimble extends HTMLElement {}

customElements.define("nim-ble", Nimble);

The define call is typically packaged up in the component code (for ease of use) and not in your application code. I typically adapt this into a static function like so:

class Nimble extends HTMLElement {
  static define(tagName) {
    customElements.define(tagName || "nim-ble", this);
  }
}

Nimble.define();

On first glance, this might not offer much benefit, but depending on the platform features I’m using I may also include a Cut-the-Mustard style feature test in there:

class Nimble extends HTMLElement {
  static define(tagName) {
		// Baseline 2020: Chrome 71 Safari 12.1 Firefox 65
		// (extended browser support on top of ESM and Custom Elements)
		if(typeof globalThis !== "undefined") {
			customElements.define(tagName || "nim-ble", this);
		}
  }
}

Nimble.define();

Automatic Definition

The biggest drawback I’m struggling with in these patterns is how to allow folks to opt-out of the customElements.define call when they import or bundle the script. Perhaps they might want to use a different tag name (you can only define a class once in the registry), or run some additional advanced configuration before definition.

Here’s how you would typically import the script code (or import "./nimble.js" works too):

<!-- <nim-ble> is defined for you -->
<script type="module" src="nimble.js"></script>

Here’s a new idea I’m experimenting with to allow opt-out of define() by adding a ?nodefine query parameter to the script URL:

<!-- <nim-ble> is *not* defined for you -->
<script type="module" src="nimble.js?nodefine"></script>
<script type="module">
Nimble.define("some-other-name");
</script>

And then in your component code you’ll check for ?nodefine before auto defining the custom element:

class Nimble extends HTMLElement {
  static define(tagName) {
    customElements.define(tagName || "nim-ble", this);
  }
}

// This line is the magic: if(!(new URL(import.meta.url)).searchParams.has("nodefine")) { Nimble.define(); }

window.Nimble = Nimble; export { Nimble };

This pattern works with import too:

<script type="module">
// <nim-ble> is *not* defined for you
import { Nimble } from "./nimble.js?nodefine";

Nimble.define("some-other-name");

// Or directly: customElements.define("nim-ble", Nimble); </script>

This would also work on any Custom Element code adopting this pattern nestled inside a larger bundle:

<script type="module" src="bundle-398720.js?nodefine"></script>

What do y’all think?

Community Addendums

Updated February 14, 2025 I somehow missed these excellent links which cover similar ground:

Scroll to top