diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 34383bb7..8f75308a 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,7 +1,7 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/javascript-node/.devcontainer/base.Dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster
-ARG VARIANT="16"
+ARG VARIANT="24"
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 7f65a505..09f00ea2 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -7,7 +7,7 @@
// Update 'VARIANT' to pick a Node version: 16, 14, 12.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local arm64/Apple Silicon.
- "args": { "VARIANT": "16" }
+ "args": { "VARIANT": "24" }
},
// Set *default* container specific settings.json values on container create.
diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml
index 7cd07238..6c03a934 100644
--- a/.github/workflows/lighthouse.yml
+++ b/.github/workflows/lighthouse.yml
@@ -10,10 +10,10 @@ jobs:
- name: Checkout the project
uses: actions/checkout@v3
- - name: Use Node.js 16.x (LTS)
+ - name: Use Node.js 24.x (LTS)
uses: actions/setup-node@v3
with:
- node-version: 16.x
+ node-version: 24.x
cache: 'npm'
- run: npm ci
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 6d84019f..3e112e41 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -11,10 +11,10 @@ jobs:
steps:
- name: Checkout the project
uses: actions/checkout@v3
- - name: Use Node.js 16.x (LTS)
+ - name: Use Node.js 24.x (LTS)
uses: actions/setup-node@v3
with:
- node-version: 16.x
+ node-version: 24.x
cache: 'npm'
- run: npm ci
- name: Lint Codebase
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 264bdd37..14143971 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,10 +10,10 @@ jobs:
steps:
- name: Checkout the project
uses: actions/checkout@v3
- - name: Use Node.js 16.x (LTS)
+ - name: Use Node.js 24.x (LTS)
uses: actions/setup-node@v3
with:
- node-version: 16.x
+ node-version: 24.x
registry-url: https://registry.npmjs.org/
cache: npm
- run: npm ci
diff --git a/docs/_guide/your-first-component.md b/docs/_guide/your-first-component.md
index 152f710f..dde4e394 100644
--- a/docs/_guide/your-first-component.md
+++ b/docs/_guide/your-first-component.md
@@ -31,6 +31,27 @@ Catalyst will automatically convert the classes name; removing the trailing `Ele
By convention Catalyst controllers end in `Element`; Catalyst will omit this when generating a tag name. The `Element` suffix is _not_ required - just convention. All examples in this guide use `Element` suffixed names.
+### Custom Element Names
+
+If you need to use a specific element name that doesn't match your class name (for example, to support minification), you can pass the element name directly to the `@controller` decorator:
+
+```js
+import {controller} from '@github/catalyst'
+
+@controller('hello-widget')
+class SomeClass extends HTMLElement {
+ connectedCallback() {
+ this.innerHTML = 'Hello from hello-widget!'
+ }
+}
+```
+
+
+This will register the element as `` regardless of the class name. This is particularly useful when:
+- Your production build minifies class names
+- You want explicit control over the element name
+- The class name doesn't follow the naming pattern required for automatic naming
+
{% capture callout %}
Remember! A class name _must_ include at least two CamelCased words (not including the `Element` suffix). One-word elements will raise exceptions. Example of good names: `UserListElement`, `SubTaskElement`, `PagerContainerElement`
{% endcapture %}{% include callout.md %}
@@ -40,8 +61,8 @@ Remember! A class name _must_ include at least two CamelCased words (not includi
The `@controller` decorator ties together the various other decorators within Catalyst, plus a few extra conveniences such as automatically registering the element, which saves you writing some boilerplate that you'd otherwise have to write by hand. Specifically the `@controller` decorator:
- - Derives a tag name based on your class name, removing the trailing `Element` suffix and lowercasing all capital letters, separating them with a dash.
- - Calls `window.customElements.define` with the newly derived tag name and your class.
+ - Derives a tag name based on your class name, removing the trailing `Element` suffix and lowercasing all capital letters, separating them with a dash. You can optionally provide a custom element name as a parameter (e.g., `@controller('my-element')`).
+ - Calls `window.customElements.define` with the newly derived (or provided) tag name and your class.
- Calls `defineObservedAttributes` with the class to add map any `@attr` decorators. See [attrs]({{ site.baseurl }}/guide/attrs) for more on this.
- Injects the following code inside of the `connectedCallback()` function of your class:
- `bind(this)`; ensures that as your element connects it picks up any `data-action` handlers. See [actions]({{ site.baseurl }}/guide/actions) for more on this.
@@ -79,4 +100,16 @@ controller(
}
)
```
+
+Or with a custom element name:
+
+```js
+import {controller} from '@github/catalyst'
+
+controller('my-custom-name')(
+ class HelloWorldElement extends HTMLElement {
+ //...
+ }
+)
+```
diff --git a/src/controller.ts b/src/controller.ts
index 13365c8a..0b4b74d6 100644
--- a/src/controller.ts
+++ b/src/controller.ts
@@ -6,6 +6,15 @@ import type {CustomElementClass} from './custom-element.js'
* registry, as well as ensuring `bind(this)` is called on `connectedCallback`,
* wrapping the classes `connectedCallback` method if needed.
*/
-export function controller(classObject: CustomElementClass): void {
- new CatalystDelegate(classObject)
+export function controller(classObject: CustomElementClass): void
+export function controller(name: string): (classObject: CustomElementClass) => void
+export function controller(
+ classObjectOrName: CustomElementClass | string
+): void | ((classObject: CustomElementClass) => void) {
+ if (typeof classObjectOrName === 'string') {
+ return (classObject: CustomElementClass) => {
+ new CatalystDelegate(classObject, classObjectOrName)
+ }
+ }
+ new CatalystDelegate(classObjectOrName)
}
diff --git a/src/core.ts b/src/core.ts
index 43b7edfe..de10ee75 100644
--- a/src/core.ts
+++ b/src/core.ts
@@ -8,7 +8,7 @@ import {observe} from './lazy-define.js'
const symbol = Symbol.for('catalyst')
export class CatalystDelegate {
- constructor(classObject: CustomElementClass) {
+ constructor(classObject: CustomElementClass, elementName?: string) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const delegate = this
@@ -44,7 +44,7 @@ export class CatalystDelegate {
})
defineObservedAttributes(classObject)
- register(classObject)
+ register(classObject, elementName)
}
observedAttributes(instance: HTMLElement, observedAttributes: string[]) {
diff --git a/src/register.ts b/src/register.ts
index 837a2d46..6473ec5d 100644
--- a/src/register.ts
+++ b/src/register.ts
@@ -8,14 +8,14 @@ import {dasherize} from './dasherize.js'
*
* Example: HelloController => hello-controller
*/
-export function register(classObject: CustomElementClass): CustomElementClass {
- const name = dasherize(classObject.name).replace(/-element$/, '')
+export function register(classObject: CustomElementClass, name?: string): CustomElementClass {
+ const tagName = name || dasherize(classObject.name).replace(/-element$/, '')
try {
- window.customElements.define(name, classObject)
+ window.customElements.define(tagName, classObject)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- window[classObject.name] = customElements.get(name)
+ window[classObject.name] = customElements.get(tagName)
} catch (e: unknown) {
// The only reason for window.customElements.define to throw a `NotSupportedError`
// is if the element has already been defined.
diff --git a/test/controller.ts b/test/controller.ts
index ffd5e32d..872a26e0 100644
--- a/test/controller.ts
+++ b/test/controller.ts
@@ -14,6 +14,29 @@ describe('controller', () => {
expect(instance).to.be.instanceof(ControllerRegisterElement)
})
+ it('registers element with custom name when provided', async () => {
+ @controller('happy-widget')
+ class SomeClass extends HTMLElement {}
+ instance = await fixture(html``)
+ expect(instance).to.be.instanceof(SomeClass)
+ })
+
+ it('registers element with custom name using function syntax', async () => {
+ controller('custom-element-name')(class AnotherClass extends HTMLElement {})
+ instance = await fixture(html``)
+ expect(instance).to.exist
+ })
+
+ it('adds data-catalyst to elements with custom names', async () => {
+ @controller('custom-named-element')
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ class CustomNamedElement extends HTMLElement {}
+
+ instance = await fixture(html``)
+ expect(instance.hasAttribute('data-catalyst')).to.equal(true)
+ expect(instance.getAttribute('data-catalyst')).to.equal('')
+ })
+
it('adds data-catalyst to elements', async () => {
@controller
// eslint-disable-next-line @typescript-eslint/no-unused-vars
diff --git a/web-test-runner.config.js b/web-test-runner.config.js
index cf4744e9..68d0a046 100644
--- a/web-test-runner.config.js
+++ b/web-test-runner.config.js
@@ -4,6 +4,7 @@ export default {
files: ['test/*'],
nodeResolve: true,
concurrency: 1,
+ testsFinishTimeout: 30000,
plugins: [esbuildPlugin({ts: true, target: 'es2020'})],
filterBrowserLogs: log => !log.args.some(arg => typeof arg === 'string' && arg.includes('Lit is in dev mode'))
}