├── .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 ├── Gemfile.lock ├── _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 │ ├── devtools-coverage.png │ ├── 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 ├── fonts │ ├── Inter-Bold.woff │ ├── Inter-Medium.woff │ └── Inter-Regular.woff ├── github-syntax.css ├── icons │ ├── icon-144x144.png │ ├── icon-192x192.png │ ├── icon-256x256.png │ ├── icon-384x384.png │ ├── icon-48x48.png │ ├── icon-512x512.png │ ├── icon-72x72.png │ └── icon-96x96.png ├── index.html ├── index.js ├── primer.css └── primer.css.map ├── lighthouserc.json ├── package-lock.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 /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/javascript-node/.devcontainer/base.Dockerfile 2 | 3 | # [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 4 | ARG VARIANT="16" 5 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 6 | 7 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 8 | && apt-get -y install --no-install-recommends bundler 9 | 10 | # [Optional] Uncomment if you want to install an additional version of node using nvm 11 | # ARG EXTRA_NODE_VERSION=10 12 | # RUN su node -c "source/usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 13 | 14 | # [Optional] Uncomment if you want to install more global node modules 15 | # RUN su node -c "npm install -g " 16 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/javascript-node 3 | { 4 | "name": "Node.js", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 16, 14, 12. 8 | // Append -bullseye or -buster to pin to an OS version. 9 | // Use -bullseye variants on local arm64/Apple Silicon. 10 | "args": { "VARIANT": "16" } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "dbaeumer.vscode-eslint" 19 | ], 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | "postCreateCommand": "npm i && cd docs && sudo bundle install", 26 | 27 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 28 | "remoteUser": "node", 29 | "features": { 30 | "git": "latest" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["github"], 4 | "extends": ["plugin:github/recommended", "plugin:github/typescript", "plugin:github/browser"], 5 | "rules": { 6 | "import/no-unresolved": "off", 7 | "github/no-inner-html": "off", 8 | "i18n-text/no-en": "off", 9 | "import/extensions": ["error", "ignorePackages"], 10 | "@typescript-eslint/consistent-type-imports": ["error", {"prefer": "type-imports"}] 11 | }, 12 | "overrides": [ 13 | { 14 | "files": "test/*", 15 | "rules": { 16 | "@typescript-eslint/no-empty-function": "off" 17 | }, 18 | "globals": { 19 | "chai": false, 20 | "expect": false 21 | }, 22 | "env": { 23 | "mocha": true 24 | } 25 | }, 26 | { 27 | "files": "*.cjs", 28 | "rules": { 29 | "import/no-commonjs": "off" 30 | }, 31 | "env": { 32 | "node": true 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/lighthouse.yml: -------------------------------------------------------------------------------- 1 | name: Lighthouse 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lhci: 7 | name: Lighthouse 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout the project 11 | uses: actions/checkout@v3 12 | 13 | - name: Use Node.js 16.x (LTS) 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 16.x 17 | cache: 'npm' 18 | - run: npm ci 19 | 20 | - name: Use Ruby 2.7.3 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: '2.7.3' 24 | bundler-cache: true 25 | working-directory: docs 26 | 27 | - name: Build docs 28 | run: npm run build:docs 29 | env: 30 | JEKYLL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Run Lighthouse CI 33 | run: npx @lhci/cli@0.9.x autorun 34 | env: 35 | JEKYLL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | LHCI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | test-node: 9 | name: Test on Node.js 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout the project 13 | uses: actions/checkout@v3 14 | - name: Use Node.js 16.x (LTS) 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16.x 18 | cache: 'npm' 19 | - run: npm ci 20 | - name: Lint Codebase 21 | run: npm run lint 22 | - name: Run Node.js Tests 23 | run: npm run test 24 | - name: Check Bundle Size 25 | run: npm run size 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout the project 12 | uses: actions/checkout@v3 13 | - name: Use Node.js 16.x (LTS) 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 16.x 17 | registry-url: https://registry.npmjs.org/ 18 | cache: npm 19 | - run: npm ci 20 | - run: npm test 21 | - run: npm version ${TAG_NAME} --git-tag-version=false 22 | env: 23 | TAG_NAME: ${{ github.event.release.tag_name }} 24 | - run: npm whoami; npm publish 25 | env: 26 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _site 3 | *.tsbuildinfo 4 | lib/ 5 | .jekyll-cache 6 | .lighthouseci 7 | coverage 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 13.11.0 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @github/primer-reviewers @koddsson 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@github.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: https://github.com/github/catalyst/fork 4 | [pr]: https://github.com/github/catalyst/compare 5 | [style]: https://github.com/styleguide/ruby 6 | [code-of-conduct]: CODE_OF_CONDUCT.md 7 | 8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 9 | 10 | 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). 11 | 12 | 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. 13 | 14 | ## Submitting a pull request 15 | 16 | 0. [Fork][fork] and clone the repository 17 | 0. Configure and install the dependencies: `npm i` 18 | 0. Make sure the tests pass on your machine: `npm t` 19 | 0. Create a new branch: `git checkout -b my-branch-name` 20 | 0. Make your change, add tests, and make sure the tests still pass 21 | 0. Push to your fork and [submit a pull request][pr] 22 | 0. Pat your self on the back and wait for your pull request to be reviewed and merged. 23 | 24 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 25 | 26 | - Follow the [style guide][style]. 27 | - Write tests. 28 | - 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. 29 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 30 | 31 | ## Resources 32 | 33 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 34 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 35 | - [GitHub Help](https://help.github.com) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GitHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Catalyst 2 | 3 | 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. 4 | 5 | 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). 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | If you discover a security issue in this repository, please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github). 2 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | 18 | 19 |
20 |

404

21 | 22 |

Page not found :(

23 |

The requested page could not be found.

24 |
25 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'jekyll' 6 | 7 | group :jekyll_plugins do 8 | gem 'jekyll-commonmark-ghpages' 9 | gem 'jekyll-github-metadata' 10 | gem 'jekyll-gzip' 11 | end 12 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | colorator (1.1.0) 7 | commonmarker (0.23.7) 8 | concurrent-ruby (1.1.10) 9 | em-websocket (0.5.3) 10 | eventmachine (>= 0.12.9) 11 | http_parser.rb (~> 0) 12 | eventmachine (1.2.7) 13 | faraday (1.10.0) 14 | faraday-em_http (~> 1.0) 15 | faraday-em_synchrony (~> 1.0) 16 | faraday-excon (~> 1.1) 17 | faraday-httpclient (~> 1.0) 18 | faraday-multipart (~> 1.0) 19 | faraday-net_http (~> 1.0) 20 | faraday-net_http_persistent (~> 1.0) 21 | faraday-patron (~> 1.0) 22 | faraday-rack (~> 1.0) 23 | faraday-retry (~> 1.0) 24 | ruby2_keywords (>= 0.0.4) 25 | faraday-em_http (1.0.0) 26 | faraday-em_synchrony (1.0.0) 27 | faraday-excon (1.1.0) 28 | faraday-httpclient (1.0.1) 29 | faraday-multipart (1.0.3) 30 | multipart-post (>= 1.2, < 3) 31 | faraday-net_http (1.0.1) 32 | faraday-net_http_persistent (1.2.0) 33 | faraday-patron (1.0.0) 34 | faraday-rack (1.0.0) 35 | faraday-retry (1.0.3) 36 | ffi (1.15.5) 37 | forwardable-extended (2.6.0) 38 | http_parser.rb (0.8.0) 39 | i18n (0.9.5) 40 | concurrent-ruby (~> 1.0) 41 | jekyll (3.9.2) 42 | addressable (~> 2.4) 43 | colorator (~> 1.0) 44 | em-websocket (~> 0.5) 45 | i18n (~> 0.7) 46 | jekyll-sass-converter (~> 1.0) 47 | jekyll-watch (~> 2.0) 48 | kramdown (>= 1.17, < 3) 49 | liquid (~> 4.0) 50 | mercenary (~> 0.3.3) 51 | pathutil (~> 0.9) 52 | rouge (>= 1.7, < 4) 53 | safe_yaml (~> 1.0) 54 | jekyll-commonmark (1.4.0) 55 | commonmarker (~> 0.22) 56 | jekyll-commonmark-ghpages (0.2.0) 57 | commonmarker (~> 0.23.4) 58 | jekyll (~> 3.9.0) 59 | jekyll-commonmark (~> 1.4.0) 60 | rouge (>= 2.0, < 4.0) 61 | jekyll-github-metadata (2.14.0) 62 | jekyll (>= 3.4, < 5.0) 63 | octokit (~> 4.0, != 4.4.0) 64 | jekyll-gzip (2.5.1) 65 | jekyll (>= 3.0, < 5.0) 66 | jekyll-sass-converter (1.5.2) 67 | sass (~> 3.4) 68 | jekyll-watch (2.2.1) 69 | listen (~> 3.0) 70 | kramdown (2.4.0) 71 | rexml 72 | liquid (4.0.3) 73 | listen (3.7.1) 74 | rb-fsevent (~> 0.10, >= 0.10.3) 75 | rb-inotify (~> 0.9, >= 0.9.10) 76 | mercenary (0.3.6) 77 | multipart-post (2.1.1) 78 | octokit (4.22.0) 79 | faraday (>= 0.9) 80 | sawyer (~> 0.8.0, >= 0.5.3) 81 | pathutil (0.16.2) 82 | forwardable-extended (~> 2.6) 83 | public_suffix (4.0.7) 84 | rb-fsevent (0.11.1) 85 | rb-inotify (0.10.1) 86 | ffi (~> 1.0) 87 | rexml (3.2.5) 88 | rouge (3.28.0) 89 | ruby2_keywords (0.0.5) 90 | safe_yaml (1.0.5) 91 | sass (3.7.4) 92 | sass-listen (~> 4.0.0) 93 | sass-listen (4.0.0) 94 | rb-fsevent (~> 0.9, >= 0.9.4) 95 | rb-inotify (~> 0.9, >= 0.9.7) 96 | sawyer (0.8.2) 97 | addressable (>= 2.3.5) 98 | faraday (> 0.8, < 2.0) 99 | 100 | PLATFORMS 101 | x86_64-linux 102 | 103 | DEPENDENCIES 104 | jekyll 105 | jekyll-commonmark-ghpages 106 | jekyll-github-metadata 107 | jekyll-gzip 108 | 109 | BUNDLED WITH 110 | 2.3.13 111 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Catalyst 2 | 3 | markdown: CommonMarkGhPages 4 | 5 | commonmark: 6 | extensions: ['autolink', 'table'] 7 | 8 | permalink: pretty 9 | 10 | exclude: 11 | - Gemfile 12 | - Gemfile.lock 13 | - node_modules 14 | - vendor 15 | 16 | collections: 17 | guide: 18 | output: true 19 | 20 | defaults: 21 | - scope: 22 | type: guide 23 | values: 24 | layout: guide 25 | 26 | repository: github/catalyst 27 | 28 | plugins: 29 | - 'jekyll-github-metadata' 30 | - 'jekyll-gzip' 31 | -------------------------------------------------------------------------------- /docs/_guide/abilities.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 4 4 | title: Abilities 5 | subtitle: Abilities 6 | permalink: /guide-v2/abilities 7 | --- 8 | 9 | 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. 10 | 11 | ### Using Abilities 12 | 13 | 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): 14 | 15 | ```typescript 16 | import {actionable} from '@github/catalyst' 17 | 18 | @actionable 19 | class HelloWorld extends HTMLElement { 20 | } 21 | ``` 22 | 23 | ### Using Marks 24 | 25 | 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: 26 | 27 | ```typescript 28 | import {targetable, target, targets} from '@github/catalyst' 29 | 30 | @targetable 31 | class HelloWorldElement extends HTMLElement { 32 | @target name 33 | @targets people 34 | } 35 | ``` 36 | 37 | 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. 38 | 39 | ### Built-In Abilities 40 | 41 | Catalyst ships with a set of built in abilities. The `@controller` decorator applies the following built-in abilities: 42 | 43 | - `controllable` - the base ability which other abilities require for functionality. 44 | - `targetable` - the ability to define `@target` and `@targets` properties. See [Targets]({{ site.baseurl }}/guide/targets) for more. 45 | - `actionable` - the ability to automatically bind events based on `data-action` attributes. See [Actions]({{ site.baseurl }}/guide/actions) for more. 46 | - `attrable` - the ability to define `@attr`s. See [Attrs]({{ site.baseurl }}/guide/attrs) for more. 47 | 48 | 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". 49 | 50 | The following abilities are shipped with Catalyst but require manually applying as they aren't considered critical functionality: 51 | 52 | - `providable` - the ability to define `provider` and `consumer` properties. See [Providable]({{ site.baseurl }}/guide/providable) for more. 53 | 54 | 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! 55 | -------------------------------------------------------------------------------- /docs/_guide/actions-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 6 4 | title: Actionable 5 | subtitle: Binding Events 6 | permalink: /guide-v2/actions 7 | --- 8 | 9 | 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`. 10 | 11 | {% capture callout %} 12 | Remember! Actions are _automatically_ bound using the `@controller` decorator. There's no extra JavaScript code needed. 13 | {% endcapture %}{% include callout.md %} 14 | 15 | ### Example 16 | 17 |
18 |
19 | 20 | 23 | 24 | ```html 25 | 26 | 30 | 31 | 35 | 36 | 38 | 39 | 40 | ``` 41 | 42 |
43 |
44 | 45 | 48 | 49 | ```js 50 | import { controller, target } from "@github/catalyst" 51 | 52 | @controller 53 | class HelloWorldElement extends HTMLElement { 54 | @target name: HTMLElement 55 | @target output: HTMLElement 56 | 57 | greetSomeone() { 58 | this.output.textContent = 59 | `Hello, ${this.name.value}!` 60 | } 61 | } 62 | ``` 63 | 64 |
65 |
66 | 67 | ### Actions Syntax 68 | 69 | The actions syntax follows a pattern of `event:controller#method`. 70 | 71 | - `event` must be the name of a [_DOM Event_](https://developer.mozilla.org/en-US/docs/Web/Events), e.g. `click`. 72 | - `controller` must be the name of a controller ascendant to the element. 73 | - `method` (optional) must be a _public_ _method_ attached to a controller's prototype. Static methods will not work. 74 | 75 | If method is not supplied, it will default to `handleEvent`. 76 | 77 | Some examples of Actions Syntax: 78 | 79 | - `click:my-element#foo` -> `click` events will call `foo` on `my-element` elements. 80 | - `submit:my-element#foo` -> `submit` events will call `foo` on `my-element` elements. 81 | - `click:user-list` -> `click` events will call `handleEvent` on `user-list` elements. 82 | - `click:user-list#` -> `click` events will call `handleEvent` on `user-list` elements. 83 | - `click:top-header-user-profile#` -> `click` events will call `handleEvent` on `top-header-user-profile` elements. 84 | - `nav:keydown:user-list` -> `navigation:keydown` events will call `handleEvent` on `user-list` elements. 85 | 86 | ### Multiple Actions 87 | 88 | Multiple actions can be bound to multiple events, methods, and controllers. For example: 89 | 90 | 93 | 94 | ```html 95 | 96 | 97 | 106 | 107 | 116 | 117 | 118 | ``` 119 | 120 | ### Custom Events 121 | 122 | 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: 123 | 124 | 127 | 128 | ```html 129 | 130 | 131 | 132 | 133 | 134 | ``` 135 | 136 | 140 | 141 | ```js 142 | import {controller} from '@github/catalyst' 143 | 144 | @controller 145 | class LazyLoader extends HTMLElement { 146 | 147 | connectedCallback() { 148 | this.innerHTML = await (await fetch(this.dataset.url)).text() 149 | this.dispatchEvent(new CustomEvent('loaded')) 150 | } 151 | 152 | } 153 | 154 | @controller 155 | class HoverCard extends HTMLElement { 156 | 157 | enable() { 158 | this.disabled = false 159 | } 160 | 161 | } 162 | ``` 163 | 164 | ### Targets and "ShadowRoots" 165 | 166 | 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. 167 | 168 | ### What about without Decorators? 169 | 170 | If you're using decorators, then the `@controller` decorator automatically handles binding of actions to a Controller. 171 | 172 | If you're not using decorators, then you'll need to call `bind(this)` somewhere inside of `connectedCallback()`. 173 | 174 | ```js 175 | import {bind} from '@github/catalyst' 176 | 177 | class HelloWorldElement extends HTMLElement { 178 | connectedCallback() { 179 | bind(this) 180 | } 181 | } 182 | ``` 183 | 184 | ### Binding dynamically added actions 185 | 186 | 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. 187 | 188 | ```js 189 | import {listenForBind} from '@github/catalyst' 190 | 191 | @controller 192 | class HelloWorldElement extends HTMLElement { 193 | @target iframe: HTMLIFrameElement 194 | 195 | connectedCallback() { 196 | // listenForBind(this.ownerDocument) is automatically called. 197 | 198 | listenForBind(this.iframe.document.body) 199 | } 200 | } 201 | ``` 202 | -------------------------------------------------------------------------------- /docs/_guide/conventions-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 13 4 | title: Conventions 5 | subtitle: Common naming and patterns 6 | permalink: /guide-v2/conventions 7 | --- 8 | 9 | Catalyst strives for convention over code. Here are a few conventions we recommend when writing Catalyst code: 10 | 11 | ### Suffix your controllers consistently, for symmetry 12 | 13 | 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. 14 | 15 | ```typescript 16 | @controller 17 | class UserListElement extends HTMLElement {} // `` 18 | ``` 19 | 20 | ```typescript 21 | @controller 22 | class UserListComponent extends HTMLElement {} // `` 23 | ``` 24 | 25 | ### The best class-names are two word descriptions 26 | 27 | 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: 28 | 29 | - `theme-picker` (`class ThemePickerElement`) 30 | - `markdown-toolbar` (`class MarkdownToolbarElement`) 31 | - `user-list` (`class UserListElement`) 32 | - `content-pager` (`class ContentPagerElement`) 33 | - `image-gallery` (`class ImageGalleryElement`) 34 | 35 | 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?). 36 | 37 | ### Keep class-names short (but not too short) 38 | 39 | 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. 40 | 41 | 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! 42 | 43 | ### Method names should describe what they do 44 | 45 | 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`. 46 | 47 | ### `@target` should use singular naming, while `@targets` should use plural 48 | 49 | 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`). 50 | 51 | -------------------------------------------------------------------------------- /docs/_guide/conventions.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | chapter: 9 4 | title: Conventions 5 | subtitle: Common naming and patterns 6 | --- 7 | 8 | Catalyst strives for convention over code. Here are a few conventions we recommend when writing Catalyst code: 9 | 10 | ### Use `Element` to suffix your controller class 11 | 12 | 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. 13 | 14 | ```typescript 15 | @controller 16 | class UserListElement extends HTMLElement {} 17 | ``` 18 | 19 | ### The best class-names are two word descriptions 20 | 21 | 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: 22 | 23 | - `theme-picker` (`class ThemePickerElement`) 24 | - `markdown-toolbar` (`class MarkdownToolbarElement`) 25 | - `user-list` (`class UserListElement`) 26 | - `content-pager` (`class ContentPagerElement`) 27 | - `image-gallery` (`class ImageGalleryElement`) 28 | 29 | 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?). 30 | 31 | ### Keep class-names short (but not too short) 32 | 33 | 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. 34 | 35 | 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! 36 | 37 | ### Method names should describe what they do 38 | 39 | 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`. 40 | 41 | ### `@target` should use singular naming, while `@targets` should use plural 42 | 43 | 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`). 44 | -------------------------------------------------------------------------------- /docs/_guide/decorators-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 3 4 | title: Decorators 5 | subtitle: Using TypeScript for ergonomics 6 | permalink: /guide-v2/decorators 7 | --- 8 | 9 | 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)). 10 | 11 | You can read more about [decorators in the TypeScript handbook](https://www.typescriptlang.org/docs/handbook/decorators.html), but here's quick guide: 12 | 13 | Decorators can be used three ways: 14 | 15 | ### Class Decorators 16 | 17 | Catalyst comes with the `@controller` decorator. This gets put on top of the class, like so: 18 | 19 | ```js 20 | @controller 21 | class HelloWorldElement extends HTMLElement {} 22 | ``` 23 | 24 | ### Class Field Decorators 25 | 26 | 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: 27 | 28 | ```js 29 | class HelloWorldElement extends HTMLElement { 30 | 31 | @target something 32 | 33 | // Alternative style 34 | @targets 35 | others 36 | 37 | } 38 | ``` 39 |
40 | 41 | 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. 42 | 43 | ### Method Decorators 44 | 45 | Method decorators work just like Field Decorators. Put them on top or on the left of the method, like so: 46 | 47 | 48 | ```js 49 | class HelloWorldElement extends HTMLElement { 50 | 51 | @log 52 | submit() { 53 | // ... 54 | } 55 | 56 | // Alternative style 57 | 58 | @log load() { 59 | // ... 60 | } 61 | 62 | } 63 | ``` 64 | 65 | ### Getter/Setter 66 | 67 | 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: 68 | 69 | 70 | ```js 71 | class HelloWorldElement extends HTMLElement { 72 | 73 | @target set something() { 74 | // ... 75 | } 76 | 77 | // Can be used over just one field 78 | @attr get data() { 79 | return {} 80 | } 81 | set data() { 82 | 83 | } 84 | } 85 | ``` 86 | 87 | ### Supporting `strictPropertyInitialization` 88 | 89 | 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: 90 | 91 | 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: 92 | 93 | ```typescript 94 | class HelloWorldElement extends HTMLElement { 95 | @target declare something: HTMLElement 96 | @targets declare items: HTMLElement[] 97 | } 98 | ``` 99 | 100 | 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. 101 | 102 | 2. You can also disable the compiler option (other strict mode rules can still apply) in your `tsconfig.json` like so: 103 | 104 | ```json 105 | { 106 | "compilerOptions": { 107 | "strict": true, 108 | "strictPropertyInitialization": false 109 | } 110 | } 111 | ``` 112 | 113 | ### Function Calling Decorators 114 | 115 | 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. 116 | 117 | 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: 118 | 119 | ```js 120 | class HelloWorldElement extends HTMLElement { 121 | 122 | @debounce(100) 123 | handleInput() { 124 | // ... 125 | } 126 | 127 | } 128 | ``` 129 |
130 | -------------------------------------------------------------------------------- /docs/_guide/decorators.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | chapter: 3 4 | title: Decorators 5 | subtitle: Using TypeScript for ergonomics 6 | --- 7 | 8 | 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)). 9 | 10 | You can read more about [decorators in the TypeScript handbook](https://www.typescriptlang.org/docs/handbook/decorators.html), but here's quick guide: 11 | 12 | Decorators can be used three ways: 13 | 14 | ### Class Decorators 15 | 16 | Catalyst comes with the `@controller` decorator. This gets put on top of the class, like so: 17 | 18 | ```js 19 | @controller 20 | class HelloWorldElement extends HTMLElement {} 21 | ``` 22 | 23 | ### Class Field Decorators 24 | 25 | 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: 26 | 27 | ```js 28 | class HelloWorldElement extends HTMLElement { 29 | 30 | @target something 31 | 32 | // Alternative style 33 | @targets 34 | others 35 | 36 | } 37 | ``` 38 |
39 | 40 | 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. 41 | 42 | ### Method Decorators 43 | 44 | Method decorators work just like Field Decorators. Put them on top or on the left of the method, like so: 45 | 46 | 47 | ```js 48 | class HelloWorldElement extends HTMLElement { 49 | 50 | @log 51 | submit() { 52 | // ... 53 | } 54 | 55 | // Alternative style 56 | 57 | @log load() { 58 | // ... 59 | } 60 | 61 | } 62 | ``` 63 | 64 | ### Getter/Setter 65 | 66 | 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: 67 | 68 | 69 | ```js 70 | class HelloWorldElement extends HTMLElement { 71 | 72 | @target set something() { 73 | // ... 74 | } 75 | 76 | // Can be used over just one field 77 | @attr get data() { 78 | return {} 79 | } 80 | set data() { 81 | 82 | } 83 | } 84 | ``` 85 | 86 | ### Supporting `strictPropertyInitialization` 87 | 88 | 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: 89 | 90 | 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: 91 | 92 | ```typescript 93 | class HelloWorldElement extends HTMLElement { 94 | @target declare something: HTMLElement 95 | @targets declare items: HTMLElement[] 96 | } 97 | ``` 98 | 99 | 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. 100 | 101 | 2. You can also disable the compiler option (other strict mode rules can still apply) in your `tsconfig.json` like so: 102 | 103 | ```json 104 | { 105 | "compilerOptions": { 106 | "strict": true, 107 | "strictPropertyInitialization": false 108 | } 109 | } 110 | ``` 111 | 112 | ### Function Calling Decorators 113 | 114 | 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. 115 | 116 | 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: 117 | 118 | ```js 119 | class HelloWorldElement extends HTMLElement { 120 | 121 | @debounce(100) 122 | handleInput() { 123 | // ... 124 | } 125 | 126 | } 127 | ``` 128 |
129 | -------------------------------------------------------------------------------- /docs/_guide/devtools-coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/catalyst/ee0ea2c5adcdf2a0708b7f415447923a9432befa/docs/_guide/devtools-coverage.png -------------------------------------------------------------------------------- /docs/_guide/introduction-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 1 4 | title: Introduction 5 | subtitle: Origins & Concepts 6 | permalink: /guide-v2/introduction 7 | --- 8 | 9 | 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. 10 | 11 | ## How did we get here? 12 | 13 | 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/). 14 | 15 | 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. 16 | 17 | 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. 18 | 19 | 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. 20 | 21 | ## Three core concepts: Observe, Listen, Query 22 | 23 | Catalyst takes these three core concepts and delivers them in the lightest possible way they can be delivered. 24 | 25 | - **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). 26 | 27 | - **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). 28 | 29 | - **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). 30 | -------------------------------------------------------------------------------- /docs/_guide/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | chapter: 1 4 | title: Introduction 5 | subtitle: Origins & Concepts 6 | --- 7 | 8 | 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. 9 | 10 | ## How did we get here? 11 | 12 | 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/). 13 | 14 | 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. 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | ## Three core concepts: Observe, Listen, Query 21 | 22 | Catalyst takes these three core concepts and delivers them in the lightest possible way they can be delivered. 23 | 24 | - **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). 25 | 26 | - **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). 27 | 28 | - **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). 29 | -------------------------------------------------------------------------------- /docs/_guide/lazy-elements-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 16 4 | title: Lazy Elements 5 | subtitle: Dynamically load elements just in time 6 | permalink: /guide-v2/lazy-elements 7 | --- 8 | 9 | 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. 10 | 11 | ![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) 12 | 13 | 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. 14 | 15 | ```typescript 16 | import {lazyDefine} from '@github/catalyst' 17 | 18 | // Dynamically import the Catalyst controller when the `` tag is seen. 19 | lazyDefine('user-avatar', () => import('./components/user-avatar')) 20 | ``` 21 | 22 | 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. 23 | 24 | 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. 25 | 26 | 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: 27 | 28 | - `` (default) 29 | - 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. 30 | - `` 31 | - This element is loaded on the first user interaction with the page. This listens for `mousedown`, `touchstart`, `pointerdown` and `keydown` events on `document`. 32 | - `` 33 | - This element is loaded when it's close to being visible. Similar to `` . The functionality is driven by an `IntersectionObserver`. 34 | 35 | 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. 36 | -------------------------------------------------------------------------------- /docs/_guide/lazy-elements.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | chapter: 13 4 | title: Lazy Elements 5 | subtitle: Dynamically load elements just in time 6 | --- 7 | 8 | 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. 9 | 10 | ![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) 11 | 12 | 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. 13 | 14 | ```typescript 15 | import {lazyDefine} from '@github/catalyst' 16 | 17 | // Dynamically import the Catalyst controller when the `` tag is seen. 18 | lazyDefine('user-avatar', () => import('./components/user-avatar')) 19 | ``` 20 | 21 | 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. 22 | 23 | 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. 24 | 25 | 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: 26 | 27 | - `` (default) 28 | - 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. 29 | - `` 30 | - This element is loaded on the first user interaction with the page. This listens for `mousedown`, `touchstart`, `pointerdown` and `keydown` events on `document`. 31 | - `` 32 | - This element is loaded when it's close to being visible. Similar to `` . The functionality is driven by an `IntersectionObserver`. 33 | 34 | 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. 35 | -------------------------------------------------------------------------------- /docs/_guide/lifecycle-hooks-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 10 4 | title: Lifecycle Hooks 5 | subtitle: Observing the life cycle of an element 6 | permalink: /guide-v2/lifecycle-hooks 7 | --- 8 | 9 | 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. 10 | 11 | ### `connectedCallback()` 12 | 13 | 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. 14 | 15 | 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. 16 | 17 | #### Things to remember 18 | 19 | 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. 20 | 21 | 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. 22 | 23 | ### `attributeChangedCallback()` 24 | 25 | 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: 26 | 27 | ```typescript 28 | attributeChangedCallback(name: string, oldValue: string|null, newValue: string|null): void {} 29 | ``` 30 | 31 | #### Things to remember 32 | 33 | 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. 34 | 35 | ### `disconnectedCallback()` 36 | 37 | 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. 38 | 39 | ### `adoptedCallback()` 40 | 41 | 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. 42 | 43 | [ce-callbacks]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks 44 | -------------------------------------------------------------------------------- /docs/_guide/lifecycle-hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | chapter: 7 4 | title: Lifecycle Hooks 5 | subtitle: Observing the life cycle of an element 6 | --- 7 | 8 | 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. 9 | 10 | ### `connectedCallback()` 11 | 12 | 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. 13 | 14 | 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. 15 | 16 | #### Things to remember 17 | 18 | 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. 19 | 20 | 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. 21 | 22 | ### `attributeChangedCallback()` 23 | 24 | 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: 25 | 26 | ```typescript 27 | attributeChangedCallback(name: string, oldValue: string|null, newValue: string|null): void {} 28 | ``` 29 | 30 | #### Things to remember 31 | 32 | 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. 33 | 34 | ### `disconnectedCallback()` 35 | 36 | 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. 37 | 38 | ### `adoptedCallback()` 39 | 40 | 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. 41 | 42 | [ce-callbacks]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks 43 | -------------------------------------------------------------------------------- /docs/_guide/patterns-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 14 4 | title: Patterns 5 | subtitle: Best Practices for behaviours 6 | permalink: /guide-v2/patterns 7 | --- 8 | 9 | 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. 10 | 11 | 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: 12 | 13 | ### Debouncing or Throttling events 14 | 15 | 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: 16 | 17 | ```typescript 18 | import {controller} from '@github/catalyst' 19 | import {debounce} from '@github/mini-throttle/decorators' 20 | 21 | @controller 22 | class FuzzySearchElement extends HTMLElement { 23 | 24 | // Adding `@debounce(100)` here means this method will only be called once in a 100ms period. 25 | @debounce(100) 26 | search(event: Event) { 27 | const value = event.currentTarget.value 28 | // This function is very computationally intensive, so we should run it as little as possible 29 | this.filterAllItemsWithValue(value) 30 | } 31 | 32 | } 33 | ``` 34 | 35 | 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: 36 | 37 | ```typescript 38 | import {controller} from '@github/catalyst' 39 | 40 | @controller 41 | class FuzzySearchElement extends HTMLElement { 42 | 43 | #searchAnimationFrame = 0 44 | search(event: Event) { 45 | clearAnimationFrame(this.#searchAnimationFrame) 46 | this.#searchAnimationFrame = requestAnimationFrame(() => this.searchNow(event: Event)) 47 | } 48 | 49 | searchNow(event: Event) { 50 | const value = event.currentTarget.value 51 | // This function is very computationally intensive, so we should run it as little as possible 52 | this.filterAllItemsWithValue(value) 53 | } 54 | 55 | } 56 | ``` 57 | 58 | ### Aborting Network Requests 59 | 60 | 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). 61 | 62 | ```typescript 63 | @controller 64 | class RemoveSearchElement extends HTMLElement { 65 | 66 | #remoteSearchController: AbortController|null 67 | 68 | async search(event: Event) { 69 | // Abort the old Request 70 | this.#remoteSearchController?.abort() 71 | 72 | // To start making a new request, construct an AbortController 73 | const {signal} = (this.#remoteSearchController = new AbortController()) 74 | 75 | try { 76 | const res = await fetch(myUrl, {signal}) 77 | 78 | // ... Add logic here with the completed network response 79 | } catch (e) { 80 | 81 | // ... Add logic here if you need to report a failed network request. 82 | // Do not rethrow for network errors! 83 | 84 | } 85 | 86 | if (signal.aborted) { 87 | // Here you can add logic for if the request was cancelled, but 88 | // usually what you want to do is just return early to avoid 89 | // cleaning up the loading UI (bear in mind if the request is 90 | // cancelled then another one will be in its place). 91 | return 92 | } 93 | 94 | // ... Add cleanup logic here, such as removing `loading` classes. 95 | 96 | } 97 | } 98 | ``` 99 | 100 | ### Registering global or many event listeners 101 | 102 | 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. 103 | 104 | 105 | ```typescript 106 | @controller 107 | class UnsavedChangesElement extends HTMLElement { 108 | 109 | #eventAbortController: AbortController|null = null 110 | 111 | connectedCallback(event: Event) { 112 | // Create the new AbortController and get the new signal 113 | const {signal} = (this.#eventAbortController = new AbortController()) 114 | 115 | // You can `signal` as an option to any `addEventListener` call: 116 | window.addEventListener('hashchange', this, { signal }) 117 | window.addEventListener('blur', this, { signal }) 118 | window.addEventListener('popstate', this, { signal }) 119 | window.addEventListener('pagehide', this, { signal }) 120 | } 121 | 122 | disconnectedCallback() { 123 | // This will clean up any `addEventListener` calls which were given the `signal` 124 | this.#eventAbortController?.abort() 125 | } 126 | 127 | handleEvent(event) { 128 | // `handleEvent` will be called when each one of the event listeners 129 | // defined in `connectedCallback` is dispatched. 130 | } 131 | } 132 | ``` 133 | -------------------------------------------------------------------------------- /docs/_guide/patterns.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | chapter: 10 4 | title: Patterns 5 | subtitle: Best Practices for behaviours 6 | --- 7 | 8 | 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. 9 | 10 | 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: 11 | 12 | ### Debouncing or Throttling events 13 | 14 | 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: 15 | 16 | ```typescript 17 | import {controller} from '@github/catalyst' 18 | import {debounce} from '@github/mini-throttle/decorators' 19 | 20 | @controller 21 | class FuzzySearchElement extends HTMLElement { 22 | 23 | // Adding `@debounce(100)` here means this method will only be called once in a 100ms period. 24 | @debounce(100) 25 | search(event: Event) { 26 | const value = event.currentTarget.value 27 | // This function is very computationally intensive, so we should run it as little as possible 28 | this.filterAllItemsWithValue(value) 29 | } 30 | 31 | } 32 | ``` 33 | 34 | 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: 35 | 36 | ```typescript 37 | import {controller} from '@github/catalyst' 38 | 39 | @controller 40 | class FuzzySearchElement extends HTMLElement { 41 | 42 | #searchAnimationFrame = 0 43 | search(event: Event) { 44 | clearAnimationFrame(this.#searchAnimationFrame) 45 | this.#searchAnimationFrame = requestAnimationFrame(() => this.searchNow(event: Event)) 46 | } 47 | 48 | searchNow(event: Event) { 49 | const value = event.currentTarget.value 50 | // This function is very computationally intensive, so we should run it as little as possible 51 | this.filterAllItemsWithValue(value) 52 | } 53 | 54 | } 55 | ``` 56 | 57 | ### Aborting Network Requests 58 | 59 | 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). 60 | 61 | ```typescript 62 | @controller 63 | class RemoveSearchElement extends HTMLElement { 64 | 65 | #remoteSearchController: AbortController|null 66 | 67 | async search(event: Event) { 68 | // Abort the old Request 69 | this.#remoteSearchController?.abort() 70 | 71 | // To start making a new request, construct an AbortController 72 | const {signal} = (this.#remoteSearchController = new AbortController()) 73 | 74 | try { 75 | const res = await fetch(myUrl, {signal}) 76 | 77 | // ... Add logic here with the completed network response 78 | } catch (e) { 79 | 80 | // ... Add logic here if you need to report a failed network request. 81 | // Do not rethrow for network errors! 82 | 83 | } 84 | 85 | if (signal.aborted) { 86 | // Here you can add logic for if the request was cancelled, but 87 | // usually what you want to do is just return early to avoid 88 | // cleaning up the loading UI (bear in mind if the request is 89 | // cancelled then another one will be in its place). 90 | return 91 | } 92 | 93 | // ... Add cleanup logic here, such as removing `loading` classes. 94 | 95 | } 96 | } 97 | ``` 98 | 99 | ### Registering global or many event listeners 100 | 101 | 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. 102 | 103 | 104 | ```typescript 105 | @controller 106 | class UnsavedChangesElement extends HTMLElement { 107 | 108 | #eventAbortController: AbortController|null = null 109 | 110 | connectedCallback(event: Event) { 111 | // Create the new AbortController and get the new signal 112 | const {signal} = (this.#eventAbortController = new AbortController()) 113 | 114 | // You can `signal` as an option to any `addEventListener` call: 115 | window.addEventListener('hashchange', this, { signal }) 116 | window.addEventListener('blur', this, { signal }) 117 | window.addEventListener('popstate', this, { signal }) 118 | window.addEventListener('pagehide', this, { signal }) 119 | } 120 | 121 | disconnectedCallback() { 122 | // This will clean up any `addEventListener` calls which were given the `signal` 123 | this.#eventAbortController?.abort() 124 | } 125 | 126 | handleEvent(event) { 127 | // `handleEvent` will be called when each one of the event listeners 128 | // defined in `connectedCallback` is dispatched. 129 | } 130 | } 131 | ``` 132 | -------------------------------------------------------------------------------- /docs/_guide/rendering-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | chapter: 11 4 | title: Rendering 5 | subtitle: Rendering HTML subtrees 6 | permalink: /guide-v2/rendering 7 | --- 8 | 9 | Sometimes it's necessary to render an HTML subtree as part of a component. This can be especially useful if a component is driving complex UI that is only interactive with JS. 10 | 11 | {% capture callout %} 12 | Remember to _always_ make your JavaScript progressively enhanced, where possible. Using JS to render large portions of the UI, that could be rendered server-side is an anti-pattern; it can be difficult for users to interact with - especially users who disable JS, or when JS fails to load, or those using assistive technologies. Rendering on the client can also impact the [CLS Web Vital](https://web.dev/cls/). 13 | {% endcapture %}{% include callout.md %} 14 | 15 | By leveraging the native [`ShadowDOM`](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) feature, Catalyst components can render complex sub-trees, fully encapsulated from the rest of the page. 16 | 17 | [Actions]({{ site.baseurl }}/guide/actions) and [Targets]({{ site.baseurl }}/guide/targets) all work within an elements ShadowRoot. 18 | 19 | You can also leverage the [declarative shadow DOM](https://web.dev/declarative-shadow-dom/) and render a template inline to your HTML, which will automatically be attached (this may require a polyfill for browsers which are yet to support this feature). 20 | 21 | ### Example 22 | 23 | ```html 24 | 25 | 30 | 31 | ``` 32 | ```typescript 33 | import { controller, target } from "@github/catalyst" 34 | 35 | @controller 36 | class HelloWorldElement extends HTMLElement { 37 | @target nameEl: HTMLElement 38 | get name() { 39 | return this.nameEl.textContent 40 | } 41 | set name(value: string) { 42 | this.nameEl.textContent = value 43 | } 44 | } 45 | ``` 46 | 47 | {% capture callout %} 48 | Remember that _all_ instances of your controller _must_ add the `