Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions docs/_guide/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ subtitle: Conventions

Catalyst strives for convention over code. Here are a few conventions we recommend when writing Catalyst code:

### Use `Element` to suffix your controller class
### Suffix your controllers consistently, for symmetry

Built in HTML elements all extend from the `HTMLElement` constructor, and are all suffixed with `Element` (for example `HTMLElement`, `SVGElement`, `HTMLInputElement` and so on). Catalyst components should be no different, they should behave as closely to the built-ins as possible.
Catalyst components can be suffixed with `Element`, `Component` or `Controller`. We think elements should behave as closely to the built-ins as possible, so we like to use `Element` (existing elements do this, for example `HTMLDivElement`, `SVGElement`). If you're using a server side comoponent framework such as [ViewComponent](https://viewcomponent.org/), it's probably better to suffix `Component` for symmetry with that framework.

```typescript
@controller
class UserListElement extends HTMLElement {}
class UserListElement extends HTMLElement {} // `<user-list />`
```

```typescript
@controller
class UserListComponent extends HTMLElement {} // `<user-list />`
```

### The best class-names are two word descriptions
Expand Down
34 changes: 28 additions & 6 deletions docs/_guide/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ Remember to _always_ make your JavaScript progressively enhanced, where possible

By leveraging the native [`ShadowDOM`](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) feature, Catalyst components can render complex sub-trees, fully encapsulated from the rest of the page.

Catalyst will automatically look for elements that match the `template[data-shadowroot]` selector, within your controller. If it finds one as a direct-child of your controller, it will use that to create a shadowRoot.
[Actions]({{ site.baseurl }}/guide/actions) and [Targets]({{ site.baseurl }}/guide/targets) all work within an elements ShadowRoot.

Catalyst Controllers will search for a direct child of `template[data-shadowroot]` and load its contents as the `shadowRoot` of the element. [Actions]({{ site.baseurl }}/guide/actions) and [Targets]({{ site.baseurl }}/guide/targets) all work within an elements ShadowRoot.
You can also leverage the [declarative shadow DOM](https://web.dev/declarative-shadow-dom/) and render a template inline to your HTML, which will automatically be attached (this may require a polyfill for browsers which are yet to support this feature).

### Example

```html
<hello-world>
<template data-shadowroot>
<template shadowroot="open">
<p>
Hello <span data-target="hello-world.nameEl">World</span>
</p>
Expand All @@ -41,12 +41,34 @@ class HelloWorldElement extends HTMLElement {
}
```

Providing the `<template data-shadowroot>` element as a direct child of the `hello-world` element tells Catalyst to render the templates contents automatically, and so all `HelloWorldElements` with this template will be rendered with the contents.

{% capture callout %}
Remember that _all_ instances of your controller _must_ add the `<template data-shadowroot>` HTML. If an instance does not have the `<template data-shadowroot>` as a direct child, then the shadow DOM won't be rendered for it!
Remember that _all_ instances of your controller _must_ add the `<template shadowroot>` HTML. If an instance does not have the `<template data-shadowroot>` as a direct child, then the shadow DOM won't be rendered for it!
{% endcapture %}{% include callout.md %}


It is also possible to attach a shadowRoot to your element during the `connectedCallback`, like so:

```typescript
import { controller, target } from "@github/catalyst"

@controller
class HelloWorldElement extends HTMLElement {
@target nameEl: HTMLElement
get name() {
return this.nameEl.textContent
}
set name(value: string) {
this.nameEl.textContent = value
}

connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML = `<p>
Hello <span data-target="hello-world.nameEl">World</span>
</p>`
}
}
```

### Updating a Template element using JS templates

Sometimes you wont have a template that is server rendered, and instead want to make a template using JS. Catalyst does not support this out of the box, but it is possible to use another library: `@github/jtml`. This library can be used to write declarative templates using JS. Let's re-work the above example using `@github/jtml`:
Expand Down
10 changes: 4 additions & 6 deletions docs/_guide/your-first-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ class HelloWorldElement extends HTMLElement {
```
<br>

Catalyst will automatically convert the classes name; removing the trailing `Element` suffix and lowercasing all capital letters, separating them with a dash.
Catalyst will automatically convert the classes name so the HTML tag will be `<hello-world>`. It removes the trailing `Element` suffix and lowercases all capital letters, separating them with a dash.

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.
Catalyst controllers can end in `Element`, `Controller`, or `Component` and Catalyst will remove this suffix when generating a tag name. Adding one of these suffixes is _not_ required - just convention. All examples in this guide use `Element` suffixed names (see our [convention note on this for more]({{ site.baseurl }}/guide/conventions#suffix-your-controllers-consistently-for-symmetry)).

{% 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`
Remember! A class name _must_ include at least two CamelCased words (not including the `Element`, `Controller` or `Component` suffix). One-word elements will raise exceptions. Example of good names: `UserListElement`, `SubTaskController`, `PagerContainerComponent`
{% endcapture %}{% include callout.md %}


Expand All @@ -43,16 +43,14 @@ The `@controller` decorator ties together the various other decorators within Ca
- 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.
- `autoShadowRoot(this)`; ensures that your element loads any `data-shadowroot` templates. See [rendering]({{ site.baseurl }}/guide/rendering) for more on this.
- `initializeAttrs(this)`; ensures that your element binds any `data-*` attributes to props. See [attrs]({{ site.baseurl }}/guide/attrs) for more on this.

You can do all of this manually; for example here's the above `HelloWorldElement`, written without the `@controller` annotation:

```js
import {bind, autoShadowRoot, initializeAttrs, defineObservedAttributes} from '@github/catalyst'
import {bind, initializeAttrs, defineObservedAttributes} from '@github/catalyst'
class HelloWorldElement extends HTMLElement {
connectedCallback() {
autoShadowRoot(this)
initializeAttrs(this)
this.innerHTML = 'Hello World!'
bind(this)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
{
"path": "lib/index.js",
"import": "{controller, attr, target, targets}",
"limit": "1.66kb"
"limit": "2kb"
}
]
}
11 changes: 0 additions & 11 deletions src/auto-shadow-root.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {register} from './register.js'
import {bind, bindShadow} from './bind.js'
import {autoShadowRoot} from './auto-shadow-root.js'
import {defineObservedAttributes, initializeAttrs} from './attr.js'
import type {CustomElementClass} from './custom-element.js'

Expand Down Expand Up @@ -53,7 +52,6 @@ export class CatalystDelegate {
connectedCallback(instance: HTMLElement, connectedCallback: () => void) {
instance.toggleAttribute('data-catalyst', true)
customElements.upgrade(instance)
autoShadowRoot(instance)
initializeAttrs(instance)
bind(instance)
connectedCallback?.call(instance)
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ export {findTarget, findTargets} from './findtarget.js'
export {target, targets} from './target.js'
export {controller} from './controller.js'
export {attr, initializeAttrs, defineObservedAttributes} from './attr.js'
export {autoShadowRoot} from './auto-shadow-root.js'
2 changes: 1 addition & 1 deletion src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {dasherize} from './dasherize.js'
* Example: HelloController => hello-controller
*/
export function register(classObject: CustomElementClass): CustomElementClass {
const name = dasherize(classObject.name).replace(/-element$/, '')
const name = dasherize(classObject.name).replace(/-(element|controller|component)$/, '')

try {
window.customElements.define(name, classObject)
Expand Down
80 changes: 0 additions & 80 deletions test/auto-shadow-root.ts

This file was deleted.

24 changes: 0 additions & 24 deletions test/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,30 +65,6 @@ describe('controller', () => {
expect(instance.foo).to.have.callCount(1)
})

it('binds auto shadowRoots', async () => {
@controller
class ControllerBindAutoShadowElement extends HTMLElement {
foo() {
return 'foo'
}
}
instance = await fixture<ControllerBindAutoShadowElement>(html`
<controller-bind-auto-shadow>
<template data-shadowroot="open">
<button data-action="click:controller-bind-auto-shadow#foo" />
</template>
</controller-bind-auto-shadow>
`)
replace(instance, 'foo', fake(instance.foo))

expect(instance.shadowRoot).to.exist
expect(instance).to.have.property('shadowRoot').not.equal(null)
expect(instance.shadowRoot!.children).to.have.lengthOf(1)
instance.shadowRoot!.querySelector('button')!.click()

expect(instance.foo).to.have.callCount(1)
})

it('upgrades child decendants when connected', async () => {
@controller
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
12 changes: 12 additions & 0 deletions test/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,16 @@ describe('register', () => {
class FirstSuffixElement extends HTMLElement {}
expect(window.customElements.get('first-suffix')).to.equal(FirstSuffixElement)
})

it('automatically drops the `Controller` suffix', () => {
@register
class SecondSuffixController extends HTMLElement {}
expect(window.customElements.get('second-suffix')).to.equal(SecondSuffixController)
})

it('automatically drops the `Component` suffix', () => {
@register
class ThirdSuffixComponent extends HTMLElement {}
expect(window.customElements.get('third-suffix')).to.equal(ThirdSuffixComponent)
})
})