Catalyst

Targets

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

Example

<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!`
  }
}

Target Syntax

The target syntax follows a pattern of controller.target.

  • controller must be the name of a controller ascendant to the element.
  • target must be the name matching that of a @target (or @targets) annotated field within the Controller code.

Multiple Targets

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())
  }
}

Target Vs Targets

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>

Targets and "ShadowRoots"

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.

What about without Decorators?

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')
  }

}