From 9b9ea5170c6801b99e687918b8d55bf8d68d38f3 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 19 Jun 2020 15:11:43 +0100 Subject: [PATCH 1/5] feat: drop `Element` suffix from controller classes --- src/register.ts | 5 ++++- test/register.js | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/register.ts b/src/register.ts index 6f9deaf8..e1b51625 100644 --- a/src/register.ts +++ b/src/register.ts @@ -10,7 +10,10 @@ interface CustomElement { * Example: HelloController => hello-controller */ export function register(classObject: CustomElement): void { - const name = classObject.name.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase() + const name = classObject.name + .replace(/([a-zA-Z])(?=[A-Z])/g, '$1-') + .replace(/-Element$/, '') + .toLowerCase() if (!window.customElements.get(name)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/test/register.js b/test/register.js index 59786860..b18ed0ae 100644 --- a/test/register.js +++ b/test/register.js @@ -27,4 +27,10 @@ describe('register', () => { ThisIsAnExampleOfADasherisedClassName ) }) + + it('automatically drops the `Element` suffix', () => { + class ASuffixedElement {} + register(ASuffixedElement) + expect(window.customElements.get('a-suffixed')).to.equal(ASuffixedElement) + }) }) From 0e95fff6894ee9329c0424647d6b92ccc58f1ba5 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 19 Jun 2020 15:13:09 +0100 Subject: [PATCH 2/5] docs: clarify `Element` suffix dropping in docs --- docs/_guide/your-first-component.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_guide/your-first-component.md b/docs/_guide/your-first-component.md index 05ee88f0..7a313843 100644 --- a/docs/_guide/your-first-component.md +++ b/docs/_guide/your-first-component.md @@ -41,9 +41,9 @@ class MyController extends HTMLElement { ```
-Catalyst will automatically "dasherize" the class name. All capital letters get lowercased and dash separated. +Catalyst will automatically convert the classes name; removing the trailing `Element` suffix and lowercasing all capital letters, separating them with a dash. -By convention, Catalyst controllers end in `Controller`, but it's not required. +By convention, Catalyst controllers end in `Element`, and Catalyst will strip this for the tag name, but suffixing `Element` is not required. All examples in this guide use `Element` suffixed names. #### What about without Decorators? From 4729b9e614edd0975f86d4c035ba397535ae26d2 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 19 Jun 2020 15:12:58 +0100 Subject: [PATCH 3/5] docs: rewrite examples to use `Element` suffix --- docs/_guide/actions.md | 44 ++++++++++++++--------------- docs/_guide/decorators.md | 8 +++--- docs/_guide/targets.md | 36 +++++++++++------------ docs/_guide/your-first-component.md | 12 ++++---- docs/index.html | 12 ++++---- 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/docs/_guide/actions.md b/docs/_guide/actions.md index 86e4b734..0c11a2bf 100644 --- a/docs/_guide/actions.md +++ b/docs/_guide/actions.md @@ -27,21 +27,21 @@ Remember! Actions are _automatically_ bound using the `@controller` decorator. T
```html - + + data-target="hello-world.output"> -
+ ``` @@ -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 @@ -78,40 +78,40 @@ The actions syntax follows a pattern of `event:controller#method`. Multiple actions can be bound to multiple events, methods, and controllers. For example: ```html - - + + - - + + ``` ### 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-load` Controller may dispatch a `loaded` event, once its contents are loaded, and other controllers can listen to this event: ```html - + - + ``` @@ -123,7 +123,7 @@ Actions can always be bound to any method that is available on the Controller's import {controller} from '@github/catalyst' @controller -class HelloController extends HTMLElement { +class HelloWorldElement extends HTMLElement { hidden = () => { console.log('data-action cannot call this hidden method, but other JavaScript can!') @@ -145,7 +145,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) diff --git a/docs/_guide/decorators.md b/docs/_guide/decorators.md index fd55649a..39ef28ab 100644 --- a/docs/_guide/decorators.md +++ b/docs/_guide/decorators.md @@ -15,7 +15,7 @@ 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 @@ -23,7 +23,7 @@ class MyController extends HTMLElement {} 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: ```js -class MyController extends HTMLElement { +class HelloWorldElement extends HTMLElement { @target something @@ -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() { @@ -64,7 +64,7 @@ class MyController extends HTMLElement { 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: ```js -class MyController extends HTMLElement { +class HelloWorldElement extends HTMLElement { @debounce(100) handleInput() { diff --git a/docs/_guide/targets.md b/docs/_guide/targets.md index 2e4cff74..6336d5c6 100644 --- a/docs/_guide/targets.md +++ b/docs/_guide/targets.md @@ -15,11 +15,11 @@ To create a Target, use the `@target` decorator on a class field, and add the ma
```html - + + data-target="hello-world.output"> -
+ ``` @@ -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() { @@ -71,18 +71,18 @@ The `@target` decorator will only ever return _one_ element, just like `querySel Elements can be referenced as multiple targets, and targets may be referenced multiple times within the HTML: ```html - - - - - - - - - - - - + + + + + + + + + + + + ```
@@ -91,7 +91,7 @@ Elements can be referenced as multiple targets, and targets may be referenced mu import { controller, targets } from "@github/catalyst" @controller -class HelloController extends HTMLElement { +class HelloWorldElement extends HTMLElement { @targets readCheckbox!: HTMLElement @targets writeCheckbox!: HTMLElement @@ -110,7 +110,7 @@ If you're not using decorators, then you'll need to call `findTarget(this, key)` ```js import {findTarget, findTargets} from '@github/catalyst' -class MyController extends HTMLElement { +class HelloWorldElement extends HTMLElement { get outputTarget() { return findTarget(this, 'outputTarget') diff --git a/docs/_guide/your-first-component.md b/docs/_guide/your-first-component.md index 7a313843..e9ed382a 100644 --- a/docs/_guide/your-first-component.md +++ b/docs/_guide/your-first-component.md @@ -6,14 +6,14 @@ chapter: 2 Custom Elements allow you to create reusable components that you can declare in HTML, and [progressively enhance](https://en.wikipedia.org/wiki/Progressive_enhancement) within JavaScript. Custom Elements must named with a `-` in the HTML name, and the JS class must `extend HTMLElement`. When the browser connects each element class instance to the DOM node, `connectedCallback` is fired - this is where you can change parts of the element. Here's a basic example: ```html - + ```
@@ -31,13 +31,13 @@ Catalyst saves you writing some of this boilerplate, by automatically calling th ```js @controller -class MyController extends HTMLElement { +class HelloWorldElement extends HTMLElement { connectedCallback() { this.innerHTML = 'Hello World!' } } // No longer need this: -// window.customElements.register('my-controller', MyController) +// window.customElements.register('hello-world', HelloWorldElement) ```
@@ -51,7 +51,7 @@ If you don't want to use decorators, you can simply wrap the class in a call to ```js controller( - class MyController extends HTMLElement { + class HelloWorldElement extends HTMLElement { //... } ) diff --git a/docs/index.html b/docs/index.html index 25d835de..e0a4960b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -28,16 +28,16 @@

Catalyse your Web Components

{% highlight html %} - - + + - - + - + {% endhighlight %}
@@ -45,7 +45,7 @@

Catalyse your Web Components

import { controller, target } from "@github/catalyst" @controller -class HelloController extends HTMLElement { +class HelloWorldElement extends HTMLElement { @target name!: HTMLElement @target output!: HTMLElement From ed98822eca821d08e7fec77475a3b5cbe2a32f6a Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 17 Jun 2020 17:45:26 +0100 Subject: [PATCH 4/5] docs: Guide proofread and various tweaks --- docs/_guide/actions.md | 28 +++++++++++++++------------- docs/_guide/decorators.md | 8 +++++--- docs/_guide/targets.md | 38 ++++++++++++++++++++++++-------------- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/docs/_guide/actions.md b/docs/_guide/actions.md index 0c11a2bf..55041cf2 100644 --- a/docs/_guide/actions.md +++ b/docs/_guide/actions.md @@ -85,7 +85,7 @@ Multiple actions can be bound to multiple events, methods, and controllers. For data-action=" input:hello-world#validate blur:hello-world#validate - focus:analytics-tracking#hover + focus:analytics-tracking#focus " type="text" > @@ -105,32 +105,34 @@ Multiple actions can be bound to multiple events, methods, and controllers. For ### 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-load` 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 - + - + ``` -### 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 HelloWorldElement 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 } } diff --git a/docs/_guide/decorators.md b/docs/_guide/decorators.md index 39ef28ab..a1f4a697 100644 --- a/docs/_guide/decorators.md +++ b/docs/_guide/decorators.md @@ -20,7 +20,7 @@ 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 HelloWorldElement extends HTMLElement { @@ -59,9 +59,11 @@ class HelloWorldElement 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 HelloWorldElement extends HTMLElement { diff --git a/docs/_guide/targets.md b/docs/_guide/targets.md index 6336d5c6..e74a6a2e 100644 --- a/docs/_guide/targets.md +++ b/docs/_guide/targets.md @@ -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: @@ -66,7 +66,7 @@ Remember! There are two decorators available, `@target` which fetches only one e
-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` Elements can be referenced as multiple targets, and targets may be referenced multiple times within the HTML: @@ -74,12 +74,12 @@ Elements can be referenced as multiple targets, and targets may be referenced mu - - + + - - + + @@ -88,16 +88,26 @@ Elements can be referenced as multiple targets, and targets may be referenced mu
```js -import { controller, targets } from "@github/catalyst" +import { controller, target, targets } from "@github/catalyst" @controller -class HelloWorldElement 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()) } } ``` @@ -106,7 +116,7 @@ class HelloWorldElement 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' From 39a6ac00aab8bb7408e81bf29eb0921b18e220ad Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 19 Jun 2020 14:24:18 +0100 Subject: [PATCH 5/5] docs: patterns & anti patterns --- docs/_guide/anti-patterns.md | 4 ++ docs/_guide/patterns.md | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 docs/_guide/anti-patterns.md create mode 100644 docs/_guide/patterns.md diff --git a/docs/_guide/anti-patterns.md b/docs/_guide/anti-patterns.md new file mode 100644 index 00000000..35a813a5 --- /dev/null +++ b/docs/_guide/anti-patterns.md @@ -0,0 +1,4 @@ +--- +chapter: 8 +subtitle: Anti Patterns +--- diff --git a/docs/_guide/patterns.md b/docs/_guide/patterns.md new file mode 100644 index 00000000..8fab8252 --- /dev/null +++ b/docs/_guide/patterns.md @@ -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. + + } +} +```