One of the three core patterns is Querying. In Catalyst, Targets are the preferred way to query. Targets use querySelector 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 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:
<hello-world>
<span
data-target="hello-world.output">
</span>
</hello-world>
import { controller, target } from "@github/catalyst"
@controller
class HelloWorldElement extends HTMLElement {
@target output: HTMLElement
greet() {
this.output.textContent = `Hello, world!`
}
}
The target syntax follows a pattern of controller.target.
controller must be the name of a controller ascendant to the element.. is the required delimiter between controller and target.target must be the name matching that of a @target (or @targets) annotated field within the Controller code.Remember! There are two decorators available, @target which fetches only one data-target element, and @targets which fetches multiple data-targets 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 the same, but returns an Array of elements, and it searches the data-targets attribute (not data-target).
Elements can be referenced as multiple targets, and targets may be referenced multiple times within the HTML:
<team-members>
<user-list>
<user-settings data-targets="user-list.users">
<input type="checkbox" data-target="user-settings.read">
<input type="checkbox" data-target="user-settings.write">
</user-settings>
<user-settings data-targets="user-list.users">
<input type="checkbox" data-target="user-settings.read">
<input type="checkbox" data-target="user-settings.write">
</user-settings>
</user-list>
</team-members>
import { controller, target, targets } from "@github/catalyst"
@controller
class UserSettingsElement extends HTMLElement {
@target read: HTMLInputElement
@target write: HTMLInputElement
valid() {
// At least one checkbox must be checked!
return this.read.checked || this.write.checked
}
}
@controller
class UserListElement extends HTMLElement {
@targets users: HTMLElement[]
valid() {
// Every user must be valid!
return this.users.every(user => user.valid())
}
}
To clarify the difference between @target and @targets here is a handy table:
| Decorator | Equivalent Native Method | Selector | Returns |
|---|---|---|---|
@target |
querySelector |
data-target="*" |
Element |
@targets |
querySelectorAll |
data-targets="*" |
Array<Element> |
Custom elements can create encapsulated DOM trees known as "Shadow" DOM. Catalyst targets support Shadow DOM by traversing the shadowRoot first, if present.
Important to note here is that nodes from the shadowRoot get returned first. So @targets will return an array of nodes, where shadowRoot nodes are at the start of the Array, and @target will return a ShadowRoot target if it exists, otherwise it will fall back to traversing the elements direct children.
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 make a getter, and call findTarget(this, key) or findTargets(this, key) in the getter, for example:
import {findTarget, findTargets} from '@github/catalyst'
class HelloWorldElement extends HTMLElement {
get output() {
return findTarget(this, 'output')
}
get pages() {
return findTargets(this, 'pages')
}
}