Skip to content
Merged
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
62 changes: 32 additions & 30 deletions docs/_guide/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ Remember! Actions are _automatically_ bound using the `@controller` decorator. T
<div class="">

```html
<hello-controller>
<hello-world>
<input
data-target="hello-controller.name"
data-target="hello-world.name"
type="text"
>

<button
data-action="click:hello-controller#greet">
data-action="click:hello-world#greet">
Greet
</button>

<span
data-target="hello-controller.output">
data-target="hello-world.output">
</span>
</div>
</hello-world>
```

</div>
Expand All @@ -51,7 +51,7 @@ Remember! Actions are _automatically_ bound using the `@controller` decorator. T
import { controller, target } from "@github/catalyst"

@controller
class HelloController extends HTMLElement {
class HelloWorldElement extends HTMLElement {
@target nameTarget: HTMLElement
@target outputTarget: HTMLElement

Expand All @@ -78,59 +78,61 @@ The actions syntax follows a pattern of `event:controller#method`.
Multiple actions can be bound to multiple events, methods, and controllers. For example:

```html
<analytics-controller>
<hello-controller>
<analytics-tracking>
<hello-world>
<input
data-target="hello-controller.name"
data-target="hello-world.name"
data-action="
input:hello-controller#validate
blur:hello-controller#validate
focus:analytics-controller#hover
input:hello-world#validate
blur:hello-world#validate
focus:analytics-tracking#focus
"
type="text"
>

<button
data-action="
click:hello-controller#greet
click:analytics-controller#click
hover:analytics-controller#hover
click:hello-world#greet
click:analytics-tracking#click
hover:analytics-tracking#hover
"
>
Greet
</button>
</hello-controller>
</analytics-controller>
</hello-world>
</analytics-tracking>
```

### Custom Events

A Controller may emit custom events, which may be listened to by other Controllers using the same Actions Syntax. There is no extra syntax needed for this. For example a `lazy-controller` may dispatch a `loaded` event, once its contents are loaded, and other controllers can listen to this event:
A Controller may emit custom events, which may be listened to by other Controllers using the same Actions Syntax. There is no extra syntax needed for this. For example a `lazy-loader` Controller might dispatch a `loaded` event, once its contents are loaded, and other controllers can listen to this event:

```html
<hover-card disabled>
<lazy-controller data-url="/user/1" data-action="loaded:hover-card#enable">
<lazy-loader data-url="/user/1" data-action="loaded:hover-card#enable">
<loading-spinner>
</lazy-controller>
</lazy-loader>
</hover-card>
```

### Private Methods

Actions can always be bound to any method that is available on the Controller's prototype. If you need a method on a class that _must not_ be invoked within Actions, then you can instead use a [_class field_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields) or a [_private class field_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields#Private_fields).

```js
import {controller} from '@github/catalyst'

@controller
class HelloController extends HTMLElement {
class LazyLoader extends HTMLElement {

hidden = () => {
console.log('data-action cannot call this hidden method, but other JavaScript can!')
connectedCallback() {
this.innerHTML = await (await fetch(this.dataset.url)).text()
this.dispatchEvent(new CustomEvent('loaded'))
}

#reallyHidden = () => {
console.log('data-action cannot call this hidden method, neither can other JavaScript!')
}

@controller
class HoverCard extenda HTMLElement {

enable() {
this.disabled = false
}

}
Expand All @@ -145,7 +147,7 @@ If you're not using decorators, then you'll need to call `bind(this)` somewhere
```
import {bind} from '@github/catalyst'

class HelloController extends HTMLElement {
class HelloWorldElement extends HTMLElement {

connectedCallback() {
bind(this)
Expand Down
4 changes: 4 additions & 0 deletions docs/_guide/anti-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
chapter: 8
subtitle: Anti Patterns
---
16 changes: 9 additions & 7 deletions docs/_guide/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ Catalyst comes with the `@controller` decorator. This gets put on top of the cla

```js
@controller
class MyController extends HTMLElement {}
class HelloWorldElement extends HTMLElement {}
```

### Class Field Decorators

Catalyst comes with the `@target` and `@targets` decorators for more [read about Targets](/guide/targets). These get added on top or to the left of the field name, like so:
Catalyst comes with the `@target` and `@targets` decorators (for more on these [read the Targets guide section](/guide/targets)). These get added on top or to the left of the field name, like so:

```js
class MyController extends HTMLElement {
class HelloWorldElement extends HTMLElement {

@target something

Expand All @@ -43,7 +43,7 @@ Catalyst doesn't currently ship with any method decorators, but you might see th


```js
class MyController extends HTMLElement {
class HelloWorldElement extends HTMLElement {

@log
submit() {
Expand All @@ -59,12 +59,14 @@ class MyController extends HTMLElement {
}
```

### Function Call Decorators
### Function Calling Decorators

Some decorators are customisable - they get called with additional arguments, just like a function call. An example of this is the `@debounce` decorator in the [`@github/mini-throttle`](https://github.com/github/mini-throttle) package:
You might see some decorators that look like function calls, and that's because they are! Some decorators allow for customisation; calling with additional arguments. Decorators that expect to be called are generally not interchangeable with the non-call variant, a decorators documentation should tell you how to use it.

Catalyst doesn't ship with any decorators that can be called like a function; but an example of one can be found in the `@debounce` decorator in the [`@github/mini-throttle`](https://github.com/github/mini-throttle) package:

```js
class MyController extends HTMLElement {
class HelloWorldElement extends HTMLElement {

@debounce(100)
handleInput() {
Expand Down
73 changes: 73 additions & 0 deletions docs/_guide/patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
chapter: 7
subtitle: Patterns
---

An aim of Catalyst is to be as light weight as possible, and so we often avoid including helper functions for otherwise fine code. We also want to keep Catalyst focussed, and so where some helper functions might be reasonable, we recommend judicious use of other small libraries.

Here are a few common patterns which we've avoided introducing into the Catalyst code base, and instead encourage you to take the example code and run with that:


### Debouncing or Throttling events

Often times you'll want to do something computationally intensive (or network intensive) based on a user event. It's worth throttling the amount of times a function can be called for these events, to prevent saturation of the CPU or network. For this we can use the "debounce" or "throttle" patterns. We recommend using the [`@github/mini-throttle`](https://github.com/github/mini-throttle) library for this, which provides convenient decorators to put on methods which will add throttling to them:

```typescript
import {controller} from '@github/catalyst'
import {debounce} from '@github/mini-throttle/decorators'

@controller
class FuzzySearchElement extends HTMLElement {

// Adding `@debounce(100)` here means this method will only be called once in a 100ms period.
@debounce(100)
search(event: Event) {
const value = event.currentTarget.value
// This function is very computationally intensive, so we should run it as little as possible
this.filterAllItemsWithValue(value)
}

}
```

### Aborting Network Requests

When making network requests using `fetch`, based on user input, you can cancel old requests as new ones come in, this is useful for performance as well as UI responsiveness, as old requests that aren't cancelled might complete later than newer ones causing the UI to jump around. Aborting network requests requires you to use [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) (a web platform feature).

```typescript
@controller
class RemoveSearchElement extends HTMLElement {

#remoteSearchController: AbortController|null

async search(event: Event) {
// Abort the old Request
this.#remoteSearchController?.abort()

// To start making a new request, construct an AbortController
const {signal} = (this.#remoteSearchController = new AbortController())

try {
const res = await fetch(myUrl, {signal})

// ... Add logic here with the completed network response
} catch (e) {

// ... Add logic here if you need to report a failed network request.
// Do not rethrow for network errors!

}

if (signal.aborted) {
// Here you can add logic for if the request was cancelled, but
// usually what you want to do is just return early to avoid
// cleaning up the loading UI (bear in mind if the request is
// cancelled then another one will be in its place).
return
}

// ... Add cleanup logic here, such as removing `loading` classes.

}
}
```
64 changes: 37 additions & 27 deletions docs/_guide/targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ chapter: 5
subtitle: Querying Descendants
---

One of the three [core patterns](/guide/introduction#three-core-concepts-observe-listen-query) is Querying. In Catalyst, Targets are the preferred way to query. Target use `querySelectorAll` under the hood, but make it a lot simpler to work with.
One of the three [core patterns](/guide/introduction#three-core-concepts-observe-listen-query) is Querying. In Catalyst, Targets are the preferred way to query. Targets use `querySelectorAll` under the hood, but in a way that makes it a lot simpler to work with.

Catalyst Components are really just Web Components, so you could simply use `querySelector` or `querySelectorAll` to select descendants of the element. Targets avoid some of the problems of `querySelector`; they provide a more consistent interface and handle nesting intuitively. Targets are also a little more ergonomic to reuse in a class. We'd recommend using Targets over `querySelector` wherever you can.
Catalyst Components are really just Web Components, so you could simply use `querySelector` or `querySelectorAll` to select descendants of the element. Targets avoid some of the problems of `querySelector`; they provide a more consistent interface, avoid coupling CSS classes or HTML tag names to JS, and they handle subtle issues like nested components. Targets are also a little more ergonomic to reuse in a class. We'd recommend using Targets over `querySelector` wherever you can.

To create a Target, use the `@target` decorator on a class field, and add the matching `data-target` attribute to your HTML, like so:

Expand All @@ -15,11 +15,11 @@ To create a Target, use the `@target` decorator on a class field, and add the ma
<div>

```html
<hello-controller>
<hello-world>
<span
data-target="hello-controller.output">
data-target="hello-world.output">
</span>
</div>
</hello-world>
```

</div>
Expand All @@ -29,7 +29,7 @@ To create a Target, use the `@target` decorator on a class field, and add the ma
import { controller, target } from "@github/catalyst"

@controller
class HelloController extends HTMLElement {
class HelloWorldElement extends HTMLElement {
@target outputTarget!: HTMLElement

greet() {
Expand Down Expand Up @@ -66,38 +66,48 @@ Remember! There are two decorators available, `@target` which fetches only one e
</div>
</div>

The `@target` decorator will only ever return _one_ element, just like `querySelector`. If you want to get multiple Targets, you need the `@targets` decorator which works almost identically, but it'll return an _array_ of _N_ elements.
The `@target` decorator will only ever return _one_ element, just like `querySelector`. If you want to get multiple Targets, you need the `@targets` decorator which works almost identically, but it'll return an _array_ of elements. To put this into types: `@target` returns `Element|undefined` while `@targets` returns `Array<Element>`

Elements can be referenced as multiple targets, and targets may be referenced multiple times within the HTML:

```html
<teammembers-controller>
<userlist-controller>
<user-controller data-target="userlist-controller.user">
<input type="checkbox" data-target="teammembers-controller.readCheckbox">
<input type="checkbox" data-target="teammembers-controller.writeCheckbox">
</user-controller>
<user-controller data-target="userlist-controller.user">
<input type="checkbox" data-target="teammembers-controller.readCheckbox">
<input type="checkbox" data-target="teammembers-controller.writeCheckbox">
</user-controller>
</userlist-controller>
</teammembers-controller>
<team-members>
<user-list>
<user-settings data-target="user-list.user">
<input type="checkbox" data-target="team-members.read user-settings.read">
<input type="checkbox" data-target="team-members.write user-settings.write">
</user-settings>
<user-settings data-target="user-list.user">
<input type="checkbox" data-target="team-members.read user-settings.read">
<input type="checkbox" data-target="team-members.write user-settings.write">
</user-settings>
</user-list>
</team-members>
```

<br>

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

@controller
class HelloController extends HTMLElement {
@targets readCheckbox!: HTMLElement
@targets writeCheckbox!: HTMLElement
class UserSettingsElement extends HTMLElement {
@target read!: HTMLInputElement
@target write!: HTMLInputElement

validate() {
valid() {
// One checkbox must be checked!
return this.readCheckbox.length > 0 && this.writeCheckbox.length > 0
return this.read.checked || this.write.checked
}
}

@controller
class UserListElement extends HTMLElement {
@targets user!: HTMLElement

valid() {
// Every user must be valid!
return this.user.every(user => user.valid())
}
}
```
Expand All @@ -106,11 +116,11 @@ class HelloController extends HTMLElement {

If you're using decorators, then the `@target` and `@targets` decorators will turn the decorated properties into getters.

If you're not using decorators, then you'll need to call `findTarget(this, key)` or `findTargets(this, key)` in the getter, for example:
If you're not using decorators, then you'll need to make a `getter`, and call `findTarget(this, key)` or `findTargets(this, key)` in the getter, for example:

```js
import {findTarget, findTargets} from '@github/catalyst'
class MyController extends HTMLElement {
class HelloWorldElement extends HTMLElement {

get outputTarget() {
return findTarget(this, 'outputTarget')
Expand Down
Loading