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')) }