Repository: github/catalyst Branch: main Commit: 99f0747255db Files: 95 Total size: 493.1 KB Directory structure: gitextract_c09y8qnx/ ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .eslintrc.json ├── .github/ │ └── workflows/ │ ├── lighthouse.yml │ ├── nodejs.yml │ └── publish.yml ├── .gitignore ├── .nvmrc ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs/ │ ├── 404.html │ ├── Gemfile │ ├── _config.yml │ ├── _guide/ │ │ ├── abilities.md │ │ ├── actions-2.md │ │ ├── actions.md │ │ ├── anti-patterns-2.md │ │ ├── anti-patterns.md │ │ ├── attrs-2.md │ │ ├── attrs.md │ │ ├── conventions-2.md │ │ ├── conventions.md │ │ ├── create-ability.md │ │ ├── decorators-2.md │ │ ├── decorators.md │ │ ├── introduction-2.md │ │ ├── introduction.md │ │ ├── lazy-elements-2.md │ │ ├── lazy-elements.md │ │ ├── lifecycle-hooks-2.md │ │ ├── lifecycle-hooks.md │ │ ├── patterns-2.md │ │ ├── patterns.md │ │ ├── providable.md │ │ ├── rendering-2.md │ │ ├── rendering.md │ │ ├── targets-2.md │ │ ├── targets.md │ │ ├── testing-2.md │ │ ├── testing.md │ │ ├── you-will-need.md │ │ ├── your-first-component-2.md │ │ └── your-first-component.md │ ├── _includes/ │ │ ├── callout.md │ │ ├── reference_sidebar.html │ │ ├── sidebar.html │ │ └── type.html │ ├── _layouts/ │ │ ├── default.html │ │ └── guide.html │ ├── custom.css │ ├── github-syntax.css │ ├── index.html │ ├── index.js │ └── primer.css ├── lighthouserc.json ├── package.json ├── src/ │ ├── abilities.ts │ ├── ability.ts │ ├── attr.ts │ ├── auto-shadow-root.ts │ ├── bind.ts │ ├── controllable.ts │ ├── controller.ts │ ├── core.ts │ ├── custom-element.ts │ ├── dasherize.ts │ ├── findtarget.ts │ ├── get-property-descriptor.ts │ ├── index.ts │ ├── lazy-define.ts │ ├── mark.ts │ ├── providable.ts │ ├── register.ts │ ├── tag-observer.ts │ └── target.ts ├── test/ │ ├── ability.ts │ ├── attr.ts │ ├── auto-shadow-root.ts │ ├── bind.ts │ ├── controllable.ts │ ├── controller.ts │ ├── dasherize.ts │ ├── lazy-define.ts │ ├── mark.ts │ ├── providable.ts │ ├── register.ts │ ├── tag-observer.ts │ └── target.ts ├── tsconfig.build.json ├── tsconfig.json └── web-test-runner.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/javascript-node/.devcontainer/base.Dockerfile # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster ARG VARIANT="24" FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends bundler # [Optional] Uncomment if you want to install an additional version of node using nvm # ARG EXTRA_NODE_VERSION=10 # RUN su node -c "source/usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" # [Optional] Uncomment if you want to install more global node modules # RUN su node -c "npm install -g " ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/javascript-node { "name": "Node.js", "build": { "dockerfile": "Dockerfile", // Update 'VARIANT' to pick a Node version: 16, 14, 12. // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local arm64/Apple Silicon. "args": { "VARIANT": "24" } }, // Set *default* container specific settings.json values on container create. "settings": {}, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "dbaeumer.vscode-eslint" ], // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "npm i && cd docs && sudo bundle install", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "node", "features": { "git": "latest" } } ================================================ FILE: .eslintrc.json ================================================ { "root": true, "plugins": ["github"], "extends": ["plugin:github/recommended", "plugin:github/typescript", "plugin:github/browser"], "rules": { "import/no-unresolved": "off", "github/no-inner-html": "off", "i18n-text/no-en": "off", "import/extensions": ["error", "ignorePackages"], "@typescript-eslint/consistent-type-imports": ["error", {"prefer": "type-imports"}] }, "overrides": [ { "files": "test/*", "rules": { "@typescript-eslint/no-empty-function": "off" }, "globals": { "chai": false, "expect": false }, "env": { "mocha": true } }, { "files": "*.cjs", "rules": { "import/no-commonjs": "off" }, "env": { "node": true } } ] } ================================================ FILE: .github/workflows/lighthouse.yml ================================================ name: Lighthouse permissions: contents: read on: [pull_request] jobs: lhci: name: Lighthouse runs-on: ubuntu-latest steps: - name: Checkout the project uses: actions/checkout@v3 - name: Use Node.js 24.x (LTS) uses: actions/setup-node@v3 with: node-version: 24.x cache: 'npm' - run: npm ci - name: Use Ruby 2.7.3 uses: ruby/setup-ruby@v1 with: ruby-version: '2.7.3' bundler-cache: true working-directory: docs - name: Build docs run: npm run build:docs env: JEKYLL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run Lighthouse CI run: npx @lhci/cli@0.9.x autorun env: JEKYLL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LHCI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/nodejs.yml ================================================ name: Build permissions: contents: read on: pull_request: push: jobs: test-node: name: Test on Node.js runs-on: ubuntu-latest steps: - name: Checkout the project uses: actions/checkout@v3 - name: Use Node.js 24.x (LTS) uses: actions/setup-node@v3 with: node-version: 24.x cache: 'npm' - run: npm ci - name: Lint Codebase run: npm run lint - name: Run Node.js Tests run: npm run test - name: Check Bundle Size run: npm run size ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: release: types: [created] permissions: contents: read id-token: write # for provenance and publish access jobs: publish-npm: runs-on: ubuntu-latest steps: - name: Checkout the project uses: actions/checkout@v3 - name: Use Node.js 24.x (LTS) uses: actions/setup-node@v3 with: node-version: 24.x registry-url: https://registry.npmjs.org/ cache: npm - run: npm ci - run: npm test - run: npm version ${TAG_NAME} --git-tag-version=false env: TAG_NAME: ${{ github.event.release.tag_name }} - run: npm publish --provenance ================================================ FILE: .gitignore ================================================ node_modules _site *.tsbuildinfo lib/ .jekyll-cache .lighthouseci coverage ================================================ FILE: .nvmrc ================================================ 13.11.0 ================================================ FILE: CODEOWNERS ================================================ * @github/primer-reviewers @koddsson ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@github.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ ## Contributing [fork]: https://github.com/github/catalyst/fork [pr]: https://github.com/github/catalyst/compare [style]: https://github.com/styleguide/ruby [code-of-conduct]: CODE_OF_CONDUCT.md Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md). Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. ## Submitting a pull request 0. [Fork][fork] and clone the repository 0. Configure and install the dependencies: `npm i` 0. Make sure the tests pass on your machine: `npm t` 0. Create a new branch: `git checkout -b my-branch-name` 0. Make your change, add tests, and make sure the tests still pass 0. Push to your fork and [submit a pull request][pr] 0. Pat your self on the back and wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: - Follow the [style guide][style]. - Write tests. - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). ## Resources - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) - [GitHub Help](https://help.github.com) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 GitHub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Catalyst Catalyst is a set of patterns and techniques for developing components within a complex application. At its core, Catalyst simply provides a small library of functions to make developing [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) easier. For more see the [Catalyst Website](https://github.github.io/catalyst/) which includes a [Guide To using Catalyst](https://github.github.io/catalyst/guide/introduction). ================================================ FILE: SECURITY.md ================================================ If you discover a security issue in this repository, please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github). ================================================ FILE: docs/404.html ================================================ --- layout: default ---

404

Page not found :(

The requested page could not be found.

================================================ FILE: docs/Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' gem 'jekyll' group :jekyll_plugins do gem 'jekyll-commonmark-ghpages' gem 'jekyll-github-metadata' gem 'jekyll-gzip' end ================================================ FILE: docs/_config.yml ================================================ title: Catalyst markdown: CommonMarkGhPages commonmark: extensions: ['autolink', 'table'] permalink: pretty exclude: - Gemfile - Gemfile.lock - node_modules - vendor collections: guide: output: true defaults: - scope: type: guide values: layout: guide repository: github/catalyst plugins: - 'jekyll-github-metadata' - 'jekyll-gzip' ================================================ FILE: docs/_guide/abilities.md ================================================ --- version: 2 chapter: 4 title: Abilities subtitle: Abilities permalink: /guide-v2/abilities --- Under the hood Catalyst's controller decorator is comprised of a handful of separate "abilities". An "ability" is essentially a mixin or perhaps "higher order class". An ability takes a class and returns an extended class that adds additional behaviours. By convention all abilities exported by Catalyst are suffixed with `able` which we think is a nice way to denote that something is an ability and should be used as such. ### Using Abilities Abilities are fundementally just class decorators, and so can be used just like the `@controller` decorator. For example to add only the `actionable` decorator (which automatically binds events based on `data-action` attributes): ```typescript import {actionable} from '@github/catalyst' @actionable class HelloWorld extends HTMLElement { } ``` ### Using Marks Abilities also come with complementary field decorators which we call "marks" (we give them a distinctive name because they're a more restrictive subset of field decorators). Marks annotate fields which abilities can then extend with custom logic, both [Targets]({{ site.baseurl }}/guide/targets) and [Attrs]({{ site.baseurl }}/guide/attrs) are abilities that use marks. The `targetable` ability includes `target` & `targets` marks, and the `attrable` ability includes the `attr` mark. Marks decorate individual fields, like so: ```typescript import {targetable, target, targets} from '@github/catalyst' @targetable class HelloWorldElement extends HTMLElement { @target name @targets people } ``` Marks _can_ decorate over fields, get/set functions, or class methods - but individual marks can set their own validation logic, for example enforcing a naming pattern or disallowing application on methods. ### Built-In Abilities Catalyst ships with a set of built in abilities. The `@controller` decorator applies the following built-in abilities: - `controllable` - the base ability which other abilities require for functionality. - `targetable` - the ability to define `@target` and `@targets` properties. See [Targets]({{ site.baseurl }}/guide/targets) for more. - `actionable` - the ability to automatically bind events based on `data-action` attributes. See [Actions]({{ site.baseurl }}/guide/actions) for more. - `attrable` - the ability to define `@attr`s. See [Attrs]({{ site.baseurl }}/guide/attrs) for more. The `@controller` decorator also applies the `@register` decorator which automatically registers the element in the Custom Element registry, however this decorator isn't an "ability". The following abilities are shipped with Catalyst but require manually applying as they aren't considered critical functionality: - `providable` - the ability to define `provider` and `consumer` properties. See [Providable]({{ site.baseurl }}/guide/providable) for more. In addition to the provided abilities, Catalyst provides all of the tooling to create your own custom abilities. Take a look at the [Create Ability]({{ site.baseurl }}/guide/create-ability) documentation for more! ================================================ FILE: docs/_guide/actions-2.md ================================================ --- version: 2 chapter: 6 title: Actionable subtitle: Binding Events permalink: /guide-v2/actions --- Catalyst Components automatically bind actions upon instantiation. Automatically as part of the `connectedCallback`, a component will search for any children with the `data-action` attribute, and bind events based on the value of this attribute. Any _public method_ on a Controller can be bound to via `data-action`. {% capture callout %} Remember! Actions are _automatically_ bound using the `@controller` decorator. There's no extra JavaScript code needed. {% endcapture %}{% include callout.md %} ### Example
```html ```
```js import { controller, target } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @target name: HTMLElement @target output: HTMLElement greetSomeone() { this.output.textContent = `Hello, ${this.name.value}!` } } ```
### Actions Syntax The actions syntax follows a pattern of `event:controller#method`. - `event` must be the name of a [_DOM Event_](https://developer.mozilla.org/en-US/docs/Web/Events), e.g. `click`. - `controller` must be the name of a controller ascendant to the element. - `method` (optional) must be a _public_ _method_ attached to a controller's prototype. Static methods will not work. If method is not supplied, it will default to `handleEvent`. Some examples of Actions Syntax: - `click:my-element#foo` -> `click` events will call `foo` on `my-element` elements. - `submit:my-element#foo` -> `submit` events will call `foo` on `my-element` elements. - `click:user-list` -> `click` events will call `handleEvent` on `user-list` elements. - `click:user-list#` -> `click` events will call `handleEvent` on `user-list` elements. - `click:top-header-user-profile#` -> `click` events will call `handleEvent` on `top-header-user-profile` elements. - `nav:keydown:user-list` -> `navigation:keydown` events will call `handleEvent` on `user-list` elements. ### Multiple Actions 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-loader` Controller might dispatch a `loaded` event, once its contents are loaded, and other controllers can listen to this event: ```html ``` ```js import {controller} from '@github/catalyst' @controller class LazyLoader extends HTMLElement { connectedCallback() { this.innerHTML = await (await fetch(this.dataset.url)).text() this.dispatchEvent(new CustomEvent('loaded')) } } @controller class HoverCard extends HTMLElement { enable() { this.disabled = false } } ``` ### Targets and "ShadowRoots" Custom elements can create encapsulated DOM trees known as "Shadow" DOM. Catalyst actions support Shadow DOM by traversing the `shadowRoot`, if present, and also automatically watching shadowRoots for changes; auto-binding new elements as they are added. ### What about without Decorators? If you're using decorators, then the `@controller` decorator automatically handles binding of actions to a Controller. If you're not using decorators, then you'll need to call `bind(this)` somewhere inside of `connectedCallback()`. ```js import {bind} from '@github/catalyst' class HelloWorldElement extends HTMLElement { connectedCallback() { bind(this) } } ``` ### Binding dynamically added actions Catalyst automatically listens for elements that are dynamically injected into the DOM, and will bind any element's `data-action` attributes. It does this by calling `listenForBind(controller.ownerDocument)`. If for some reason you need to observe other documents (such as mutations within an iframe), then you can call the `listenForBind` manually, passing a `Node` to listen to DOM mutations on. ```js import {listenForBind} from '@github/catalyst' @controller class HelloWorldElement extends HTMLElement { @target iframe: HTMLIFrameElement connectedCallback() { // listenForBind(this.ownerDocument) is automatically called. listenForBind(this.iframe.document.body) } } ``` ================================================ FILE: docs/_guide/actions.md ================================================ --- version: 1 chapter: 5 title: Actions subtitle: Binding Events --- Catalyst Components automatically bind actions upon instantiation. Automatically as part of the `connectedCallback`, a component will search for any children with the `data-action` attribute, and bind events based on the value of this attribute. Any _public method_ on a Controller can be bound to via `data-action`. {% capture callout %} Remember! Actions are _automatically_ bound using the `@controller` decorator. There's no extra JavaScript code needed. {% endcapture %}{% include callout.md %} ### Example
```html ```
```js import { controller, target } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @target name: HTMLElement @target output: HTMLElement greetSomeone() { this.output.textContent = `Hello, ${this.name.value}!` } } ```
### Actions Syntax The actions syntax follows a pattern of `event:controller#method`. - `event` must be the name of a [_DOM Event_](https://developer.mozilla.org/en-US/docs/Web/Events), e.g. `click`. - `:` is the required delimiter between the `event` and `controller`. - `controller` must be the name of a controller ascendant to the element. - `#` is the required delimieter between the `controller` and `method`. - `method` (optional) must be a _public_ _method_ attached to a controller's prototype. Static methods will not work. If method is not supplied, it will default to `handleEvent`. Some examples of Actions Syntax: - `click:my-element#foo` -> `click` events will call `foo` on `my-element` elements. - `submit:my-element#foo` -> `submit` events will call `foo` on `my-element` elements. - `click:user-list` -> `click` events will call `handleEvent` on `user-list` elements. - `click:user-list#` -> `click` events will call `handleEvent` on `user-list` elements. - `click:top-header-user-profile#` -> `click` events will call `handleEvent` on `top-header-user-profile` elements. - `nav:keydown:user-list` -> `navigation:keydown` events will call `handleEvent` on `user-list` elements. ### Multiple Actions 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-loader` Controller might dispatch a `loaded` event, once its contents are loaded, and other controllers can listen to this event: ```html ``` ```js import {controller} from '@github/catalyst' @controller class LazyLoader extends HTMLElement { connectedCallback() { this.innerHTML = await (await fetch(this.dataset.url)).text() this.dispatchEvent(new CustomEvent('loaded')) } } @controller class HoverCard extends HTMLElement { enable() { this.disabled = false } } ``` ### Targets and "ShadowRoots" Custom elements can create encapsulated DOM trees known as "Shadow" DOM. Catalyst actions support Shadow DOM by traversing the `shadowRoot`, if present, and also automatically watching shadowRoots for changes; auto-binding new elements as they are added. ### What about without Decorators? If you're using decorators, then the `@controller` decorator automatically handles binding of actions to a Controller. If you're not using decorators, then you'll need to call `bind(this)` somewhere inside of `connectedCallback()`. ```js import {bind} from '@github/catalyst' class HelloWorldElement extends HTMLElement { connectedCallback() { bind(this) } } ``` ### Binding dynamically added actions Catalyst automatically listens for elements that are dynamically injected into the DOM, and will bind any element's `data-action` attributes. It does this by calling `listenForBind(controller.ownerDocument)`. If for some reason you need to observe other documents (such as mutations within an iframe), then you can call the `listenForBind` manually, passing a `Node` to listen to DOM mutations on. ```js import {listenForBind} from '@github/catalyst' @controller class HelloWorldElement extends HTMLElement { @target iframe: HTMLIFrameElement connectedCallback() { // listenForBind(this.ownerDocument) is automatically called. listenForBind(this.iframe.document.body) } } ``` ================================================ FILE: docs/_guide/anti-patterns-2.md ================================================ --- version: 2 chapter: 15 title: Anti Patterns subtitle: Things to avoid building components permalink: /guide-v2/anti-patterns --- {% capture octx %}{% endcapture %} {% capture octick %}{% endcapture %} {% capture discouraged %}

{{ octx }} Discouraged

{% endcapture %} {% capture encouraged %}

{{ octick }} Encouraged

{% endcapture %} Here are a few common anti-patterns which we've discovered as developers have used Catalyst. We consider these anti-patterns as they're best avoided, because of surprising edge-cases, or simply because there are easier ways to achieve the same goals. ### Avoid doing any initialisation in the constructor With conventional classes, it is expected that initialisation will be done in the `constructor()` method. Custom Elements are slightly different, because the `constructor` is called _before_ the element has been put into the Document, which means any initialisation that expects to be connected to a DOM will fail. {{ discouraged }} ```typescript import { controller } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { constructor() { // This will fire before DOM is connected, so will never bubble! this.dispatchEvent(new CustomEvent('loaded')) } } ``` {{ encouraged }} ```typescript import { controller } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { connectedCallback() { // This will fire _after_ DOM is connected, so will bubble up as expected this.dispatchEvent(new CustomEvent('loaded')) } } ``` ### Avoid interacting with parents, use Events where possible Sometimes it's necessary to let ancestors know about the state of a child element, for example when an element loads or needs the parent to change somehow. Sometimes it can be tempting to use methods like `this.closest()` to get a reference to the parent element and interact with it directly, but this creates a fragile coupling to elements and is best avoided. Events can used here, instead: {{ discouraged }}
```typescript import { controller } from "@github/catalyst" @controller class UserSettingsElement extends HTMLElement { loading() { // While this is loading we need to disable // the whole User if `user-profile` ever // changes, this code will break! this .closest('user-profile') .disable() } } ```
```html ```
Instead of interacting with the parent's API directly in JS, you can use `Events` which can be listened to with `data-action`, this moves any coupling into the HTML which already has the association, and so subsequent refactors will have far less risk of breaking the code: {{ encouraged }}
```typescript import { controller } from "@github/catalyst" @controller class UserSettingsElement extends HTMLElement { loading() { this.dispatchEvent( new CustomEvent('loading') ) } } ```
```html ```
### Avoid shadowing method names When naming a method, you should avoid naming it something that already exists on the `HTMLElement` prototype; as doing so can lead to surprising behaviors. Test out the form below to see what method names are allowed or not:
### Avoid naming methods after events, e.g. `onClick` When you have a method which is only called as an event, it is tempting to name that method based off of the event, e.g. `onClick`, `onInputFocus`, and so on. This name implies a coupling between the event and method, which later refactorings may break. Also names like `onClick` are very close to `onclick` which is already [part of the Element's API](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onclick). Instead we recommend naming the method after what it does, not how it is called, for example `resetForm`: {{ discouraged }}
```js import { controller } from "@github/catalyst" @controller class UserLoginElement extends HTMLElement { // `onClick` is not clear onClick() { // Log the user in } } ```
```html ```
{{ encouraged }}
```js import { controller } from "@github/catalyst" @controller class UserLoginElement extends HTMLElement { login() { // Log the user in } } ```
```html ```
### Avoid querying against your element, use `@target` or `@targets` We find it very common for developers to return to habits and use `querySelector[All]` when needing to get elements. The `@target` and `@targets` decorators were designed to simplify `querySelector[All]` and avoid certain bugs with them (such as nesting issues, and unnecessary coupling) so it's a good idea to use them as much as possible: {{ discouraged }} ```typescript class UserListElement extends HTMLElement { showAdmins() { // Just need to get admins here... for (const user of this.querySelector('[data-is-admin]')) { user.hidden = false } } } ``` {{ encouraged }} ```typescript class UserList { @targets admins: HTMLElement[] showAdmins() { // Just need to get admins here... for (const user of this.admins) { user.hidden = false } } } ``` ### Avoid filtering `@targets`, use another `@target` or `@targets` Sometimes you might need to get a subset of elements from a `@targets` selector. When doing this, simply use another `@target` or `@targets` attribute, it's okay to have many of these! Adding getters which simply return a `@targets` subset has various drawbacks which make it an anti pattern. For example let's say we have a list of filter checkboxes and checking the "all" checkbox unchecks all other checkboxes: {{ discouraged }} ```typescript @controller class UserFilter { @targets filters: HTMLInputElement[] get allFilter() { return this.filters.find(el => el.matches('[data-filter="all"]')) } filter(event: Event) { if (event.target === this.allFilter) { for(const filter of this.filters) { if (filter !== this.allFilter) filter.checked = false } } // ... } } ``` ```html ``` While this works well, it could be more easily solved with targets: {{ encouraged }} ```typescript @controller class UserFilter { @targets filters: HTMLInputElement[] @target allFilter: HTMLInputElement filter(event: Event) { if (event.target === this.allFilter) { for (const filter of this.filters) { if (filter !== this.allFilter) filter.checked = false } } // ... } } ``` ```html ``` ================================================ FILE: docs/_guide/anti-patterns.md ================================================ --- version: 1 chapter: 11 title: Anti Patterns subtitle: Things to avoid building components --- {% capture octx %}{% endcapture %} {% capture octick %}{% endcapture %} {% capture discouraged %}

{{ octx }} Discouraged

{% endcapture %} {% capture encouraged %}

{{ octick }} Encouraged

{% endcapture %} Here are a few common anti-patterns which we've discovered as developers have used Catalyst. We consider these anti-patterns as they're best avoided, because of surprising edge-cases, or simply because there are easier ways to achieve the same goals. ### Avoid doing any initialisation in the constructor With conventional classes, it is expected that initialisation will be done in the `constructor()` method. Custom Elements are slightly different, because the `constructor` is called _before_ the element has been put into the Document, which means any initialisation that expects to be connected to a DOM will fail. {{ discouraged }} ```typescript import { controller } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { constructor() { // This will fire before DOM is connected, so will never bubble! this.dispatchEvent(new CustomEvent('loaded')) } } ``` {{ encouraged }} ```typescript import { controller } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { connectedCallback() { // This will fire _after_ DOM is connected, so will bubble up as expected this.dispatchEvent(new CustomEvent('loaded')) } } ``` ### Avoid interacting with parents, use Events where possible Sometimes it's necessary to let ancestors know about the state of a child element, for example when an element loads or needs the parent to change somehow. Sometimes it can be tempting to use methods like `this.closest()` to get a reference to the parent element and interact with it directly, but this creates a fragile coupling to elements and is best avoided. Events can used here, instead: {{ discouraged }}
```typescript import { controller } from "@github/catalyst" @controller class UserSettingsElement extends HTMLElement { loading() { // While this is loading we need to disable // the whole User if `user-profile` ever // changes, this code will break! this .closest('user-profile') .disable() } } ```
```html ```
Instead of interacting with the parent's API directly in JS, you can use `Events` which can be listened to with `data-action`, this moves any coupling into the HTML which already has the association, and so subsequent refactors will have far less risk of breaking the code: {{ encouraged }}
```typescript import { controller } from "@github/catalyst" @controller class UserSettingsElement extends HTMLElement { loading() { this.dispatchEvent( new CustomEvent('loading') ) } } ```
```html ```
### Avoid shadowing method names When naming a method, you should avoid naming it something that already exists on the `HTMLElement` prototype; as doing so can lead to surprising behaviors. Test out the form below to see what method names are allowed or not:
### Avoid naming methods after events, e.g. `onClick` When you have a method which is only called as an event, it is tempting to name that method based off of the event, e.g. `onClick`, `onInputFocus`, and so on. This name implies a coupling between the event and method, which later refactorings may break. Also names like `onClick` are very close to `onclick` which is already [part of the Element's API](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onclick). Instead we recommend naming the method after what it does, not how it is called, for example `resetForm`: {{ discouraged }}
```js import { controller } from "@github/catalyst" @controller class UserLoginElement extends HTMLElement { // `onClick` is not clear onClick() { // Log the user in } } ```
```html ```
{{ encouraged }}
```js import { controller } from "@github/catalyst" @controller class UserLoginElement extends HTMLElement { login() { // Log the user in } } ```
```html ```
### Avoid querying against your element, use `@target` or `@targets` We find it very common for developers to return to habits and use `querySelector[All]` when needing to get elements. The `@target` and `@targets` decorators were designed to simplify `querySelector[All]` and avoid certain bugs with them (such as nesting issues, and unnecessary coupling) so it's a good idea to use them as much as possible: {{ discouraged }} ```typescript class UserListElement extends HTMLElement { showAdmins() { // Just need to get admins here... for (const user of this.querySelector('[data-is-admin]')) { user.hidden = false } } } ``` {{ encouraged }} ```typescript class UserList { @targets admins: HTMLElement[] showAdmins() { // Just need to get admins here... for (const user of this.admins) { user.hidden = false } } } ``` ### Avoid filtering `@targets`, use another `@target` or `@targets` Sometimes you might need to get a subset of elements from a `@targets` selector. When doing this, simply use another `@target` or `@targets` attribute, it's okay to have many of these! Adding getters which simply return a `@targets` subset has various drawbacks which make it an anti pattern. For example let's say we have a list of filter checkboxes and checking the "all" checkbox unchecks all other checkboxes: {{ discouraged }} ```typescript @controller class UserFilter { @targets filters: HTMLInputElement[] get allFilter() { return this.filters.find(el => el.matches('[data-filter="all"]')) } filter(event: Event) { if (event.target === this.allFilter) { for(const filter of this.filters) { if (filter !== this.allFilter) filter.checked = false } } // ... } } ``` ```html ``` While this works well, it could be more easily solved with targets: {{ encouraged }} ```typescript @controller class UserFilter { @targets filters: HTMLInputElement[] @target allFilter: HTMLInputElement filter(event: Event) { if (event.target === this.allFilter) { for (const filter of this.filters) { if (filter !== this.allFilter) filter.checked = false } } // ... } } ``` ```html ``` ================================================ FILE: docs/_guide/attrs-2.md ================================================ --- version: 2 chapter: 7 title: Attrable subtitle: Using attributes as configuration permalink: /guide-v2/attrs --- Components may sometimes manage state, or configuration. We encourage the use of DOM as state, rather than maintaining a separate state. One way to maintain state in the DOM is via [Attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). As Catalyst elements are really just Web Components, they have the `hasAttribute`, `getAttribute`, `setAttribute`, `toggleAttribute`, and `removeAttribute` set of methods available, as well as [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/dataset), but these can be a little tedious to use; requiring null checking code with each call. Catalyst includes the `@attr` decorator which provides nice syntax sugar to simplify, standardise, and encourage use of attributes. `@attr` has the following benefits over the basic `*Attribute` methods: - It dasherizes a property name, making it safe for HTML serialization without conflicting with [built-in global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes). This works the same as the class name, so for example `@attr pathName` will be `path-name` in HTML, `@attr srcURL` will be `src-url` in HTML. - An `@attr` property automatically casts based on the initial value - if the initial value is a `string`, `boolean`, or `number` - it will never be `null` or `undefined`. No more null checking! - It is automatically synced with the HTML attribute. This means setting the class property will update the HTML attribute, and setting the HTML attribute will update the class property! - Assigning a value in the class description will make that value the _default_ value so if the HTML attribute isn't set, or is set but later removed the _default_ value will apply. This behaves similarly to existing HTML elements where the class field is synced with the html attribute, for example the `` element's `type` field: ```ts const input = document.createElement('input') console.assert(input.type === 'text') // default value console.assert(input.hasAttribute('type') === false) // no attribute to override input.setAttribute('type', 'number') console.assert(input.type === 'number') // overrides based on attribute input.removeAttribute('type') console.assert(input.type === 'text') // back to default value ``` {% capture callout %} An important part of `@attr`s is that they _must_ comprise of two words, so that they get a dash when serialised to HTML. This is intentional, to avoid conflicting with [built-in global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes). To see how JavaScript property names convert to HTML dasherized names, try typing the name of an `@attr` below: {% endcapture %}{% include callout.md %}
To use the `@attr` decorator, attach it to a class field, and it will get/set the value of the matching dasherized HTML attribute. ### Example ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr fooBar = 'hello' } ``` This is somewhat equivalent to: ```js import { controller } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { get fooBar(): string { return this.getAttribute('foo-bar') || '' } set fooBar(value: string): void { return this.setAttribute('foo-bar', value) } connectedCallback() { if (!this.hasAttribute('foo-bar')) this.fooBar = 'Hello' } } ``` ### Attribute Types The _type_ of an attribute is automatically inferred based on the type it is first set to. This means once a value is initially set it cannot change type; if it is set a `string` it will never be anything but a `string`. An attribute can only be one of either a `string`, `number`, or `boolean`. The types have small differences in how they behave in the DOM. Below is a handy reference for the small differences, this is all explained in more detail below that. | Type | When `get` is called | When `set` is called | |:----------|----------------------|:---------------------| | `string` | `getAttribute` | `setAttribute` | | `number` | `getAttribute` | `setAttribute` | | `boolean` | `hasAttribute` | `toggleAttribute` | #### String Attributes If an attribute is first set to a `string`, then it can only ever be a `string` during the lifetime of an element. The property will revert to the initial value if the attribute doesn't exist, and trying to set it to something that isn't a string will turn it into one before assignment. ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr fooBar = 'Hello' connectedCallback() { console.assert(this.fooBar === 'Hello') this.fooBar = 'Goodbye' console.assert(this.fooBar === 'Goodbye'') console.assert(this.getAttribute('foo-bar') === 'Goodbye') this.removeAttribute('foo-bar') // If the attribute doesn't exist, it'll output the initial value! console.assert(this.fooBar === 'Hello') } } ``` #### Boolean Attributes If an attribute is first set to a boolean, then it can only ever be a boolean during the lifetime of an element. Boolean properties check for _presence_ of an attribute, sort of like how [`required`, `disabled` & `readonly` attributes work on forms](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#boolean_attributes) The property will return `false` if the attribute doesn't exist, and `true` if it does, regardless of the value. If the property is set to `false` then `removeAttribute` is called, whereas `setAttribute(name, '')` is called when setting to a truthy value. ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr fooBar = false connectedCallback() { console.assert(this.hasAttribute('foo-bar') === false) this.fooBar = true console.assert(this.hasAttribute('foo-bar') === true) this.setAttribute('foo-bar', 'this value doesnt matter!') console.assert(this.fooBar === true) } } ``` #### Number Attributes If an attribute is first set to a number, then it can only ever be a number during the lifetime of an element. This is sort of like the [`maxlength` attribute on inputs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/maxlength). The property will return the initial value if the attribute doesn't exist, and will be coerced to `Number` if it does - this means it is _possible_ to get back `NaN`. Negative numbers and floats are also valid. ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr fooBar = 1 connectedCallback() { this.fooBar = 2 console.assert(this.getAttribute('foo-bar') === '2') this.setAttribute('foo-bar', 'not a number') console.assert(Number.isNaN(this.fooBar)) this.fooBar = -3.14 console.assert(this.getAttribute('foo-bar') === '-3.14') } } ``` ### Default Values When an element gets connected to the DOM, the attr is initialized. During this phase Catalyst will determine if the default value should be applied. The default value is defined in the class property. The basic rules are as such: - If the class property has a value, that is the _default_ - When connected, if the element _does not_ have a matching attribute, the _default is_ applied. - When connected, if the element _does_ have a matching attribute, the default _is not_ applied, the property will be assigned to the value of the attribute instead. {% capture callout %} Remember! The values defined in the class field are the _default_. They won't be set if the element is created and its attribute set to a custom value! {% endcapture %}{% include callout.md %} The following example illustrates this behavior: ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr dataName = 'World' connectedCallback() { this.textContent = `Hello ${this.dataName}` } } ``` ```html // This will render `Hello World` // This will render `Hello Catalyst` // This will render `Hello ` ``` ### Advanced usage #### Determining when an @attr changes value To be notified when an `@attr` changes value, you can use the decorator over "setter" method instead, and the method will be called with the new value whenever it is re-assigned, either through HTML or JavaScript: ```typescript import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr get dataName() { return 'World' // Used to get the intial value } // Called whenever `name` changes set dataName(newValue: string) { this.textContent = `Hello ${newValue}` } } ``` ### What about without Decorators? If you're not using decorators, then the `@attr` decorator has an escape hatch: You can define a static class field using the `[attr.static]` computed property, as an array of key names. Like so: ```js import {controller, attr} from '@github/catalyst' controller( class HelloWorldElement extends HTMLElement { // Same as @attr fooBar [attr.static] = ['fooBar'] // Field can still be defined fooBar = 1 } ) ``` This example is functionally identical to: ```js import {controller, attr} from '@github/catalyst' @controller class HelloWorldElement extends HTMLElement { @attr fooBar = 1 } ``` ================================================ FILE: docs/_guide/attrs.md ================================================ --- version: 1 chapter: 6 title: Attrs subtitle: Using attributes as configuration --- Components may sometimes manage state, or configuration. We encourage the use of DOM as state, rather than maintaining a separate state. One way to maintain state in the DOM is via [Attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). As Catalyst elements are really just Web Components, they have the `hasAttribute`, `getAttribute`, `setAttribute`, `toggleAttribute`, and `removeAttribute` set of methods available, as well as [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/dataset), but these can be a little tedious to use; requiring null checking code with each call. Catalyst includes the `@attr` decorator, which provides nice syntax sugar to simplify, standardise, and encourage use of attributes. `@attr` has the following benefits over the basic `*Attribute` methods: - It maps whatever the property name is to `data-*`, [similar to how `dataset` does](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/dataset#name_conversion), but with more intuitive naming (e.g. `URL` maps to `data-url` not `data--u-r-l`). - An `@attr` property is limited to `string`, `boolean`, or `number`, it will never be `null` or `undefined` - instead it has an "empty" value. No more null checking! - The attribute name is automatically [observed, meaning `attributeChangedCallback` will fire when it changes](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks). - Assigning a value in the class description will make that value the _default_ value, so when the element is connected that value is set (unless the element has the attribute defined already). To use the `@attr` decorator, attach it to a class field, and it will get/set the value of the matching `data-*` attribute. ### Example ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr foo = 'hello' } ``` This is the equivalent to: ```js import { controller } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { get foo(): string { return this.getAttribute('data-foo') || '' } set foo(value: string): void { return this.setAttribute('data-foo', value) } connectedCallback() { if (!this.hasAttribute('data-foo')) this.foo = 'Hello' } static observedAttributes = ['data-foo'] } ``` ### Attribute Types The _type_ of an attribute is automatically inferred based on the type it is first set to. This means once a value is set it cannot change type; if it is set a `string` it will never be anything but a `string`. An attribute can only be one of either a `string`, `number`, or `boolean`. The types have small differences in how they behave in the DOM. Below is a handy reference for the small differences, this is all explained in more detail below that. | Type | "Empty" value | When `get` is called | When `set` is called | |:----------|:--------------|----------------------|:---------------------| | `string` | `''` | `getAttribute` | `setAttribute` | | `number` | `0` | `getAttribute` | `setAttribute` | | `boolean` | `false` | `hasAttribute` | `toggleAttribute` | #### String Attributes If an attribute is first set to a `string`, then it can only ever be a `string` during the lifetime of an element. The property will return an empty string (`''`) if the attribute doesn't exist, and trying to set it to something that isn't a string will turn it into one before assignment. ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr foo = 'Hello' connectedCallback() { console.assert(this.foo === 'Hello') this.foo = null // TypeScript won't like this! console.assert(this.foo === 'null') delete this.dataset.foo // Removes the attribute console.assert(this.foo === '') // If the attribute doesn't exist, its an empty string! } } ``` #### Boolean Attributes If an attribute is first set to a boolean, then it can only ever be a boolean during the lifetime of an element. Boolean properties check for _presence_ of an attribute, sort of like how [`required`, `disabled` & `readonly` attributes work on forms](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#boolean_attributes) The property will return `false` if the attribute doesn't exist, and `true` if it does, regardless of the value. If the property is set to `false` then `removeAttribute` is called, whereas `setAttribute(name, '')` is called when setting to a truthy value. ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr foo = false connectedCallback() { console.assert(this.hasAttribute('data-foo') === false) this.foo = true console.assert(this.hasAttribute('data-foo') === true) this.setAttribute('data-foo', 'this value doesnt matter!') console.assert(this.foo === true) } } ``` #### Number Attributes If an attribute is first set to a number, then it can only ever be a number during the lifetime of an element. This is sort of like the [`maxlength` attribute on inputs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/maxlength). The property will return `0` if the attribute doesn't exist, and will be coerced to `Number` if it does - this means it is _possible_ to get back `NaN`. Negative numbers and floats are also valid. ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr foo = 1 connectedCallback() { console.assert(this.getAttribute('data-foo') === '1') this.setAttribute('data-foo', 'not a number') console.assert(Number.isNaN(this.foo)) this.foo = -3.14 console.assert(this.getAttribute('data-foo') === '-3.14') } } ``` ### Default Values When an element gets connected to the DOM, the attr is initialized. During this phase Catalyst will determine if the default value should be applied. The default value is defined in the class property. The basic rules are as such: - If the class property has a value, that is the _default_ - When connected, if the element _does not_ have a matching attribute, the default _is_ applied. - When connected, if the element _does_ have a matching attribute, the default _is not_ applied, the property will be assigned to the value of the attribute instead. {% capture callout %} Remember! The values defined in the class field are the _default_. They won't be set if the element is created and its attribute set to a custom value! {% endcapture %}{% include callout.md %} The following example illustrates this behavior: ```js import { controller, attr } from "@github/catalyst" @controller class HelloWorldElement extends HTMLElement { @attr name = 'World' connectedCallback() { this.textContent = `Hello ${this.name}` } } ``` ```html // This will render `Hello World` // This will render `Hello Catalyst` // This will render `Hello ` ``` ### What about without Decorators? If you're not using decorators, then you won't be able to use the `@attr` decorator, but there is still a way to achieve the same result. Under the hood `@attr` simply tags a field, but `initializeAttrs` and `defineObservedAttributes` do all of the logic. Calling `initializeAttrs` in your connected callback, with the list of properties you'd like to initialize, and calling `defineObservedAttributes` with the class, can achieve the same result as `@attr`. The class fields can still be defined in your class, and they'll be overridden as described above. For example: ```js import {initializeAttrs, defineObservedAttributes} from '@github/catalyst' class HelloWorldElement extends HTMLElement { foo = 1 connectedCallback() { initializeAttrs(this, ['foo']) } } defineObservedAttributes(HelloWorldElement, ['foo']) ``` This example is functionally identical to: ```js import {controller, attr} from '@github/catalyst' @controller class HelloWorldElement extends HTMLElement { @attr foo = 1 } ``` ================================================ FILE: docs/_guide/conventions-2.md ================================================ --- version: 2 chapter: 13 title: Conventions subtitle: Common naming and patterns permalink: /guide-v2/conventions --- Catalyst strives for convention over code. Here are a few conventions we recommend when writing Catalyst code: ### Suffix your controllers consistently, for symmetry 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 {} // `` ``` ```typescript @controller class UserListComponent extends HTMLElement {} // `` ``` ### The best class-names are two word descriptions Custom elements are required to have a `-` inside the tag name. Catalyst's `@controller` will derive the tag name from the class name - and so as such the class name needs to have at least two capital letters, or to put it another way, it needs to consist of at least two CamelCased words. The element name should describe what it does succinctly in two words. Some examples: - `theme-picker` (`class ThemePickerElement`) - `markdown-toolbar` (`class MarkdownToolbarElement`) - `user-list` (`class UserListElement`) - `content-pager` (`class ContentPagerElement`) - `image-gallery` (`class ImageGalleryElement`) If you're struggling to come up with two words, think about one word being the "what" (what does it do?) and another being the "how" (how does it do it?). ### Keep class-names short (but not too short) Brevity is good, element names are likely to be typed out a lot, especially throughout HTML in as tag names, and `data-target`, `data-action` attributes. A good rule of thumb is to try to keep element names down to less than 15 characters (excluding the `Element` suffix), and ideally less than 10. Also, longer words are generally harder to spell, which means mistakes might creep into your code. Be careful not to go too short! We'd recommend avoiding contracting words such as using `Img` to mean `Image`. It can create confusion, especially if there are inconsistencies across your code! ### Method names should describe what they do A good method name, much like a good class name, describes what it does, not how it was invoked. While methods can be given most names, you should avoid names that conflict with existing methods on the `HTMLElement` prototype (more on that in [anti-patterns]({{ site.baseurl }}/guide/anti-patterns#avoid-shadowing-method-names)). Names like `onClick` are best avoided, overly generic names like `toggle` should also be avoided. Just like class names it is a good idea to ask "how" and "what", so for example `showAdmins`, `filterUsers`, `updateURL`. ### `@target` should use singular naming, while `@targets` should use plural To help differentiate the two `@target`/`@targets` decorators, the properties should be named with respective to their cardinality. That is to say, if you're using an `@target` decorator, then the name should be singular (e.g. `user`, `field`) while the `@targets` decorator should be coupled with plural property names (e.g. `users`, `fields`). ================================================ FILE: docs/_guide/conventions.md ================================================ --- version: 1 chapter: 9 title: Conventions subtitle: Common naming and patterns --- Catalyst strives for convention over code. Here are a few conventions we recommend when writing Catalyst code: ### Use `Element` to suffix your controller class 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. ```typescript @controller class UserListElement extends HTMLElement {} ``` ### The best class-names are two word descriptions Custom elements are required to have a `-` inside the tag name. Catalyst's `@controller` will derive the tag name from the class name - and so as such the class name needs to have at least two capital letters, or to put it another way, it needs to consist of at least two CamelCased words. The element name should describe what it does succinctly in two words. Some examples: - `theme-picker` (`class ThemePickerElement`) - `markdown-toolbar` (`class MarkdownToolbarElement`) - `user-list` (`class UserListElement`) - `content-pager` (`class ContentPagerElement`) - `image-gallery` (`class ImageGalleryElement`) If you're struggling to come up with two words, think about one word being the "what" (what does it do?) and another being the "how" (how does it do it?). ### Keep class-names short (but not too short) Brevity is good, element names are likely to be typed out a lot, especially throughout HTML in as tag names, and `data-target`, `data-action` attributes. A good rule of thumb is to try to keep element names down to less than 15 characters (excluding the `Element` suffix), and ideally less than 10. Also, longer words are generally harder to spell, which means mistakes might creep into your code. Be careful not to go too short! We'd recommend avoiding contracting words such as using `Img` to mean `Image`. It can create confusion, especially if there are inconsistencies across your code! ### Method names should describe what they do A good method name, much like a good class name, describes what it does, not how it was invoked. While methods can be given most names, you should avoid names that conflict with existing methods on the `HTMLElement` prototype (more on that in [anti-patterns]({{ site.baseurl }}/guide/anti-patterns#avoid-shadowing-method-names)). Names like `onClick` are best avoided, overly generic names like `toggle` should also be avoided. Just like class names it is a good idea to ask "how" and "what", so for example `showAdmins`, `filterUsers`, `updateURL`. ### `@target` should use singular naming, while `@targets` should use plural To help differentiate the two `@target`/`@targets` decorators, the properties should be named with respective to their cardinality. That is to say, if you're using an `@target` decorator, then the name should be singular (e.g. `user`, `field`) while the `@targets` decorator should be coupled with plural property names (e.g. `users`, `fields`). ================================================ FILE: docs/_guide/create-ability.md ================================================ --- version: 2 chapter: 9 title: Create Ability subtitle: Create your own abilities permalink: /guide-v2/create-ability --- Catalyst provides the functionality to create your own abilities, with a few helper methods and a `controllable` base-level ability. These are explained in detail below, but for a quick summary they are: - `createAbility` - a helper function to make new abilities (class decorators). - `createMark` - a helper function to generate class field & method decorators. - `tag-observer` - a set of helper functions to watch for tagged children in an element's subtree. - `controllable` - the base ability which allows interacting with semi-private parts of an element. ## createAbility This function allows you to make your own [Ability]({{ site.baseurl }}/guide/abilities). Abilities are really Class Decorators, but there's a couple of things that `createAbility` provides to simplify the ergonomics of Class Decorators: - TypeScript can be a little tricky when working with Class Decorators. `createAbility` simplifies this a bit. - JavaScript does not copy over the `name` property when extending a class (e.g. via a decorator), and it can be a little cumbersome to do this, so `createAbility` does this for you. - Abilities are [idempotent](https://en.wikipedia.org/wiki/Idempotence). Class decorators are not idempotent by default, which means applying a decorator multiple times can cause issues. `createAbility` mitigates this by memoizing the classes it has applied to, meaning applying an ability multiple times has no effect past the first application. The above three features of `createAbility` make it really useful when creating mixins for web components, and makes them much easier for developers as they can trust an ability to not be sensitive to these problems. To create an ability, call the `createAbility` method and pass in a callback function which takes a `CustomElementClass` and returns a new class. You can also provide extra types if your returned class adds new methods or fields. Here's an example, using TypeScript: ```typescript import type {CustomElementClass} from '@github/catalyst' import {createAbility} from '@github/catalyst' // by convention, abilities end in "able" interface Fooable { foo(): void // This interface has additional methods on top of `CustomElementClass`! } // Fooable: automatically calls `foo()` on `connectedCallback` export const fooable = createAbility( // ↓ Notice the `& { new (): Fooable }` (Class: T): T & { new (): Fooable } => class extends Class { connectedCallback() { this.foo() } foo() { console.log('Foo was called!') } } ) ``` Inside the `class extends Class` block, you can author custom element logic that you might want to make reusable across a multitude of elements. You can also adjust the input type to subclass `CustomElementClass`, which can be useful for setting up a contract between your Ability and the classes that rely on it: ```typescript import type {CustomElementClass} from '@github/catalyst' import {createAbility} from '@github/catalyst' // by convention, abilities end in "able" interface Fooable { foo(): void // This interface has additional methods on top of `CustomElementClass`! } interface FooableClass { new(...args: any[]): Fooable } // Fooable: automatically calls `foo()` on `connectedCallback` export const fooable = createAbility( // ↓ Notice the `& FooableClass` (Class: T): T => class extends Class { // TypeScript will expect the constructor to be defined for a mixin constructor(...args: any[]) { super(...args) } connectedCallback() { // Classes that apply this ability _must_ implement `foo()`. super.foo() } } ``` If you're interested in some advanced examples, you can take a look at the Catalyst source code - every feature of Catalyst is an Ability! ## createMark This function allows you to make annotations for fields (like `@attr` and `@target`). Marks are really Field/Method Decorators, but with simpler ergonomics: - Marks are only initialized on instances, which makes them easier to reason about. - Marks are not configurable, which keeps them simple. - They are built to ease a transition between TypeScript decorators and ECMAScript decorators, which will help as decorators become standardised. `createMark` can be called with a `validate` and an `init` function, and gives back a tuple of 3 functions: the decorator itself, a function to get a list of marks that an instance has, and a function that will initialise the marks on an instance. It can be used like so: ```typescript // Makes the @prop decorator const [prop, getProps, initProps] = createMark( ({name, kind}) => { // Validate the name and kind that a mark can have. // Name will be the PropertyKey that was decorated, and `kind` will be one of: // "method", "field", "getter", "setter". if (kind === "method") { throw new Error(`@prop cannot be used to mark a method`) } }, (instance: CustomElement, {name, kind, access}) => { // Put field initialization logic here. // Return a property descriptor to define a field's functionality: let value = kind === 'field' ? access.value : access.get?.call(instance) return { get() { return value } set(newValue) { value = newValue instance.propChanged(name, newValue) } } } ) ``` If you want to find some examples of how marks work, take a look at the Catalyst source code! All field decorators (`@attr`, `@target`, `@provide`, `@consume` and so on) use `createMark`. ## tag-observer Tag Observer provides a set of functions to observe elements marked with well-known attributes across the DOM, allowing classes to be reactive to DOM mutations. These functions operate over a `MutationObserver` set up to detect new elements coming into the page that have a registered attribute. To call register a new tag you can use the `registerTag` function which takes an attribute name to observe, a parse function (that parses the attribute value), and a found function (which is called for each element that has the attribute): ```typescript registerTag( `data-foo`, (value: string) => value.split('.'), (el: Element, controller: Element | ShadowRoot, ...meta: string[]) => { // ... } ) ``` Tag Observer also provides a `observeElementForTags` function, which can be called on an element to adopt it into observation. A good place to use this is in your Abilities `connectedCallback`. This function can also take a `shadowRoot` if you're interested in observing tags within the shadow DOM (recommended). This function will find the root element (`ownerDocument`) and begin observing it. ```typescript export const fooable = createAbility( (Class: T): T => class extends Class { connectedCallback() { observeElementForTags(this) // This elements ownerDocument will now look out for new tags } } ``` Whenever an element appears on the page with the matching attribute (e.g. `data-foo`), the value is extracted, split by whitespace, and each substring is then given to `parse` to turn into an array of strings. The first value in the array that the parse function returns must be a parent selector, which is then used to find the controller this attribute could pertain to. If the element is a child of the given controller selector, then the found function is called with the element, the controller, and any additional metadata that the parse function extracted. Let's see an example for how this might work, given the above registered tag: ```html
``` - Our `data-foo` attribute is found in the DOM, belonging to the `div` element. - The value is extracted and split by whitespace. - Our parse function gets called twice, firstly with `my-element.foo.bar` - The parse function splits this by `.` which gets us `['my-element', 'foo', 'bar']`. - Tag observer uses `my-element` as the parent selector and calls `div.closest('my-element')`, - The `` controller is found. - Our found function is called with `(
, , ['foo', 'bar'])` - The parse function is also called with `other-element.baz.bing`. - The parse function splits this by `.` which gets us `['other-element', 'baz', 'bing']`. - Tag observer uses `other-element` as the parent selector and calls `div.closest('other-element')`, - No parent element is found, so the found function is not called. To take a look at how Tag Observer is used in Catalyst, you can look at [`data-action` (the Actionable ability)]({{ site.baseurl }}/guide/actions) or [`data-target` & `data-targets` (the Targetable ability)]({{ site.baseurl }}/guide/targets). ## controllable `controllable` is a basic ability which other abilities can use to simplify connecting to a custom elements private state. This ability isn't _required_ to be used when creating your own abilities, but it's very useful for abilities which expect to use either the [ShadowDOM](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) or [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals). You can create an ability that itself uses the `controllable` ability like so: ```typescript import type {CustomElementClass} from '@github/catalyst' import {createAbility, controllable} from '@github/catalyst' createAbility((Class: CustomElementClass) => class extends controllable(Class) { // Your behaviour goes here! } ``` The `controllable` ability provides 2 _custom_ callbacks which allow you to safely & robustly intercept the attachment of a ShadowRoot, and the attachment of ElementInternals. Let's look at each: ### `[attachShadowCallback](shadowRoot: ShadowRoot)` ```typescript import type {CustomElementClass} from '@github/catalyst' import {createAbility, attachShadowCallback, controllable} from '@github/catalyst' createAbility((Class: CustomElementClass) => class extends controllable(Class) { [attachShadowCallback](shadowRoot: ShadowRoot) { super[attachShadowCallback](shadowRoot) // Do stuff with the `shadowRoot`. } } ``` `attachShadowCallback` is a special `Symbol()` which allows you to make a method mostly hidden from other classes. `controllable` will call this symbol method whenever a ShadowRoot is attached to the element, which can be attached in 2 different ways: - During the constructor, where the element might recieve a declarative ShadowDOM root (closed or open). - Any time the `attachShadow()` function is called. This method is _usually_ called zero or once, but may be called twice if the element recieves a Declarative ShadowDOM root, and overrides this with another call to `attachShadow()`. Simply overriding `this.attachShadow` or trying to access `this.shadowRoot` can be a little tricky (if an element has a closed declarative shadow root this can be especially difficult to access within mixins), so this callback can be especially useful for working around the various ways a shadowRoot can be created. ### `[attachInternalsCallback](internals: ElementInternals)` ```typescript import type {CustomElementClass} from '@github/catalyst' import {createAbility, attachInternalsCallback, controllable} from '@github/catalyst' createAbility((Class: CustomElementClass) => class extends controllable(Class) { [attachInternalsCallback](internals: ElementInternals) { super[attachInternalsCallback](internals) // Do stuff with the `internals`. } } ``` `attachInternalsCallback` is a special `Symbol()` which allows you to make a method mostly hidden from other classes. `controllable` will call this symbol method whenever an element is constructed, giving it the element's `ElementInternals`. This enables custom enablies [Abilities]({{ site.baseurl }}/guide/abilities) to also have access to `ElementInternals`. It does so while also preserving the ability for `attachInternals()` to be called again (usually `attachInternals()` will error if called twice). If you need access to the internals, then the `attachInternalsCallback` can be very useful as it protects you from calling `attachInternals` in a way which the concrete classes will then fail. ================================================ FILE: docs/_guide/decorators-2.md ================================================ --- version: 2 chapter: 3 title: Decorators subtitle: Using TypeScript for ergonomics permalink: /guide-v2/decorators --- Decorators are used heavily in Catalyst, because they provide really clean ergonomics and makes using the library a lot easier. Decorators are a special, (currently) non standard, feature of TypeScript. You'll need to turn the `experimentalDecorators` option on inside of your TypeScript project to use them (if you're using `@babel/plugin-proposal-decorators` plugin, you need to use [`legacy` option](https://babeljs.io/docs/en/babel-plugin-proposal-decorators#legacy)). You can read more about [decorators in the TypeScript handbook](https://www.typescriptlang.org/docs/handbook/decorators.html), but here's quick guide: Decorators can be used three ways: ### Class Decorators Catalyst comes with the `@controller` decorator. This gets put on top of the class, like so: ```js @controller class HelloWorldElement extends HTMLElement {} ``` ### Class Field Decorators Catalyst comes with the `@target` and `@targets` decorators (for more on these [read the Targets guide section]({{ site.baseurl }}/guide/targets)). These get added on top or to the left of the field name, like so: ```js class HelloWorldElement extends HTMLElement { @target something // Alternative style @targets others } ```
Class Field decorators get given the class and the field name so they can add custom functionality to the field. Because they operate on the fields, they must be put on top of or to the left of the field. ### Method Decorators Method decorators work just like Field Decorators. Put them on top or on the left of the method, like so: ```js class HelloWorldElement extends HTMLElement { @log submit() { // ... } // Alternative style @log load() { // ... } } ``` ### Getter/Setter Decorators can also be used over a `get` or `set` field. These work just like Field Decorators, but you can put them over one or both the `get` or `set` field. Some decorators might throw an error if you put them over a `get` field, when they expect to be put over a `set` field: ```js class HelloWorldElement extends HTMLElement { @target set something() { // ... } // Can be used over just one field @attr get data() { return {} } set data() { } } ``` ### Supporting `strictPropertyInitialization` TypeScript comes with various "strict" mode settings, one of which is `strictPropertyInitialization` which lets TypeScript catch potential class properties which might not be assigned during construction of a class. This option conflicts with Catalyst's `@target`/`@targets` decorators, which safely do the assignment but TypeScript's simple heuristics cannot detect this. There are two ways to work around this: 1. Use TypeScript's [`declare` modifier](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier) to tell TypeScript that the decorated field will still be set up correctly: ```typescript class HelloWorldElement extends HTMLElement { @target declare something: HTMLElement @targets declare items: HTMLElement[] } ``` Note that this only works on TypeScript 3.7+, so if you're on an older version, you can also use the [definite initialization operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions) to do the same thing. 2. You can also disable the compiler option (other strict mode rules can still apply) in your `tsconfig.json` like so: ```json { "compilerOptions": { "strict": true, "strictPropertyInitialization": false } } ``` ### Function Calling Decorators 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 { @debounce(100) handleInput() { // ... } } ```
================================================ FILE: docs/_guide/decorators.md ================================================ --- version: 1 chapter: 3 title: Decorators subtitle: Using TypeScript for ergonomics --- Decorators are used heavily in Catalyst, because they provide really clean ergonomics and makes using the library a lot easier. Decorators are a special, (currently) non standard, feature of TypeScript. You'll need to turn the `experimentalDecorators` option on inside of your TypeScript project to use them (if you're using `@babel/plugin-proposal-decorators` plugin, you need to use [`legacy` option](https://babeljs.io/docs/en/babel-plugin-proposal-decorators#legacy)). You can read more about [decorators in the TypeScript handbook](https://www.typescriptlang.org/docs/handbook/decorators.html), but here's quick guide: Decorators can be used three ways: ### Class Decorators Catalyst comes with the `@controller` decorator. This gets put on top of the class, like so: ```js @controller class HelloWorldElement extends HTMLElement {} ``` ### Class Field Decorators Catalyst comes with the `@target` and `@targets` decorators (for more on these [read the Targets guide section]({{ site.baseurl }}/guide/targets)). These get added on top or to the left of the field name, like so: ```js class HelloWorldElement extends HTMLElement { @target something // Alternative style @targets others } ```
Class Field decorators get given the class and the field name so they can add custom functionality to the field. Because they operate on the fields, they must be put on top of or to the left of the field. ### Method Decorators Method decorators work just like Field Decorators. Put them on top or on the left of the method, like so: ```js class HelloWorldElement extends HTMLElement { @log submit() { // ... } // Alternative style @log load() { // ... } } ``` ### Getter/Setter Decorators can also be used over a `get` or `set` field. These work just like Field Decorators, but you can put them over one or both the `get` or `set` field. Some decorators might throw an error if you put them over a `get` field, when they expect to be put over a `set` field: ```js class HelloWorldElement extends HTMLElement { @target set something() { // ... } // Can be used over just one field @attr get data() { return {} } set data() { } } ``` ### Supporting `strictPropertyInitialization` TypeScript comes with various "strict" mode settings, one of which is `strictPropertyInitialization` which lets TypeScript catch potential class properties which might not be assigned during construction of a class. This option conflicts with Catalyst's `@target`/`@targets` decorators, which safely do the assignment but TypeScript's simple heuristics cannot detect this. There are two ways to work around this: 1. Use TypeScript's [`declare` modifier](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier) to tell TypeScript that the decorated field will still be set up correctly: ```typescript class HelloWorldElement extends HTMLElement { @target declare something: HTMLElement @targets declare items: HTMLElement[] } ``` Note that this only works on TypeScript 3.7+, so if you're on an older version, you can also use the [definite initialization operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions) to do the same thing. 2. You can also disable the compiler option (other strict mode rules can still apply) in your `tsconfig.json` like so: ```json { "compilerOptions": { "strict": true, "strictPropertyInitialization": false } } ``` ### Function Calling Decorators 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 { @debounce(100) handleInput() { // ... } } ```
================================================ FILE: docs/_guide/introduction-2.md ================================================ --- version: 2 chapter: 1 title: Introduction subtitle: Origins & Concepts permalink: /guide-v2/introduction --- Catalyst is a set of patterns and techniques for developing _components_ within a complex application. At its core, Catalyst simply provides a small library of functions to make developing [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) easier. The library is an implementation detail, though. The concepts are what we're most interested in. ## How did we get here? GitHub's first page interactions were written using jQuery, which was widely used at the time. Eventually, as browser compatibility increased and jQuery patterns such as the Selector Pattern & easy class manipulation became standard, [GitHub moved away from jQuery](https://github.blog/2018-09-06-removing-jquery-from-github-frontend/). Rather than moving to entirely new paradigms, GitHub continued to use the same concepts within jQuery. Event Delegation was still heavily used, as well as querySelector. The event delegation concept was also extended to "element delegation" - discovering when Elements were added to the DOM, using the [Selector Observer](https://github.com/josh/selector-observer) library. These patterns were reduced to first principles: _Observing_ elements on the page, _listening_ to the events these elements or their children emit, and _querying_ the children of an element to mutate or extend them. The Web Systems team at GitHub explored other tools that adopt these set of patterns and principles. The closest match to those goals was [Stimulus](https://stimulusjs.org/) (from which Catalyst is heavily inspired), but ultimately the desire to leverage technology that engineers at GitHub were already familiar with was the motivation to create Catalyst. ## Three core concepts: Observe, Listen, Query Catalyst takes these three core concepts and delivers them in the lightest possible way they can be delivered. - **Observability** Catalyst solves observability by leveraging [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements). Custom Elements are given unique names within a system, and the browser will automatically use the Custom Element registry to observe these Elements entering and leaving the DOM. Read more about this in the Guide Section entitled [Your First Component]({{ site.baseurl }}/guide/your-first-component). - **Listening** Event Delegation makes a great deal of sense when observing events "high up the tree" - registering global event listeners on the Window element - but Custom Elements sit much closer to their children within the tree, and so Direct Event binding is preferred. Catalyst solves this by binding event listeners to any descendants with `data-action` attributes. Read more about this in the Guide Section entitled [Actions]({{ site.baseurl }}/guide/actions). - **Querying** Custom Elements largely solve querying, by simply calling `querySelector` - however CSS selectors are loosely disciplined and can create unnecessary coupling to the DOM structure (e.g. by querying tag names). Catalyst extends the `data-action` concept by also using `data-target` to declare descendants that the Custom Element is interested in querying. Read more about this in the Guide Section entitled [Targets]({{ site.baseurl }}/guide/targets). ================================================ FILE: docs/_guide/introduction.md ================================================ --- version: 1 chapter: 1 title: Introduction subtitle: Origins & Concepts --- Catalyst is a set of patterns and techniques for developing _components_ within a complex application. At its core, Catalyst simply provides a small library of functions to make developing [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) easier. The library is an implementation detail, though. The concepts are what we're most interested in. ## How did we get here? GitHub's first page interactions were written using jQuery, which was widely used at the time. Eventually, as browser compatibility increased and jQuery patterns such as the Selector Pattern & easy class manipulation became standard, [GitHub moved away from jQuery](https://github.blog/2018-09-06-removing-jquery-from-github-frontend/). Rather than moving to entirely new paradigms, GitHub continued to use the same concepts within jQuery. Event Delegation was still heavily used, as well as querySelector. The event delegation concept was also extended to "element delegation" - discovering when Elements were added to the DOM, using the [Selector Observer](https://github.com/josh/selector-observer) library. These patterns were reduced to first principles: _Observing_ elements on the page, _listening_ to the events these elements or their children emit, and _querying_ the children of an element to mutate or extend them. The Web Systems team at GitHub explored other tools that adopt these set of patterns and principles. The closest match to those goals was [Stimulus](https://stimulusjs.org/) (from which Catalyst is heavily inspired), but ultimately the desire to leverage technology that engineers at GitHub were already familiar with was the motivation to create Catalyst. ## Three core concepts: Observe, Listen, Query Catalyst takes these three core concepts and delivers them in the lightest possible way they can be delivered. - **Observability** Catalyst solves observability by leveraging [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements). Custom Elements are given unique names within a system, and the browser will automatically use the Custom Element registry to observe these Elements entering and leaving the DOM. Read more about this in the Guide Section entitled [Your First Component]({{ site.baseurl }}/guide/your-first-component). - **Listening** Event Delegation makes a great deal of sense when observing events "high up the tree" - registering global event listeners on the Window element - but Custom Elements sit much closer to their children within the tree, and so Direct Event binding is preferred. Catalyst solves this by binding event listeners to any descendants with `data-action` attributes. Read more about this in the Guide Section entitled [Actions]({{ site.baseurl }}/guide/actions). - **Querying** Custom Elements largely solve querying, by simply calling `querySelector` - however CSS selectors are loosely disciplined and can create unnecessary coupling to the DOM structure (e.g. by querying tag names). Catalyst extends the `data-action` concept by also using `data-target` to declare descendants that the Custom Element is interested in querying. Read more about this in the Guide Section entitled [Targets]({{ site.baseurl }}/guide/targets). ================================================ FILE: docs/_guide/lazy-elements-2.md ================================================ --- version: 2 chapter: 16 title: Lazy Elements subtitle: Dynamically load elements just in time permalink: /guide-v2/lazy-elements --- A common practice in modern web development is to combine all JavaScript code into JS "bundles". By bundling the code together we avoid the network overhead of fetching each file. However the trade-off of bundling is that we might deliver JS code that will never run in the browser. ![A screenshot from Chrome Devtools showing the Coverage panel. The panel has multiple request to JS assets and it shows that most of them have large chunks that are unused.](/catalyst/guide/devtools-coverage.png) An alternative solution to bundling is to load JavaScript just in time. Downloding the JavaScript for Catalyst controllers when the browser first encounters them can be done with the `lazyDefine` function. ```typescript import {lazyDefine} from '@github/catalyst' // Dynamically import the Catalyst controller when the `` tag is seen. lazyDefine('user-avatar', () => import('./components/user-avatar')) ``` Serving this file allows us to defer loading of the component code until it's actually needed by the web page. The tradeoff of deferring loading is that the elements will be inert until the dynamic import of the component code resolves. Consider what your UI might look like while these components are resolving. Consider providing a loading indicator and disabling controls as the default state. The smaller the component, the faster it will resolve which means that your users might not notice a inert state. A good rule of thumb is that a component should load within 100ms on a "Fast 3G" connection. Generally we think it's a good idea to `lazyDefine` all elements and then prioritize eager loading of ciritical elements as needed. You might consider using code-generation to generate a file lazy defining all your components. By default the component will be loaded when the element is present in the document and the document has finished loading. This can happen before sub-resources such as scripts, images, stylesheets and frames have finished loading. It is possible to defer loading even later by adding a `data-load-on` attribute on your element. The value of which must be one of the following prefefined values: - `` (default) - The element is loaded when the document has finished loading. This listens for changes to `document.readyState` and triggers when it's no longer loading. - `` - This element is loaded on the first user interaction with the page. This listens for `mousedown`, `touchstart`, `pointerdown` and `keydown` events on `document`. - `` - This element is loaded when it's close to being visible. Similar to `` . The functionality is driven by an `IntersectionObserver`. This functionality is similar to the ["Lazy Custom Element Definitions" spec proposal](https://github.com/WICG/webcomponents/issues/782) and as this proposal matures we see Catalyst conforming to the spec and leveraging this new API to lazy load elements. ================================================ FILE: docs/_guide/lazy-elements.md ================================================ --- version: 1 chapter: 13 title: Lazy Elements subtitle: Dynamically load elements just in time --- A common practice in modern web development is to combine all JavaScript code into JS "bundles". By bundling the code together we avoid the network overhead of fetching each file. However the trade-off of bundling is that we might deliver JS code that will never run in the browser. ![A screenshot from Chrome Devtools showing the Coverage panel. The panel has multiple request to JS assets and it shows that most of them have large chunks that are unused.](/catalyst/guide/devtools-coverage.png) An alternative solution to bundling is to load JavaScript just in time. Downloding the JavaScript for Catalyst controllers when the browser first encounters them can be done with the `lazyDefine` function. ```typescript import {lazyDefine} from '@github/catalyst' // Dynamically import the Catalyst controller when the `` tag is seen. lazyDefine('user-avatar', () => import('./components/user-avatar')) ``` Serving this file allows us to defer loading of the component code until it's actually needed by the web page. The tradeoff of deferring loading is that the elements will be inert until the dynamic import of the component code resolves. Consider what your UI might look like while these components are resolving. Consider providing a loading indicator and disabling controls as the default state. The smaller the component, the faster it will resolve which means that your users might not notice a inert state. A good rule of thumb is that a component should load within 100ms on a "Fast 3G" connection. Generally we think it's a good idea to `lazyDefine` all elements and then prioritize eager loading of ciritical elements as needed. You might consider using code-generation to generate a file lazy defining all your components. By default the component will be loaded when the element is present in the document and the document has finished loading. This can happen before sub-resources such as scripts, images, stylesheets and frames have finished loading. It is possible to defer loading even later by adding a `data-load-on` attribute on your element. The value of which must be one of the following prefefined values: - `` (default) - The element is loaded when the document has finished loading. This listens for changes to `document.readyState` and triggers when it's no longer loading. - `` - This element is loaded on the first user interaction with the page. This listens for `mousedown`, `touchstart`, `pointerdown` and `keydown` events on `document`. - `` - This element is loaded when it's close to being visible. Similar to `` . The functionality is driven by an `IntersectionObserver`. This functionality is similar to the ["Lazy Custom Element Definitions" spec proposal](https://github.com/WICG/webcomponents/issues/782) and as this proposal matures we see Catalyst conforming to the spec and leveraging this new API to lazy load elements. ================================================ FILE: docs/_guide/lifecycle-hooks-2.md ================================================ --- version: 2 chapter: 10 title: Lifecycle Hooks subtitle: Observing the life cycle of an element permalink: /guide-v2/lifecycle-hooks --- Catalyst Controllers - like many other frameworks - have several "well known" method names which are called periodically through the life cycle of the element, and let you observe when an element changes in various ways. Here is a comprehensive list of all life-cycle callbacks. Each one is suffixed `Callback`, to denote that it will be called by the framework. ### `connectedCallback()` The [`connectedCallback()` is part of Custom Elements][ce-callbacks], and gets fired as soon as your element is _appended_ to the DOM. This callback is a good time to initialize any variables, perhaps add some global event listeners, or start making any early network requests. JavaScript traditionally uses the `constructor()` callback to listen for class creation. While this still works for Custom Elements, it is best avoided as the element won't be in the DOM when `constructor()` is fired, limiting its utility. #### Things to remember The `connectedCallback` is called _as soon as_ the element is attached to a `document`. This _may_ occur _before_ an element has any children appended to it, so you should be careful not expect an element to have children during a `connectedCallback` call. This means avoiding checking any `target`s or using other methods like `querySelector`. Instead use this function to initialize itself and avoid doing initialization work which depend on children existing. If your element depends heavily on its children existing, consider adding a [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) in the `connectedCallback` to track when your elements children change. ### `attributeChangedCallback()` The [`attributeChangedCallback()` is part of Custom Elements][ce-callbacks], and gets fired when _observed attributes_ are added, changed, or removed from your element. It required you set a `static observedAttributes` array on your class, the values of which will be any attributes that will be observed for mutations. This is given a set of arguments, the signature of your function should be: ```typescript attributeChangedCallback(name: string, oldValue: string|null, newValue: string|null): void {} ``` #### Things to remember The `attributeChangedCallback` will fire whenever `setAttribute` is called with an observed attribute, even if the _new_ value is the same as the _old_ value. In other words, it is possible for `attributeChangedCallback` to be called when `oldValue === newValue`. In most cases this really won't matter much, and in some cases this is very helpful; but sometimes this can bite, especially if you have [non-idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_examples) code inside your `attributeChangedCallback`. Try to make sure operations inside `attributeChangedCallback` are idempotent, or perhaps consider adding a check to ensure `oldValue !== newValue` before performing operations which may be sensitive to this. ### `disconnectedCallback()` The [`disconnectedCallback()` is part of Custom Elements][ce-callbacks], and gets fired as soon as your element is _removed_ from the DOM. Event listeners will automatically be cleaned up, and memory will be freed automatically from JavaScript, so you're unlikely to need this callback for much. ### `adoptedCallback()` The [`adoptedCallback()` is part of Custom Elements][ce-callbacks], and gets called when your element moves from one `document` to another (such as an iframe). It's very unlikely to occur, you'll almost never need this. [ce-callbacks]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks ================================================ FILE: docs/_guide/lifecycle-hooks.md ================================================ --- version: 1 chapter: 7 title: Lifecycle Hooks subtitle: Observing the life cycle of an element --- Catalyst Controllers - like many other frameworks - have several "well known" method names which are called periodically through the life cycle of the element, and let you observe when an element changes in various ways. Here is a comprehensive list of all life-cycle callbacks. Each one is suffixed `Callback`, to denote that it will be called by the framework. ### `connectedCallback()` The [`connectedCallback()` is part of Custom Elements][ce-callbacks], and gets fired as soon as your element is _appended_ to the DOM. This callback is a good time to initialize any variables, perhaps add some global event listeners, or start making any early network requests. JavaScript traditionally uses the `constructor()` callback to listen for class creation. While this still works for Custom Elements, it is best avoided as the element won't be in the DOM when `constructor()` is fired, limiting its utility. #### Things to remember The `connectedCallback` is called _as soon as_ the element is attached to a `document`. This _may_ occur _before_ an element has any children appended to it, so you should be careful not expect an element to have children during a `connectedCallback` call. This means avoiding checking any `target`s or using other methods like `querySelector`. Instead use this function to initialize itself and avoid doing initialization work which depend on children existing. If your element depends heavily on its children existing, consider adding a [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) in the `connectedCallback` to track when your elements children change. ### `attributeChangedCallback()` The [`attributeChangedCallback()` is part of Custom Elements][ce-callbacks], and gets fired when _observed attributes_ are added, changed, or removed from your element. It required you set a `static observedAttributes` array on your class, the values of which will be any attributes that will be observed for mutations. This is given a set of arguments, the signature of your function should be: ```typescript attributeChangedCallback(name: string, oldValue: string|null, newValue: string|null): void {} ``` #### Things to remember The `attributeChangedCallback` will fire whenever `setAttribute` is called with an observed attribute, even if the _new_ value is the same as the _old_ value. In other words, it is possible for `attributeChangedCallback` to be called when `oldValue === newValue`. In most cases this really won't matter much, and in some cases this is very helpful; but sometimes this can bite, especially if you have [non-idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_examples) code inside your `attributeChangedCallback`. Try to make sure operations inside `attributeChangedCallback` are idempotent, or perhaps consider adding a check to ensure `oldValue !== newValue` before performing operations which may be sensitive to this. ### `disconnectedCallback()` The [`disconnectedCallback()` is part of Custom Elements][ce-callbacks], and gets fired as soon as your element is _removed_ from the DOM. Event listeners will automatically be cleaned up, and memory will be freed automatically from JavaScript, so you're unlikely to need this callback for much. ### `adoptedCallback()` The [`adoptedCallback()` is part of Custom Elements][ce-callbacks], and gets called when your element moves from one `document` to another (such as an iframe). It's very unlikely to occur, you'll almost never need this. [ce-callbacks]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks ================================================ FILE: docs/_guide/patterns-2.md ================================================ --- version: 2 chapter: 14 title: Patterns subtitle: Best Practices for behaviours permalink: /guide-v2/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 throttling decorators for methods: ```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) } } ``` Alternatively, if you'd like more precise control over the exact way debouncing happens (for example you'd like to make the debounce timeout dynamic, or sometimes call _without_ debouncing), you can have two methods following the pattern of `foo`/`fooNow` or `foo`/`fooSync`, where the non-suffixed method dispatches asynchronously to the `Now`/`Sync` suffixed method, a little like this: ```typescript import {controller} from '@github/catalyst' @controller class FuzzySearchElement extends HTMLElement { #searchAnimationFrame = 0 search(event: Event) { clearAnimationFrame(this.#searchAnimationFrame) this.#searchAnimationFrame = requestAnimationFrame(() => this.searchNow(event: Event)) } searchNow(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, and 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. } } ``` ### Registering global or many event listeners Generally speaking, you'll want to use ["Actions"]({{ site.baseurl }}/guide/actions) to register event listeners with your Controller, but Actions only work for components nested within your Controller. It may also be necessary to listen for events on the Document, Window, or across well-known adjacent elements. We can manually call `addEventListener` for these types, including during the `connectedCallback` phase. Cleanup for `addEventListener` can be a bit error prone, but [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) can be useful here to pass a signal that the element is cleaning up. AbortControllers should be created once per `connectedCallback`, as they are not re-usable, while Controllers _can_ be reused. ```typescript @controller class UnsavedChangesElement extends HTMLElement { #eventAbortController: AbortController|null = null connectedCallback(event: Event) { // Create the new AbortController and get the new signal const {signal} = (this.#eventAbortController = new AbortController()) // You can `signal` as an option to any `addEventListener` call: window.addEventListener('hashchange', this, { signal }) window.addEventListener('blur', this, { signal }) window.addEventListener('popstate', this, { signal }) window.addEventListener('pagehide', this, { signal }) } disconnectedCallback() { // This will clean up any `addEventListener` calls which were given the `signal` this.#eventAbortController?.abort() } handleEvent(event) { // `handleEvent` will be called when each one of the event listeners // defined in `connectedCallback` is dispatched. } } ``` ================================================ FILE: docs/_guide/patterns.md ================================================ --- version: 1 chapter: 10 title: Patterns subtitle: Best Practices for behaviours --- 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 throttling decorators for methods: ```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) } } ``` Alternatively, if you'd like more precise control over the exact way debouncing happens (for example you'd like to make the debounce timeout dynamic, or sometimes call _without_ debouncing), you can have two methods following the pattern of `foo`/`fooNow` or `foo`/`fooSync`, where the non-suffixed method dispatches asynchronously to the `Now`/`Sync` suffixed method, a little like this: ```typescript import {controller} from '@github/catalyst' @controller class FuzzySearchElement extends HTMLElement { #searchAnimationFrame = 0 search(event: Event) { clearAnimationFrame(this.#searchAnimationFrame) this.#searchAnimationFrame = requestAnimationFrame(() => this.searchNow(event: Event)) } searchNow(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, and 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. } } ``` ### Registering global or many event listeners Generally speaking, you'll want to use ["Actions"]({{ site.baseurl }}/guide/actions) to register event listeners with your Controller, but Actions only work for components nested within your Controller. It may also be necessary to listen for events on the Document, Window, or across well-known adjacent elements. We can manually call `addEventListener` for these types, including during the `connectedCallback` phase. Cleanup for `addEventListener` can be a bit error prone, but [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) can be useful here to pass a signal that the element is cleaning up. AbortControllers should be created once per `connectedCallback`, as they are not re-usable, while Controllers _can_ be reused. ```typescript @controller class UnsavedChangesElement extends HTMLElement { #eventAbortController: AbortController|null = null connectedCallback(event: Event) { // Create the new AbortController and get the new signal const {signal} = (this.#eventAbortController = new AbortController()) // You can `signal` as an option to any `addEventListener` call: window.addEventListener('hashchange', this, { signal }) window.addEventListener('blur', this, { signal }) window.addEventListener('popstate', this, { signal }) window.addEventListener('pagehide', this, { signal }) } disconnectedCallback() { // This will clean up any `addEventListener` calls which were given the `signal` this.#eventAbortController?.abort() } handleEvent(event) { // `handleEvent` will be called when each one of the event listeners // defined in `connectedCallback` is dispatched. } } ``` ================================================ FILE: docs/_guide/providable.md ================================================ --- version: 2 chapter: 8 title: Providable subtitle: The Provider pattern permalink: /guide-v2/providable --- The [Provider pattern](https://www.patterns.dev/posts/provider-pattern/) allows for deeply nested children to ask ancestors for values. This can be useful for decoupling state inside a component, centralising it higher up in the DOM heirarchy. A top level container component might store values, and many children can consume those values, without having logic duplicated across the app. It's quite an abstract pattern so is better explained with examples... Say for example a set of your components are built to perform actions on a user, but need a User ID. One way to handle this is to set the User ID as an attribute on each element, but this can lead to a lot of duplication. Instead these actions can request the ID from a parent component, which can provide the User ID without creating an explicit relationship (which can lead to brittle code). The `@providable` ability allows a Catalyst controller to become a provider or consumer (or both) of one or many properties. To provide a property to nested controllers that ask for it, mark a property as `@provide` or `@provideAsync`. To consume a property from a parent, mark a property as `@consume`. Let's try implementing the user actions using `@providable`: ```typescript import {providable, consume, provide, controller} from '@github/catalyst' @controller @providable class BlockUser extends HTMLElement { // This will request `userId`, and default to '' if not provided. @consume userId = '' // This will request `userName`, and default to '' if not provided. @consume userName = '' async handleEvent() { if (confirm(`Would you like to block ${this.userName}?`)) { await fetch(`/users/${userId}/delete`) } } } @controller @providable class FollowUser extends HTMLElement { // This will request `userId`, and default to '' if not provided. @consume userId = '' // This will request `userName`, and default to '' if not provided. @consume userName = '' async handleEvent() { if (confirm(`Would you like to follow ${this.userName}?`)) { await fetch(`/users/${userId}/delete`) } } } @controller @providable class UserRow extends HTMLElement { // This will provide `userId` as '123' to any nested children that request it. @provide userId = '123' // This will provide `userName` as 'Alex' to any nested children that request it. @provide userName = 'Alex' } ``` ```html