Skip to content

Commit dda4d5e

Browse files
Copilotdgreif
andcommitted
Implement optional element name parameter for @controller decorator
Co-authored-by: dgreif <3026298+dgreif@users.noreply.github.com>
1 parent 93d47c3 commit dda4d5e

File tree

5 files changed

+65
-10
lines changed

5 files changed

+65
-10
lines changed

docs/_guide/your-first-component.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@ Catalyst will automatically convert the classes name; removing the trailing `Ele
3131

3232
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.
3333

34+
### Custom Element Names
35+
36+
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:
37+
38+
```js
39+
import {controller} from '@github/catalyst'
40+
41+
@controller('happy-widget')
42+
class SomeClass extends HTMLElement {
43+
connectedCallback() {
44+
this.innerHTML = 'Hello from happy-widget!'
45+
}
46+
}
47+
```
48+
<br>
49+
50+
This will register the element as `<happy-widget>` regardless of the class name. This is particularly useful when:
51+
- Your production build minifies class names
52+
- You want explicit control over the element name
53+
- The class name doesn't follow the naming pattern required for automatic naming
54+
3455
{% capture callout %}
3556
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`
3657
{% endcapture %}{% include callout.md %}
@@ -40,8 +61,8 @@ Remember! A class name _must_ include at least two CamelCased words (not includi
4061

4162
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:
4263

43-
- Derives a tag name based on your class name, removing the trailing `Element` suffix and lowercasing all capital letters, separating them with a dash.
44-
- Calls `window.customElements.define` with the newly derived tag name and your class.
64+
- 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')`).
65+
- Calls `window.customElements.define` with the newly derived (or provided) tag name and your class.
4566
- Calls `defineObservedAttributes` with the class to add map any `@attr` decorators. See [attrs]({{ site.baseurl }}/guide/attrs) for more on this.
4667
- Injects the following code inside of the `connectedCallback()` function of your class:
4768
- `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(
79100
}
80101
)
81102
```
103+
104+
Or with a custom element name:
105+
106+
```js
107+
import {controller} from '@github/catalyst'
108+
109+
controller('my-custom-name')(
110+
class HelloWorldElement extends HTMLElement {
111+
//...
112+
}
113+
)
114+
```
82115
<br>

src/controller.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ import type {CustomElementClass} from './custom-element.js'
66
* registry, as well as ensuring `bind(this)` is called on `connectedCallback`,
77
* wrapping the classes `connectedCallback` method if needed.
88
*/
9-
export function controller(classObject: CustomElementClass): void {
10-
new CatalystDelegate(classObject)
9+
export function controller(classObject: CustomElementClass): void
10+
export function controller(name: string): (classObject: CustomElementClass) => void
11+
export function controller(
12+
classObjectOrName: CustomElementClass | string
13+
): void | ((classObject: CustomElementClass) => void) {
14+
if (typeof classObjectOrName === 'string') {
15+
return (classObject: CustomElementClass) => {
16+
new CatalystDelegate(classObject, classObjectOrName)
17+
}
18+
}
19+
new CatalystDelegate(classObjectOrName)
1120
}

src/core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {observe} from './lazy-define.js'
88
const symbol = Symbol.for('catalyst')
99

1010
export class CatalystDelegate {
11-
constructor(classObject: CustomElementClass) {
11+
constructor(classObject: CustomElementClass, elementName?: string) {
1212
// eslint-disable-next-line @typescript-eslint/no-this-alias
1313
const delegate = this
1414

@@ -44,7 +44,7 @@ export class CatalystDelegate {
4444
})
4545

4646
defineObservedAttributes(classObject)
47-
register(classObject)
47+
register(classObject, elementName)
4848
}
4949

5050
observedAttributes(instance: HTMLElement, observedAttributes: string[]) {

src/register.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import {dasherize} from './dasherize.js'
88
*
99
* Example: HelloController => hello-controller
1010
*/
11-
export function register(classObject: CustomElementClass): CustomElementClass {
12-
const name = dasherize(classObject.name).replace(/-element$/, '')
11+
export function register(classObject: CustomElementClass, name?: string): CustomElementClass {
12+
const tagName = name || dasherize(classObject.name).replace(/-element$/, '')
1313

1414
try {
15-
window.customElements.define(name, classObject)
15+
window.customElements.define(tagName, classObject)
1616
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1717
// @ts-ignore
18-
window[classObject.name] = customElements.get(name)
18+
window[classObject.name] = customElements.get(tagName)
1919
} catch (e: unknown) {
2020
// The only reason for window.customElements.define to throw a `NotSupportedError`
2121
// is if the element has already been defined.

test/controller.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ describe('controller', () => {
1414
expect(instance).to.be.instanceof(ControllerRegisterElement)
1515
})
1616

17+
it('registers element with custom name when provided', async () => {
18+
@controller('happy-widget')
19+
class SomeClass extends HTMLElement {}
20+
instance = await fixture(html`<happy-widget />`)
21+
expect(instance).to.be.instanceof(SomeClass)
22+
})
23+
24+
it('registers element with custom name using function syntax', async () => {
25+
controller('custom-element-name')(class AnotherClass extends HTMLElement {})
26+
instance = await fixture(html`<custom-element-name />`)
27+
expect(instance).to.exist
28+
})
29+
1730
it('adds data-catalyst to elements', async () => {
1831
@controller
1932
// eslint-disable-next-line @typescript-eslint/no-unused-vars

0 commit comments

Comments
 (0)