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: