diff --git a/.changeset/config.json b/.changeset/config.json index c03035a239..8856c6591c 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,14 +1,17 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", "changelog": [ - "@svitejs/changesets-changelog-github-compact", - { "repo": "TanStack/table" } + "@changesets/changelog-github", + { "repo": "TanStack/table", "disableThanks": true } ], "commit": false, "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "fixed": [], - "linked": [], - "ignore": [] + "linked": [["@tanstack/*"]], + "ignore": [], + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + } } diff --git a/.gitattributes b/.gitattributes index dfe0770424..5a0d5e480b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ # Auto detect text files and perform LF normalization -* text=auto +* text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..ec37670885 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +.github/ @TanStack/tanstack-core +.nx/ @TanStack/tanstack-core +nx.json @TanStack/tanstack-core +.changeset/config.json @TanStack/tanstack-core +scripts/ @TanStack/tanstack-core +.npmrc @TanStack/tanstack-core \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 6a5ed2d4a4..963fe444ec 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Feature Requests & Questions + - name: 🤔 Feature Requests & Questions url: https://github.com/TanStack/table/discussions about: Please ask and answer questions here. - - name: Community Chat + - name: 💬 Community Chat url: https://discord.gg/mQd7egN about: A dedicated discord server hosted by TanStack + - name: 🦋 TanStack Bluesky + url: https://bsky.app/profile/tanstack.com + about: Stay up to date with new releases of our libraries diff --git a/.github/pull_request_template b/.github/pull_request_template new file mode 100644 index 0000000000..2c10bc7d7d --- /dev/null +++ b/.github/pull_request_template @@ -0,0 +1,8 @@ +## 🎯 Changes + + + +## ✅ Checklist + +- [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/table/blob/main/CONTRIBUTING.md). +- [ ] I have tested this code locally with `pnpm test:pr`. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5dcbf401f2..3cda47f4f1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,7 @@ ## ✅ Checklist - [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/table/blob/main/CONTRIBUTING.md). -- [ ] I have tested this code locally with `pnpm test:pr`. +- [ ] I have tested this code locally with `pnpm run test:pr`. ## 🚀 Release Impact diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 8ca8702155..4b056ba1d6 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -2,8 +2,6 @@ name: autofix.ci # needed to securely identify the workflow on: pull_request: - push: - branches: [main, alpha, beta, rc] concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} @@ -18,13 +16,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Setup Tools - uses: tanstack/config/.github/setup@main + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - name: Fix formatting - run: pnpm prettier:write + run: pnpm format - name: Apply fixes uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a with: diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml new file mode 100644 index 0000000000..4adbae05f2 --- /dev/null +++ b/.github/workflows/check-skills.yml @@ -0,0 +1,52 @@ +# check-skills.yml +# +# Validates @tanstack/intent skills on PRs that touch skills or artifacts. +# +# Staleness checking after a release is intentionally NOT automated here — run +# `pnpm test:intent` (which calls `intent validate && intent stale`) locally +# before cutting a release. Keeping this workflow validation-only means it +# needs zero write permissions. + +name: Check Skills + +on: + pull_request: + paths: + - 'skills/**' + - '**/skills/**' + - '_artifacts/**' + - '**/_artifacts/**' + - 'scripts/sync-skill-versions.mjs' + - 'scripts/typecheck-skill-snippets.mjs' + - 'scripts/validate-skill-content.mjs' + - 'package.json' + - 'pnpm-lock.yaml' + - '.github/workflows/check-skills.yml' + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + validate: + name: Validate intent skills + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22.13.0 + + - name: Install pnpm + run: npm install --global "$(node -p 'require("./package.json").packageManager')" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Validate skills and examples + run: pnpm test:skills diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cc68eec29a..d646d3ce88 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -2,10 +2,12 @@ name: PR on: pull_request: - paths-ignore: - - 'docs/**' - - 'media/**' - - '**/*.md' + paths: + - '**' + - '!docs/**' + - '!media/**' + - '!**/*.md' + - '.changeset/**' concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} @@ -23,33 +25,64 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Start Nx Agents run: npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yaml" - name: Setup Tools - uses: tanstack/config/.github/setup@main + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - name: Get base and head commits for `nx affected` - uses: nrwl/nx-set-shas@v4.3.3 + uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4.4.0 with: main-branch-name: main - name: Run Checks - run: pnpm run test:pr --parallel=3 + run: pnpm run test:pr --parallel=4 - name: Stop Nx Agents if: ${{ always() }} run: npx nx-cloud stop-all-agents + coverage: + name: Coverage Report + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - name: Setup Tools + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + - name: Collect Coverage + run: pnpm --filter "@tanstack/table-core" test:coverage + - name: Write Coverage Summary + run: node scripts/coverage-summary.mjs packages/table-core/coverage/coverage-summary.json "table-core Coverage" >> "$GITHUB_STEP_SUMMARY" preview: name: Preview runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Setup Tools - uses: tanstack/config/.github/setup@main + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - name: Build Packages run: pnpm run build:all - name: Publish Previews run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' + version-preview: + name: Version Preview + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - name: Setup Tools + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + - name: Changeset Preview + uses: TanStack/config/.github/changeset-preview@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8348d7025a..ba730a7313 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: ci +name: Release on: push: @@ -12,31 +12,35 @@ env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} permissions: - contents: write - id-token: write - pull-requests: write + contents: read jobs: release: name: Release if: github.repository_owner == 'TanStack' runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + pull-requests: write steps: - name: Checkout - uses: actions/checkout@v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Start Nx Agents run: npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yaml" - name: Setup Tools - uses: tanstack/config/.github/setup@main + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - name: Run Tests - run: pnpm run test:ci --parallel=3 + run: pnpm run test:ci --parallel=4 - name: Stop Nx Agents if: ${{ always() }} run: npx nx-cloud stop-all-agents - name: Run Changesets (version or publish) - uses: changesets/action@v1.5.3 + id: changesets + uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0 with: version: pnpm run changeset:version publish: pnpm run changeset:publish @@ -44,4 +48,29 @@ jobs: title: 'ci: Version Packages' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Comment on PRs about release + if: steps.changesets.outputs.published == 'true' + uses: TanStack/config/.github/comment-on-release@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + with: + published-packages: ${{ steps.changesets.outputs.publishedPackages }} + + audit: + name: Audit + needs: release + if: github.repository_owner == 'TanStack' && contains(fromJSON('["main","alpha","beta"]'), github.ref_name) + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Tools + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + - name: Build Packages + run: pnpm run build:all + - name: Install Playwright Browsers + run: pnpm run test:e2e:install + - name: Run E2E Tests + run: pnpm run test:e2e diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000..1d4088db88 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,25 @@ +name: GitHub Actions Security Analysis + +on: + push: + branches: [main] + pull_request: + branches: ['**'] + +permissions: {} + +jobs: + zizmor: + name: Run zizmor + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + advanced-security: false + annotations: true diff --git a/.gitignore b/.gitignore index 78f36fcdd4..eee942d0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,14 +7,15 @@ package-lock.json yarn.lock # builds -types build dist lib +!examples/**/src/lib/ es artifacts .rpt2_cache coverage +test-results *.tgz # misc @@ -49,9 +50,18 @@ yarn.lock *.tsbuildinfo *.tsbuildinfo +.svelte-kit .nx/cache .nx/workspace-data vite.config.js.timestamp-* vite.config.ts.timestamp-* .angular + +.nx/polygraph +.claude/* +.cursor/* + +Agents.md +.agents/* +terminalOutput diff --git a/.npmrc b/.npmrc index 84aee8d998..268c392d3c 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1 @@ -link-workspace-packages=true -prefer-workspace-packages=true provenance=true diff --git a/.nvmrc b/.nvmrc index b404027604..c4697fd566 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.8.0 +26.3.0 diff --git a/.prettierignore b/.prettierignore index aa12baab9e..6ee0b18740 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,10 +1,14 @@ +**/.nx/ **/.nx/cache **/.svelte-kit **/build **/coverage **/dist -**/docs +**/reference **/old-examples +**/examples/**/*.svelte +**/test-results pnpm-lock.yaml +docs/config.json .angular diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..1d7ac851ea --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ec4ceec5b..3e82ab7dd0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,17 +29,19 @@ Before proceeding with development, ensure you match one of the following criter - Fork this repository, we prefer the `feat-*` branch name style - Ensure you have `pnpm` installed -- Install projects dependencies and linkages by running `pnpm install` +- Install the project's dependencies and linkages by running `pnpm install` - Auto-build and auto-test files as you edit by running `pnpm dev` - Implement your changes and tests - To run examples, follow their individual directions. Usually this includes: - - Installing dependencies with `pnpm install` (from the root directory of the workspace) - - Starting the dev server with `pnpm start` (from the example directory) + - cd into the example directory + - Do NOT install dependencies again or do any linking. Nx already handles this for you. Only run install from the project root. + - Starting the dev server with `pnpm dev` or `pnpm start` (from the example directory) - To test in your own projects: - Build/watch for changes with `pnpm build`/`pnpm dev` - Document your changes in the appropriate documentation website markdown pages +- Create a changeset (changelog entry) for your changes by running `pnpm changeset` - Commit your work and open a pull request -- Submit PR for review +- Submit the PR for review ## Adding a new example diff --git a/README.md b/README.md index 59506ffbfd..f9ec9e8e28 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@
- TanStack Table + + + + TanStack Table +

@@ -33,23 +47,36 @@ > [!NOTE] > You may know TanStack Table by the adapter names: > -> - [Angular Table](https://tanstack.com/table/v8/docs/framework/angular/angular-table) -> - [Lit Table](https://tanstack.com/table/v8/docs/framework/lit/lit-table) -> - [Qwik Table](https://tanstack.com/table/v8/docs/framework/qwik/qwik-table) -> - [React Table](https://tanstack.com/table/v8/docs/framework/react/react-table) -> - [Solid Table](https://tanstack.com/table/v8/docs/framework/solid/solid-table) -> - [Svelte Table](https://tanstack.com/table/v8/docs/framework/svelte/svelte-table) -> - [Vue Table](https://tanstack.com/table/v8/docs/framework/vue/vue-table) +> - [Angular Table](https://tanstack.com/table/latest/docs/framework/angular/angular-table) +> - [Lit Table](https://tanstack.com/table/latest/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/latest/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/latest/docs/framework/preact/quick-start) +> - [React Table](https://tanstack.com/table/latest/docs/framework/react/react-table) +> - [Solid Table](https://tanstack.com/table/latest/docs/framework/solid/solid-table) +> - [Svelte Table](https://tanstack.com/table/latest/docs/framework/svelte/svelte-table) +> - [Vue Table](https://tanstack.com/table/latest/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/latest/docs/framework/alpine/alpine-table) +> - [Ember Table](https://tanstack.com/table/latest/docs/framework/ember/ember-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly ### Read the Docs → +## Using an AI Coding Agent? + +TanStack Table ships [TanStack Intent](https://github.com/TanStack/intent) skills inside each adapter package. After installing the library, run: + +```sh +npx @tanstack/intent@latest install +``` + +to add skill-loading guidance for your agent (Claude Code, Cursor, Copilot, etc.). The same CLI also exposes `intent list` to browse available skills and `intent load ` to print one for inspection. Skills are versioned with the library — your agent gets guidance that matches the version of `@tanstack/-table` you installed. Only available for v9 and above. + ## Get Involved - We welcome issues and pull requests! diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml new file mode 100644 index 0000000000..213172c21f --- /dev/null +++ b/_artifacts/domain_map.yaml @@ -0,0 +1,1728 @@ +library: + name: '@tanstack/table' + version: '9.1.2' + repository: 'https://github.com/TanStack/table' + homepage: 'https://tanstack.com/table' + description: 'Headless, framework-agnostic data-grid state and row-processing primitives with tree-shakeable v9 features and framework adapters.' + primary_framework: 'framework-agnostic' + monorepo: true + package_version_overrides: + '@tanstack/angular-table': '9.2.1' + '@tanstack/angular-table-devtools': '9.2.0' + '@tanstack/preact-table-devtools': '9.2.0' + '@tanstack/react-table-devtools': '9.2.0' + '@tanstack/solid-table-devtools': '9.2.0' + '@tanstack/table-devtools': '9.2.0' + '@tanstack/vue-table-devtools': '9.2.0' + +meta: + generated_by: '@tanstack/intent scaffold domain discovery' + date: '2026-08-01' + status: reviewed + maintainer_review_pending: false + phase_4_date: '2026-07-10' + release_voice: 'Treat v9 as stable guidance while recording the exact prerelease package version in skill metadata.' + +scope: + goals: + - 'Route agents to the correct package and skill, then correct common foot-guns and misconceptions.' + - 'Use docs and examples for intended workflows, and direct exact API discovery to installed package declarations (dist/**/*.d.ts).' + - 'Teach headless rendering, feature registration, row-model ownership, framework state, and v8-to-v9 changes as foundations.' + included: + - 'All 18 public workspace packages.' + - 'All 17 stock Table v9 features, custom features, TypeScript helpers, migrations, framework state, composable table hooks, Query, Virtual, Devtools, and fuzzy ranking.' + - 'Renderer-owned CSS edge cases for sticky positioning, column widths, resizing, layout, and virtualization.' + excluded: + - 'Component-library-specific integrations such as shadcn, Material UI, Mantine, and equivalent design systems.' + - 'Worker row models and experimental worker guidance.' + - 'A dedicated performance skill; performance guidance is placed where the relevant state, row model, or virtualization decision is made.' + - 'A promoted useLegacyTable workflow; it may only be identified as a deprecated temporary stopgap when encountered.' + +validation_contract: + - 'Every generated skill must pass Intent validation and exact package-version synchronization.' + - 'Load-bearing TypeScript examples marked with skill-snippet:check must compile against workspace source and their package tsconfig.' + - 'Content checks reject malformed Markdown tables, invented package imports, feature-gated APIs without their feature, unstable empty-array data getters, and adapter-specific corrected foot-guns.' + - 'Wrong/Correct pairs are reserved for code where Wrong actually misbehaves; valid defaults and supported alternatives must use decision-oriented prose instead.' + - 'Virtual integration patterns must follow the maintained adapter guide/example rather than borrowing another framework adapter API.' + - 'Release automation updates skill and artifact version metadata after package versions are calculated and before packages publish.' + +migration_depth_contract: + exception: 'Migration skills are intentionally comprehensive rather than bare-bones. Each adapter migration skill must stand alone and list every breaking change in its maintained v8-to-v9 guide, even when the same change also appears in the table-core migration skill.' + required_coverage: + - 'Framework package and construction entrypoint changes, including framework-version prerequisites.' + - 'Logical start/end column-pinning rename and the full old-to-new API mapping.' + - 'Prototype-bound row, cell, column, and header methods; object-spread/Object.keys/JSON implications.' + - 'Required tableFeatures registration, stockFeatures audit guidance, all 16 feature imports, and feature-gated state/APIs.' + - 'Core row model removal plus every get*RowModel to create*RowModel feature-slot mapping.' + - 'filterFns, sortFns, aggregationFns, and filterMeta registry-slot migration.' + - 'Adapter state access, selectors/subscriptions, controlled slices, external atoms, precedence, and onStateChange removal.' + - 'createColumnHelper TFeatures and columns() inference changes.' + - 'Framework rendering helper/component/directive changes.' + - 'tableOptions and createTableHook composability.' + - 'enablePinning split, columnSizing/columnResizing split and state/API renames.' + - 'Sorting option/API/type/built-in registry renames.' + - 'Removed underscore-prefixed internal APIs and public row API replacements.' + - 'Row-selection some-selected semantic changes and correct indeterminate checks.' + - 'TFeatures generic changes, StockFeatures typing, meta typing, function-registry augmentation replacement, and RowData restriction.' + - 'A final exhaustive migration checklist suitable for auditing an existing codebase.' + +table_state_depth_contract: + exception: 'Framework table-state skills are foundational and intentionally richer than ordinary feature skills. Preserve the guide mental model and adapter-specific reactivity; do not reduce them to a few controlled-state snippets.' + required_coverage: + - 'Table as a state coordinator and internal state as the default ownership choice.' + - 'Feature-gated state slices and the relationship between registered features, options, APIs, atoms, and inferred types.' + - 'The distinct baseAtoms, atoms, store, and adapter-selected state surfaces.' + - 'Current snapshot reads versus framework-reactive subscriptions or tracked reads.' + - 'Exactly one owner per slice: internal state, initialState, external atoms, or state plus on[State]Change.' + - 'External-atom precedence, controlled-state synchronization, updater-function handling, and global onStateChange removal.' + - 'Feature methods as the preferred write surface, baseAtoms as a low-level escape hatch, and external-atom writes when externally owned.' + - 'initialState timing, feature reset behavior, reset-to-default arguments, and core reset limitations with external atoms.' + - 'Feature-specific state types and TableState inference.' + - 'Adapter-specific selectors, subscriptions, compiler/reactivity boundaries, and option synchronization.' + +stable_model_input_contract: + rule: 'Every skill example must keep data and columns references stable between meaningful changes. Never recommend inline map/filter/slice chains, makeColumns calls, or fresh fallback arrays in repeatedly evaluated table options.' + accepted_patterns: + - 'Module or component-lifetime constants for static data and columns.' + - 'Framework memo/computed primitives for derived data, keyed to the actual derivation inputs.' + - 'Stable state, signal, ref, rune, resource, or Query result references for changing data.' + - 'A module-level stable empty fallback instead of data ?? [] in React-like render paths or reactive option initializers.' + +custom_feature_depth_contract: + exception: 'The custom-features skill must enumerate the complete extension surface in one authoritative example. Do not pair a minimal density example with separate map/API examples or add a later misconception that merely explains their relationship.' + required_feature_maps: + - 'TableState_FeatureMap' + - 'TableOptions_FeatureMap' + - 'Table_FeatureMap' + - 'ColumnDef_FeatureMap' + - 'Column_FeatureMap' + - 'Row_FeatureMap' + - 'Cell_FeatureMap' + - 'Header_FeatureMap' + - 'RowModelFns_FeatureMap' + - 'CachedRowModels_FeatureMap' + required_api_installation: + - 'assignTableAPIs inside constructTableAPIs for the singleton table.' + - 'assignPrototypeAPIs inside assignColumnPrototype for shared column methods.' + - 'assignPrototypeAPIs inside assignRowPrototype for shared row methods.' + - 'assignPrototypeAPIs inside assignCellPrototype for shared cell methods.' + - 'assignPrototypeAPIs inside assignHeaderPrototype for shared header methods.' + - 'initColumnInstanceData and initRowInstanceData for per-instance mutable data rather than shared methods.' + - 'The table_/column_/row_/cell_/header_ static-name prefixes, self argument difference, optional memoDeps, and absence of per-object assign*APIs utilities.' + +api_discovery_policy: + rule: 'For exact exports, option types, state shapes, and instance APIs, inspect dist declarations (.d.ts) in the installed package before inventing an API or relying on memory.' + adapter_entrypoint: 'node_modules/@tanstack/-table/dist/index.d.ts' + core_entrypoint: 'node_modules/@tanstack/table-core/dist/index.d.ts' + feature_source: 'node_modules/@tanstack/table-core/dist/features//' + ember_entrypoint: 'node_modules/@tanstack/ember-table/declarations/index.d.ts' + angular_entrypoint: 'node_modules/@tanstack/angular-table/dist/types/' + octane_entrypoint: 'node_modules/@tanstack/octane-table/src/index.d.ts' + fallback: 'If declarations are unavailable, resolve the installed package root and inspect published types; do not open package src/ or substitute v8 / another adapter API.' + +coverage: + ignored_packages: + - 'tanstack-solid-table-example-basic-app-table' + - 'tanstack-solid-table-example-basic-dynamic-columns' + - 'tanstack-solid-table-example-basic-external-atoms' + - 'tanstack-solid-table-example-basic-external-state' + - 'tanstack-solid-table-example-basic-use-table' + - 'tanstack-solid-table-example-header-groups' + - 'tanstack-solid-table-example-column-ordering' + - 'tanstack-solid-table-example-column-pinning' + - 'tanstack-solid-table-example-column-pinning-split' + - 'tanstack-solid-table-example-column-pinning-sticky' + - 'tanstack-solid-table-example-column-resizing' + - 'tanstack-solid-table-example-column-resizing-performant' + - 'tanstack-solid-table-example-column-sizing' + - 'tanstack-solid-table-example-column-visibility' + - 'tanstack-solid-table-example-composable-tables' + - 'tanstack-solid-table-example-expanding' + - 'tanstack-solid-table-example-filters' + - 'tanstack-solid-table-example-filters-faceted' + - 'tanstack-solid-table-example-filters-fuzzy' + - 'tanstack-solid-table-example-grouping' + - 'tanstack-solid-table-example-kitchen-sink' + - 'tanstack-solid-table-example-pagination' + - 'tanstack-solid-table-example-row-pinning' + - 'tanstack-solid-table-example-row-selection' + - 'tanstack-solid-table-example-sorting' + - 'tanstack-solid-table-example-sub-components' + - 'tanstack-solid-table-example-virtualized-columns' + - 'tanstack-solid-table-example-virtualized-infinite-scrolling' + - 'tanstack-solid-table-example-virtualized-rows' + - 'tanstack-solid-table-example-with-tanstack-form' + - 'tanstack-solid-table-example-with-tanstack-query' + - 'tanstack-solid-table-example-with-tanstack-router' + +domains: + - slug: foundations + name: 'Foundations and migration' + description: 'Headless philosophy, feature registration, client/server ownership, TypeScript inference, API discovery, custom features, and v8 migration.' + - slug: feature-plugins + name: 'Feature plugins' + description: 'The 17 stock optional features, their prerequisites, state, row-model participation, and UI responsibilities.' + - slug: framework-adapters + name: 'Framework adapters' + description: 'Per-framework setup, reactive table state, v8 migration, reusable createTableHook patterns, and supported Query/Virtual composition.' + - slug: observability + name: 'Devtools' + description: 'Framework-neutral and adapter-specific Devtools registration, keys, production exports, and connection failures.' + - slug: utilities + name: 'Utilities' + description: 'Fuzzy ranking and comparison behavior shipped by match-sorter-utils.' + +tensions: + - name: 'Explicit tree-shaking vs kitchen-sink convenience' + skills: ['core', 'table-features', 'getting-started', 'migrate-v8-to-v9'] + agent_risk: 'Agents reach for stockFeatures or a kitchen-sink example as the default and silently erase the main v9 bundle-size benefit.' + - name: 'Client-side processing vs server-owned processing' + skills: + [ + 'client-vs-server', + 'column-filtering', + 'global-filtering', + 'grouping', + 'sorting', + 'pagination', + 'with-tanstack-query', + ] + agent_risk: 'Agents enable manual flags but still expect Table row models to transform the data, or mix server and client stages without defining the boundary.' + - name: 'Simple whole-table subscriptions vs fine-grained reactive state' + skills: ['table-state', 'create-table-hook', 'with-tanstack-virtual'] + agent_risk: 'Agents either over-optimize small tables or hide state reads behind stable objects so framework compilers and memoization miss updates.' + - name: 'Reactive options vs stable model inputs' + skills: + [ + 'core', + 'client-vs-server', + 'getting-started', + 'table-state', + 'with-tanstack-query', + ] + agent_risk: 'Agents derive data with map/filter/slice or recreate columns inside render/options callbacks, invalidating row and column models on unrelated updates or causing adapter render loops.' + - name: 'Headless flexibility vs renderer-owned correctness' + skills: + [ + 'core', + 'column-pinning', + 'column-sizing', + 'column-resizing', + 'with-tanstack-virtual', + ] + agent_risk: 'Agents expect feature state to supply semantic markup or CSS and blame Table for sticky, width, accessibility, or layout behavior owned by their renderer.' + +skills: + - slug: core + package: '@tanstack/table-core' + domain: foundations + type: core + purpose: 'Establish the headless mental model, core row model, markup ownership, stable inputs, and feature-driven table architecture.' + sources: + [ + 'TanStack/table:docs/overview.md', + 'TanStack/table:docs/guide/tables.md', + 'TanStack/table:docs/guide/data.md', + 'TanStack/table:packages/table-core/src/index.ts', + ] + failure_modes: + - 'Treating Table as a component or design system instead of rendering semantic markup, styles, and accessibility in userland.' + - 'Recreating data or columns on every reactive pass and causing repeated row-model work or render loops.' + - 'Destructuring v9 row, cell, header, or column prototype methods and losing their instance this binding.' + + - slug: table-features + package: '@tanstack/table-core' + domain: foundations + type: core + purpose: 'Register only the features, row-model factories, and function registries a table actually uses.' + sources: + [ + 'TanStack/table:docs/guide/row-models.md', + 'TanStack/table:packages/table-core/src/types/TableFeatures.ts', + 'TanStack/table:packages/table-core/src/features/stockFeatures.ts', + 'TanStack/table:packages/table-core/src/core/table/constructTable.ts', + ] + failure_modes: + - 'Calling a feature API without registering its feature, so its state slice and runtime API do not exist.' + - 'Registering a row-model or function-registry slot without its prerequisite feature, or placing prerequisites after dependent slots.' + - 'Defaulting to stockFeatures for new tables and bundling every optional feature instead of preserving v9 tree-shaking.' + + - slug: client-vs-server + package: '@tanstack/table-core' + domain: foundations + type: core + purpose: 'Choose which filtering, grouping, sorting, expanding, and pagination stages Table owns and which a backend owns.' + sources: + [ + 'TanStack/table:docs/guide/row-models.md', + 'TanStack/table:packages/table-core/src/core/row-models/coreRowModelsFeature.utils.ts', + 'TanStack/table:examples/react/with-tanstack-query', + ] + failure_modes: + - 'Assuming manualPagination, manualSorting, manualFiltering, manualGrouping, or manualExpanding performs server work; each flag only bypasses that client row-model stage.' + - 'Passing a full dataset while manual pagination is enabled and expecting Table to slice it, or passing one page while a client row model expects the full dataset.' + - 'Mixing client and server stages without defining their order, producing sorting or filtering that only applies to the currently loaded page.' + + - slug: typescript + package: '@tanstack/table-core' + domain: foundations + type: core + purpose: 'Preserve userland inference with columnHelper, meta helpers, tableOptions, and feature-derived types instead of manually threading generics.' + sources: + [ + 'TanStack/table:docs/guide/helpers.md', + 'TanStack/table:docs/guide/column-defs.md', + 'TanStack/table:docs/guide/table-and-column-meta.md', + 'TanStack/table:packages/table-core/src/helpers', + ] + failure_modes: + - 'Annotating heterogeneous helper-built columns as ColumnDef[] and erasing each accessor value type.' + - 'Manually supplying deep Table feature generics rather than deriving them from typeof features or letting helpers infer them.' + - 'Using v8 global declaration merging for per-table meta when v9 tableMeta, columnMeta, filterMeta, and meta helpers can scope the types.' + + - slug: api-not-found + package: '@tanstack/table-core' + domain: foundations + type: core + purpose: 'Diagnose missing exports, options, state, and instance methods before inventing replacement code.' + sources: + [ + 'TanStack/table:packages/table-core/src/index.ts', + 'TanStack/table:packages/table-core/src/types/TableFeatures.ts', + 'TanStack/table:docs/framework/react/guide/migrating.md', + ] + failure_modes: + - 'Searching v8 docs or recalling v7 APIs instead of checking the installed v9 package src and exact package version.' + - 'Assuming an API was removed when its feature was simply omitted from tableFeatures.' + - 'Using object spread, Object.keys, or JSON serialization to discover v9 prototype methods and concluding those methods do not exist.' + + - slug: custom-features + package: '@tanstack/table-core' + domain: foundations + type: core + purpose: 'Extend Table through every v9 FeatureMap and table/column/row/cell/header API lifecycle surface only when built-in options, meta, and APIs are insufficient.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/custom-features.md', + 'TanStack/table:packages/table-core/src/types', + 'TanStack/table:packages/table-core/src/types/TableFeatures.ts', + 'TanStack/table:packages/table-core/src/utils.ts', + 'TanStack/table:packages/table-core/src/features', + 'TanStack/table:examples/react/custom-plugin', + ] + failure_modes: + - 'Building a custom feature for behavior already covered by column meta, table meta, or a built-in feature API.' + - 'Adding types without registering matching initial state, default options, state updaters, and prototype/table APIs in the feature lifecycle.' + - 'Mutating table instances ad hoc instead of registering a stable feature in tableFeatures, losing type inference and composition.' + - 'Assuming the density example exhausts the extension surface and overlooking column-def, column, row, cell, header, or advanced row-model FeatureMaps.' + - 'Looking for nonexistent assignColumnAPIs/assignRowAPIs helpers instead of using assignPrototypeAPIs inside the matching TableFeature prototype hook.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/table-core' + domain: foundations + type: migration + purpose: 'Route framework-specific migration while enforcing the shared v9 feature, row-model, instance-method, naming, and type changes.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/migrating.md', + 'TanStack/table:docs/framework/preact/guide/migrating.md', + 'TanStack/table:docs/framework/solid/guide/migrating.md', + 'TanStack/table:docs/framework/svelte/guide/migrating.md', + 'TanStack/table:docs/framework/vue/guide/migrating.md', + 'TanStack/table:docs/framework/angular/guide/migrating.md', + 'TanStack/table:docs/framework/lit/guide/migrating.md', + 'TanStack/table:packages/table-core/src/index.ts', + 'TanStack/table:packages/table-core/src/types/TableFeatures.ts', + 'TanStack/table:packages/table-core/src/features/column-pinning/columnPinningFeature.types.ts', + 'TanStack/table:packages/table-core/src/features/column-resizing/columnResizingFeature.types.ts', + 'TanStack/table:packages/react-table/src/legacy.ts', + ] + failure_modes: + - 'Promoting deprecated useLegacyTable as the migration target instead of a temporary stopgap for code already using it.' + - 'Leaving v8 getFilteredRowModel/getSortedRowModel/getPaginationRowModel table options in place instead of v9 create*RowModel slots on tableFeatures.' + - 'Missing shared breaking changes such as prototype-bound instance methods, sortFn naming, feature-gated APIs, or logical start/end pinning.' + + - slug: column-faceting + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Derive filter option counts and numeric ranges from the correct faceted row model.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-faceting.md', + 'TanStack/table:packages/table-core/src/features/column-faceting', + 'TanStack/table:examples/react/filters-faceted', + ] + failure_modes: + - 'Registering columnFacetingFeature but omitting the facetedRowModel, facetedUniqueValues, or facetedMinMaxValues slot needed by the API being called.' + - 'Expecting a column facet to include that column own active filter; the faceted row model intentionally applies the other filters and excludes its own.' + - 'Computing large server-owned facet sets from the currently loaded client page and presenting incomplete counts as global results.' + + - slug: column-filtering + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Configure column filters, filter functions, metadata, nested-row direction, and client/manual ownership.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-filtering.md', + 'TanStack/table:packages/table-core/src/features/column-filtering', + 'TanStack/table:examples/react/filters', + ] + failure_modes: + - 'Setting manualFiltering and still expecting createFilteredRowModel to transform data; manual mode returns the pre-filtered model.' + - 'Using accessor values that are objects or renderer output with built-in filter functions that expect comparable primitive values.' + - 'Providing controlled columnFilters plus onColumnFiltersChange without applying both value and updater-function forms to the external source.' + + - slug: grouping + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Group rows, aggregate cells, and reason about grouped rows in expansion and pagination.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/grouping.md', + 'TanStack/table:packages/table-core/src/features/column-grouping', + 'TanStack/table:examples/react/grouping', + ] + failure_modes: + - 'Registering groupedRowModel or aggregationFns without columnGroupingFeature, or expecting grouping state alone to process rows.' + - 'Assuming pageSize counts only leaf data rows; group headers are rows in the model and affect built-in pagination.' + - 'Rendering every grouped cell as a normal value instead of handling grouped, placeholder, and aggregated cell states.' + + - slug: column-ordering + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Control leaf-column order while respecting pinning, visibility, and grouped-column precedence.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-ordering.md', + 'TanStack/table:packages/table-core/src/features/column-ordering', + 'TanStack/table:examples/react/column-dnd', + ] + failure_modes: + - 'Treating columnOrder as the final rendered order while pinning regions and groupedColumnMode also reorder columns.' + - 'Using headers, labels, or array indexes as drag identifiers instead of stable leaf column IDs.' + - 'Mutating the columnOrder array in place so the state owner does not observe a new value.' + + - slug: column-pinning + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Partition columns into logical start, center, and end regions and implement sticky layout correctly.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-pinning.md', + 'TanStack/table:packages/table-core/src/features/column-pinning', + 'TanStack/table:examples/react/column-pinning-sticky', + ] + failure_modes: + - 'Using v8 or early-beta left/right state and APIs instead of v9 logical start/end names, especially in RTL layouts.' + - 'Expecting pinning state to apply position: sticky, offsets, z-index, backgrounds, or overflow CSS automatically.' + - 'Allowing rendered widths to diverge from column.getSize, producing gaps or overlaps between adjacent pinned columns.' + + - slug: column-resizing + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Wire resize gestures and resize state onto the numeric sizing model without avoidable render cost.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-resizing.md', + 'TanStack/table:packages/table-core/src/features/column-resizing', + 'TanStack/table:examples/react/column-resizing-performant', + ] + failure_modes: + - 'Registering columnResizingFeature without its columnSizingFeature prerequisite.' + - 'Displaying a resize handle without attaching header.getResizeHandler to the correct pointer or touch events.' + - 'Reading getSize repeatedly in every cell during onChange resizing instead of caching sizes or using CSS variables for large tables.' + + - slug: column-sizing + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Use numeric min, max, and current sizes as state inputs to a renderer-owned CSS layout.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-sizing.md', + 'TanStack/table:packages/table-core/src/features/column-sizing', + 'TanStack/table:examples/react/column-sizing', + ] + failure_modes: + - 'Expecting the numeric sizing state to set DOM widths without applying it to th/td descendants, grid tracks, or flex styles.' + - 'Forcing auto or percentage strings through a number API instead of choosing and implementing an appropriate CSS layout strategy.' + - 'Forgetting the default size and rendered content can disagree, which also corrupts pinning offsets and total-size calculations.' + + - slug: column-visibility + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Hide leaf columns while rendering only visibility-aware header, column, and cell collections.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-visibility.md', + 'TanStack/table:packages/table-core/src/features/column-visibility', + 'TanStack/table:examples/react/column-visibility', + ] + failure_modes: + - 'Updating columnVisibility but rendering getAllLeafColumns or row.getAllCells, so hidden columns remain in the DOM.' + - 'Treating absent map entries as hidden; only an explicit false hides a column.' + - 'Expecting enableHiding to hide a column when it only controls whether the user or API may hide it.' + + - slug: global-filtering + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Apply one filter across eligible columns with explicit column eligibility and client/manual ownership.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/global-filtering.md', + 'TanStack/table:packages/table-core/src/features/global-filtering', + 'TanStack/table:examples/react/filters', + ] + failure_modes: + - 'Registering globalFilteringFeature without columnFilteringFeature and a filtered row-model stage when client processing is expected.' + - 'Assuming every column participates; the default eligibility checks the first core row value and only accepts strings or numbers.' + - 'Using manualFiltering while updating globalFilter state but never sending that value to the server query.' + + - slug: expanding + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Expand hierarchical subrows or custom detail panels and place expansion correctly relative to pagination.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/expanding.md', + 'TanStack/table:packages/table-core/src/features/row-expanding', + 'TanStack/table:examples/react/expanding', + ] + failure_modes: + - 'Expecting nested data to expand without getSubRows, or expecting a custom detail panel without getRowCanExpand and renderer markup.' + - 'Updating expanded state but never rendering subrows or the custom expanded UI because Table is headless.' + - 'Assuming paginateExpandedRows changes grouping or flattens visual descendants; it only controls the expanded-row pagination stage.' + + - slug: pagination + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Configure client slicing or manual pages, counts, navigation limits, and automatic page-index resets.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/pagination.md', + 'TanStack/table:packages/table-core/src/features/row-pagination', + 'TanStack/table:examples/react/pagination', + ] + failure_modes: + - 'Enabling manualPagination and passing all rows while expecting Table to slice them; manual data must already represent the intended page.' + - 'Omitting rowCount or pageCount for server pagination and then trusting next-page or last-page availability.' + - 'Misdiagnosing pageIndex jumping to zero as failed controlled state when autoResetPageIndex is reacting to client-side data or row-model changes.' + + - slug: row-pinning + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Separate top, center, and bottom rows with stable IDs and explicit rendering/sticky behavior.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/row-pinning.md', + 'TanStack/table:packages/table-core/src/features/row-pinning', + 'TanStack/table:examples/react/row-pinning', + ] + failure_modes: + - 'Using index-derived row IDs for persistent pinning while sorting, filtering, pagination, or data insertion changes indexes.' + - 'Expecting row pinning to add sticky CSS or render top/center/bottom collections in the correct order automatically.' + - 'Ignoring keepPinnedRows, so pinned rows remain visible outside the center row model when the desired product behavior was to filter or paginate them away.' + + - slug: cell-selection + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Maintain spreadsheet-style rectangular selections as ordered include/exclude operations anchored to row and column ids, resolving them into disjoint positive regions across sorting, filtering, pagination, and column layout changes.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/cell-selection.md', + 'TanStack/table:packages/table-core/src/features/cell-selection', + 'TanStack/table:examples/react/cell-selection', + ] + failure_modes: + - 'Expecting cellSelection to be a per-cell map or a list of final positive regions; it is an ordered log of two-corner include/exclude operations, and derived bounds may split one stored range into several regions.' + - 'Reordering, deduplicating, or serializing only the corners of controlled cellSelection state and thereby changing or losing subtraction semantics.' + - 'Using additive when the intended operation is explicit subtraction; selectCellRange mode is replace, include, or exclude, while additive is only a deprecated include alias.' + - 'Assuming Ctrl/Cmd always adds a range; a modified interaction excludes when it starts on a selected cell and includes when it starts on an unselected cell.' + - 'Binding only the mousedown handler and expecting drag selection, or reimplementing mouseup even though the start handler owns its own document listener.' + - 'Drawing the selection outline with borders on a border-collapse table, which changes row heights as cells become selected.' + - 'Re-rendering every cell on each drag update instead of subscribing per row to table.atoms.cellSelection.' + + - slug: cell-spanning + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Merge adjacent body cells into row- and column-spanning cells derived from the rendered row model, with covered cells reporting a span of 0 that renderers must skip.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/cell-spanning.md', + 'TanStack/table:packages/table-core/src/features/cell-spanning', + 'TanStack/table:examples/react/cell-spanning', + ] + failure_modes: + - 'Rendering a covered cell with rowSpan={0} instead of skipping it; HTML rowspan="0" means span to the end of the row group, so the cell merges down the entire tbody.' + - 'Expecting spanning to sort or group rows; it merges adjacent equal values only, so unsorted data renders no merges at all.' + - 'Precomputing spans from the source data array, which survives sorting, filtering, and page changes and produces ragged rows.' + - 'Assuming a run continues across a page or pinned-section boundary; runs are clipped to the rendered rows of each section.' + - 'Pre-expanding stored selection corners to cover merges; selection bounds already expand at derivation time when both features are registered, and pre-expanded corners go stale when sorting or paging changes the merges.' + + - slug: row-selection + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Maintain selection IDs across current, filtered, grouped, and manually paginated data.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/row-selection.md', + 'TanStack/table:packages/table-core/src/features/row-selection', + 'TanStack/table:examples/react/row-selection', + ] + failure_modes: + - 'Assuming selection mutates application data or is automatically deleted when rows disappear; rowSelection is independent ID state.' + - 'Using default index IDs for server pagination or mutable data instead of a stable getRowId.' + - 'Treating getSelectedRowModel as a database-wide selection lookup under manual pagination even though only loaded rows can appear in a row model.' + + - slug: sorting + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Configure single/multi sorting, comparison functions, undefined placement, removal cycles, and client/manual ownership.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/sorting.md', + 'TanStack/table:packages/table-core/src/features/row-sorting', + 'TanStack/table:examples/react/sorting', + ] + failure_modes: + - 'Setting manualSorting and expecting createSortedRowModel to reorder rows; manual mode trusts incoming data.' + - 'Writing a custom sortFn that applies ascending/descending direction itself even though Table reverses the comparator result for descending order.' + - 'Assuming undefined values, multi-sort gestures, and sort-removal cycles match product expectations without configuring sortUndefined and multi-sort options.' + + - slug: getting-started + package: '@tanstack/react-table' + domain: framework-adapters + type: framework + framework: react + purpose: 'Create and render a headless v9 React table with useTable and explicit features.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/migrating.md', + 'TanStack/table:examples/react/basic-use-table', + 'TanStack/table:packages/react-table/src/index.ts', + ] + failure_modes: + - 'Copying a v8 useReactTable/getCoreRowModel setup instead of the v9 useTable plus tableFeatures shape.' + - 'Expecting useTable to render markup or styles rather than mapping headers, rows, and cells with FlexRender or flexRender.' + - 'Creating features, columns, or fallback data inside render without stable references.' + + - slug: table-state + package: '@tanstack/react-table' + domain: framework-adapters + type: framework + framework: react + purpose: 'Read, subscribe to, control, and optimize React table state with selectors, Subscribe, and external atoms.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/table-state.md', + 'TanStack/table:docs/framework/react/guide/react-compiler.md', + 'TanStack/table:examples/react/basic-subscribe', + 'TanStack/table:packages/react-table/src/Subscribe.ts', + 'TanStack/table:packages/react-table/src/useTable.ts', + ] + failure_modes: + - 'Reading table.atoms.x.get or table.store.state during render and assuming that snapshot read subscribes React to future changes.' + - 'Providing onSliceChange without state.slice, or failing to apply updater-function values when React owns the slice.' + - 'Hiding builder-method state reads in compiler-memoized children without a Subscribe boundary, or adding fine-grained Subscribe everywhere before measuring.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/react-table' + domain: framework-adapters + type: migration + framework: react + purpose: 'Migrate React from useReactTable to the native v9 feature, state, rendering, helper, and composable APIs.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/migrating.md', + 'TanStack/table:packages/react-table/src/index.ts', + 'TanStack/table:packages/react-table/src/legacy.ts', + 'TanStack/table:examples/react/basic-use-table', + ] + failure_modes: + - 'Using useLegacyTable as the finished migration rather than converting to useTable and explicit features.' + - 'Keeping the removed global onStateChange or v8 row-model options instead of per-slice control and tableFeatures slots.' + - 'Missing React-specific FlexRender/Subscribe changes while only applying the core feature renames.' + + - slug: create-table-hook + package: '@tanstack/react-table' + domain: framework-adapters + type: framework + framework: react + purpose: 'Create a typed app-level React table hook with shared features, options, helpers, components, and contexts.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/composable-tables.md', + 'TanStack/table:docs/framework/react/guide/table-context.md', + 'TanStack/table:examples/react/composable-tables', + 'TanStack/table:packages/react-table/src/createTableHook.tsx', + 'TanStack/table:packages/react-table/src/createTableHookContexts.tsx', + ] + failure_modes: + - 'Using createTableHook for a one-off table or registering a full component system when only shared features/defaults are needed.' + - 'Prop-drilling table, cell, and header values through registered components instead of using the typed context hooks returned by the same factory.' + - 'Creating contexts inside a render path or introducing circular imports between the hook module and registered components, causing HMR or remount failures.' + + - slug: with-tanstack-query + package: '@tanstack/react-table' + domain: framework-adapters + type: integration + framework: react + purpose: 'Use Query as the server-data source while Table owns data-grid state and explicit manual row-model boundaries.' + sources: + [ + 'TanStack/table:examples/react/with-tanstack-query', + 'TanStack/table:examples/react/virtualized-infinite-scrolling', + 'TanStack/table:docs/framework/react/guide/pagination.md', + ] + failure_modes: + - 'Putting server response data into a second unsynchronized React state layer instead of deriving Table data from the query result.' + - 'Omitting pagination, sorting, or filter state from the query key and showing cached data for the wrong table state.' + - 'Enabling manual processing without passing server counts, resetting invalid pages, or preserving previous data intentionally.' + + - slug: with-tanstack-virtual + package: '@tanstack/react-table' + domain: framework-adapters + type: integration + framework: react + purpose: 'Virtualize React rows or columns as a rendering concern layered over the final Table row model.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/virtualization.md', + 'TanStack/table:examples/react/virtualized-rows', + 'TanStack/table:examples/react/virtualized-columns', + 'TanStack/table:examples/react/virtualized-infinite-scrolling', + ] + failure_modes: + - 'Treating Virtual as a Table feature or row model instead of virtualizing table.getRowModel().rows in the renderer.' + - 'Measuring rows against the wrong scroll element or omitting stable keys and data-index values for dynamic measurement.' + - 'Combining semantic table layout, sticky headers/columns, transforms, and dynamic heights without copying the example CSS positioning contract.' + + - slug: getting-started + package: '@tanstack/preact-table' + domain: framework-adapters + type: framework + framework: preact + purpose: 'Create and render a native Preact v9 table without relying on the React adapter through preact/compat.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/migrating.md', + 'TanStack/table:examples/preact/basic-use-table', + 'TanStack/table:packages/preact-table/src/index.ts', + ] + failure_modes: + - 'Importing @tanstack/react-table through preact/compat instead of the native @tanstack/preact-table v9 adapter.' + - 'Copying useReactTable or React-only rendering assumptions instead of Preact useTable and its exported helpers.' + - 'Recreating stable features, columns, or data on each render and causing unnecessary option and row-model work.' + + - slug: table-state + package: '@tanstack/preact-table' + domain: framework-adapters + type: framework + framework: preact + purpose: 'Use selected table.state, Preact subscriptions, controlled slices, and external atoms correctly.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/table-state.md', + 'TanStack/table:examples/preact/basic-subscribe', + 'TanStack/table:packages/preact-table/src/useTable.ts', + ] + failure_modes: + - 'Reading atom/store snapshots in render as though they create Preact subscriptions.' + - 'Pairing an onSliceChange callback with no controlled state.slice value or ignoring updater-function inputs.' + - 'Narrowing the useTable selector so far that rendered builder APIs depend on unselected state, or optimizing before a real render problem exists.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/preact-table' + domain: framework-adapters + type: migration + framework: preact + purpose: 'Move a v8 React-adapter/preact-compat table onto the native v9 Preact adapter and architecture.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/migrating.md', + 'TanStack/table:packages/preact-table/src/index.ts', + 'TanStack/table:examples/preact/basic-use-table', + ] + failure_modes: + - 'Treating Preact as a simple hook rename while leaving the React package and compat aliases in place.' + - 'Keeping v8 get*RowModel options and globally included features instead of explicit tableFeatures slots.' + - 'Missing prototype-method, state, helper, pinning, and sorting renames shared by every v9 adapter.' + + - slug: create-table-hook + package: '@tanstack/preact-table' + domain: framework-adapters + type: framework + framework: preact + purpose: 'Build a typed reusable Preact table hook and optional component/context registry.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/composable-tables.md', + 'TanStack/table:docs/framework/preact/guide/table-context.md', + 'TanStack/table:examples/preact/composable-tables', + 'TanStack/table:packages/preact-table/src/createTableHook.tsx', + 'TanStack/table:packages/preact-table/src/createTableHookContexts.tsx', + ] + failure_modes: + - 'Using the app hook for one table when standalone useTable is clearer.' + - 'Importing context hooks from a different factory or reading them outside the matching AppTable/AppCell/AppHeader provider.' + - 'Prop-drilling stable table instances through memoized components and expecting context consumers to update without the factory wrappers/subscriptions.' + + - slug: with-tanstack-query + package: '@tanstack/preact-table' + domain: framework-adapters + type: integration + framework: preact + purpose: 'Drive manually processed Preact table data from TanStack Query and table state.' + sources: + [ + 'TanStack/table:examples/preact/with-tanstack-query', + 'TanStack/table:docs/framework/preact/guide/pagination.md', + ] + failure_modes: + - 'Copying React Query hook names or React state glue instead of the Preact Query example.' + - 'Leaving table state out of the query key or query function request.' + - 'Expecting manual pagination/filtering/sorting flags to fetch or transform server data.' + + - slug: with-tanstack-virtual + package: '@tanstack/preact-table' + domain: framework-adapters + type: integration + framework: preact + purpose: 'Apply Preact Virtual rendering to Table rows or columns using the adapter virtualization guide.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/virtualization.md', + 'TanStack/table:packages/preact-table/src/index.ts', + ] + failure_modes: + - 'Copying React-only component/hook details instead of using the Preact Virtual API exported for the installed versions.' + - 'Virtualizing raw input data instead of the final filtered, sorted, expanded, and paginated row model.' + - 'Assuming Table supplies scroll containers, measurements, spacer geometry, sticky CSS, or overscan defaults.' + + - slug: getting-started + package: '@tanstack/solid-table' + domain: framework-adapters + type: framework + framework: solid + purpose: 'Create a Solid v9 table with createTable, reactive getters, and JSX rendering.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/migrating.md', + 'TanStack/table:examples/solid/basic-use-table', + 'TanStack/table:packages/solid-table/src/index.tsx', + ] + failure_modes: + - 'Using the v8 createSolidTable entrypoint or a React hook instead of v9 createTable.' + - 'Reading a signal once and passing its snapshot as data instead of a getter/reactive option when updates are expected.' + - 'Expecting Table to render DOM or CSS rather than iterating row/header APIs in Solid JSX.' + + - slug: table-state + package: '@tanstack/solid-table' + domain: framework-adapters + type: framework + framework: solid + purpose: 'Use Solid-backed table atoms inside tracked scopes and choose native signals or external atoms for ownership.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/table-state.md', + 'TanStack/table:examples/solid/basic-external-state', + 'TanStack/table:packages/solid-table/src/createTable.ts', + ] + failure_modes: + - 'Reading table atoms outside JSX, createMemo, createEffect, or another tracked scope and expecting later changes to rerun code.' + - 'Calling a state updater with a Solid setter assumption without resolving Table value-or-updater semantics.' + - 'Adding React-style broad rerender workarounds instead of relying on Solid narrow atom reads and computations.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/solid-table' + domain: framework-adapters + type: migration + framework: solid + purpose: 'Migrate createSolidTable code to v9 createTable, explicit features, atom-backed state, and new helpers.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/migrating.md', + 'TanStack/table:packages/solid-table/src/index.tsx', + 'TanStack/table:examples/solid/basic-use-table', + ] + failure_modes: + - 'Renaming the entrypoint but retaining v8 row-model options outside tableFeatures.' + - 'Forcing React selector patterns into Solid instead of using signal-tracked table atom reads.' + - 'Missing shared v9 changes to instance method binding, feature APIs, meta helpers, sorting, and pinning.' + + - slug: create-table-hook + package: '@tanstack/solid-table' + domain: framework-adapters + type: framework + framework: solid + purpose: 'Create a shared Solid createAppTable and typed helper/component registry without losing reactivity.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/composable-tables.md', + 'TanStack/table:examples/solid/composable-tables', + 'TanStack/table:packages/solid-table/src/createTableHook.tsx', + ] + failure_modes: + - 'Using createTableHook for a one-off table instead of standalone createTable.' + - 'Passing snapshots where createAppTable needs reactive getters or signals for per-table data and state.' + - 'Reading context values outside registered App wrappers or prop-drilling the table when returned context hooks express the shared boundary.' + + - slug: with-tanstack-query + package: '@tanstack/solid-table' + domain: framework-adapters + type: integration + framework: solid + purpose: 'Compose Solid Query resources with manually processed Table state and reactive query inputs.' + sources: + [ + 'TanStack/table:examples/solid/with-tanstack-query', + 'TanStack/table:docs/framework/solid/guide/pagination.md', + ] + failure_modes: + - 'Passing signal snapshots into the query key so sorting, filtering, or pagination changes are not tracked.' + - 'Copying React Query hook/state patterns instead of Solid Query reactive option getters.' + - 'Using manual Table flags without returning already processed server data and counts.' + + - slug: with-tanstack-virtual + package: '@tanstack/solid-table' + domain: framework-adapters + type: integration + framework: solid + purpose: 'Virtualize Solid Table rows/columns and infinite server data while preserving reactive measurements.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/virtualization.md', + 'TanStack/table:examples/solid/virtualized-rows', + 'TanStack/table:examples/solid/virtualized-columns', + 'TanStack/table:examples/solid/virtualized-infinite-scrolling', + ] + failure_modes: + - 'Creating the virtualizer from a non-reactive row count or wrong scroll element.' + - 'Virtualizing raw data rather than the current Table row model.' + - 'Applying transforms, dynamic measurements, sticky regions, or grid/flex widths inconsistently with the rendered layout.' + + - slug: getting-started + package: '@tanstack/svelte-table' + domain: framework-adapters + type: framework + framework: svelte + purpose: 'Create a Svelte 5 table with createTable, rune-backed option getters, FlexRender, and headless markup.' + sources: + [ + 'TanStack/table:docs/framework/svelte/guide/migrating.md', + 'TanStack/table:examples/svelte/basic-create-table', + 'TanStack/table:packages/svelte-table/src/index.ts', + ] + failure_modes: + - 'Using the v8 createSvelteTable/store API or pre-Svelte-5 syntax instead of v9 createTable and runes.' + - 'Passing a one-time data snapshot where a getter is needed for current rune state.' + - 'Expecting render helpers to initialize interactive markup or provide table CSS automatically.' + + - slug: table-state + package: '@tanstack/svelte-table' + domain: framework-adapters + type: framework + framework: svelte + purpose: 'Use rune-aware table atoms and stores, native $derived projections, controlled $state or createTableState slices, and external atoms without broad invalidation or snapshot mismatches.' + sources: + [ + 'TanStack/table:docs/framework/svelte/guide/table-state.md', + 'TanStack/table:docs/framework/svelte/guide/pagination.md', + 'TanStack/table:examples/svelte/basic-external-state', + 'TanStack/table:packages/svelte-table/src/createTable.svelte.ts', + 'TanStack/table:packages/svelte-table/src/createTableState.svelte.ts', + ] + failure_modes: + - 'Keeping the removed beta.58 createTable/createAppTable selector argument, selected table.state property, subscribeTable helper, or SubscribeSource type after beta.59.' + - 'Reading atom or store snapshots outside a tracked Svelte scope and expecting the read itself to keep a consumer reactive.' + - 'Reading table.store.get() in an effect that only needs one atom slice, causing unrelated state changes to rerun the effect.' + - 'Pairing onSliceChange with a controlled state slice that is not actually written back with value-or-updater semantics.' + - 'Blaming pagination reactivity when autoResetPageIndex immediately overwrites an externally requested page.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/svelte-table' + domain: framework-adapters + type: migration + framework: svelte + purpose: 'Move Svelte 4/v8 store-oriented tables to the Svelte 5 v9 adapter, explicit features, and rune-backed state.' + sources: + [ + 'TanStack/table:docs/framework/svelte/guide/migrating.md', + 'TanStack/table:packages/svelte-table/src/index.ts', + 'TanStack/table:examples/svelte/basic-create-table', + ] + failure_modes: + - 'Attempting the v9 adapter migration without first adopting Svelte 5-compatible component and reactivity syntax.' + - 'Keeping readable-store assumptions or v8 row-model table options after switching to createTable.' + - 'Keeping beta.58 creation selectors, table.state, subscribeTable, or their removed selected-state generic parameters after beta.59.' + - 'Missing shared v9 prototype, feature, helper, sorting, and logical-pinning changes.' + + - slug: create-table-hook + package: '@tanstack/svelte-table' + domain: framework-adapters + type: framework + framework: svelte + purpose: 'Define a reusable Svelte createAppTable, typed column helper, and optional component/context registry.' + sources: + [ + 'TanStack/table:docs/framework/svelte/guide/composable-tables.md', + 'TanStack/table:examples/svelte/composable-tables', + 'TanStack/table:packages/svelte-table/src/createTableHook.svelte.ts', + ] + failure_modes: + - 'Reimplementing createAppTable around the framework-agnostic core instead of using the shipped rune-capable createTableHook implementation.' + - 'Passing $state snapshots instead of getters to createAppTable, freezing data or controlled state.' + - 'Prop-drilling contexts through registered components instead of consuming the typed hooks under matching App wrappers.' + + - slug: with-tanstack-query + package: '@tanstack/svelte-table' + domain: framework-adapters + type: integration + framework: svelte + purpose: 'Connect Svelte Query data and query keys to manual Table processing state.' + sources: + [ + 'TanStack/table:examples/svelte/with-tanstack-query', + 'TanStack/table:docs/framework/svelte/guide/pagination.md', + ] + failure_modes: + - 'Building a query from non-reactive snapshots so page, sort, or filter changes do not refetch.' + - 'Duplicating query data into unsynchronized $state rather than exposing the query result to Table through a getter.' + - 'Expecting manual Table flags to execute network requests or server transformations.' + + - slug: with-tanstack-virtual + package: '@tanstack/svelte-table' + domain: framework-adapters + type: integration + framework: svelte + purpose: 'Render Svelte Table row/column models through Svelte Virtual with stable measurement and layout.' + sources: + [ + 'TanStack/table:docs/framework/svelte/guide/virtualization.md', + 'TanStack/table:examples/svelte/virtualized-rows', + 'TanStack/table:examples/svelte/virtualized-columns', + 'TanStack/table:examples/svelte/virtualized-infinite-scrolling', + ] + failure_modes: + - 'Using a stale row count or scroll element instead of reactive values available to the virtualizer.' + - 'Virtualizing source data rather than the final Table row model.' + - 'Combining absolute positioning, transforms, sticky regions, and semantic table layout without the required CSS geometry.' + + - slug: getting-started + package: '@tanstack/vue-table' + domain: framework-adapters + type: framework + framework: vue + purpose: 'Create a Vue v9 table with useTable, reactive options, and template/render helpers.' + sources: + [ + 'TanStack/table:docs/framework/vue/guide/migrating.md', + 'TanStack/table:examples/vue/basic-use-table', + 'TanStack/table:packages/vue-table/src/index.ts', + ] + failure_modes: + - 'Using v8 useVueTable or React hook names instead of the installed v9 Vue entrypoint.' + - 'Destructuring reactive refs/options into snapshots before passing them to useTable.' + - 'Expecting Table to provide a Vue table component or component-library styling.' + + - slug: table-state + package: '@tanstack/vue-table' + domain: framework-adapters + type: framework + framework: vue + purpose: 'Read Vue-backed table atoms in tracked contexts and control slices with refs, computed values, or external atoms.' + sources: + [ + 'TanStack/table:docs/framework/vue/guide/table-state.md', + 'TanStack/table:examples/vue/basic-external-state', + 'TanStack/table:packages/vue-table/src/useTable.ts', + ] + failure_modes: + - 'Reading an atom snapshot outside a template, computed, watch, or other tracked context and expecting it to update consumers.' + - 'Passing a ref value snapshot rather than the ref/computed/getter shape supported by reactive table options.' + - 'Ignoring value-or-updater callback semantics when synchronizing controlled refs.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/vue-table' + domain: framework-adapters + type: migration + framework: vue + purpose: 'Migrate Vue v8 table construction, row models, state, helpers, and rendering to v9.' + sources: + [ + 'TanStack/table:docs/framework/vue/guide/migrating.md', + 'TanStack/table:packages/vue-table/src/index.ts', + 'TanStack/table:examples/vue/basic-use-table', + ] + failure_modes: + - 'Changing the hook name while retaining v8 row-model options and implicit all-feature behavior.' + - 'Flattening refs/computed inputs during migration and losing Vue reactivity.' + - 'Missing shared v9 changes to prototype methods, per-table meta, sortFn names, and start/end pinning.' + + - slug: create-table-hook + package: '@tanstack/vue-table' + domain: framework-adapters + type: framework + framework: vue + purpose: 'Create a reusable Vue useAppTable and typed component/context registry while avoiding circular inference.' + sources: + [ + 'TanStack/table:docs/framework/vue/guide/composable-tables.md', + 'TanStack/table:examples/vue/composable-tables', + 'TanStack/table:packages/vue-table/src/createTableHook.ts', + ] + failure_modes: + - 'Using createTableHook for one table or registering reusable UI components before the app has shared conventions.' + - 'Creating a circular inference/import chain between the hook module and registered components instead of exporting explicit context-hook types.' + - 'Passing Vue JSX children as slots when table.Subscribe expects an explicit children prop.' + + - slug: with-tanstack-query + package: '@tanstack/vue-table' + domain: framework-adapters + type: integration + framework: vue + purpose: 'Drive Vue Query keys and server requests from reactive Table state with explicit manual stages.' + sources: + [ + 'TanStack/table:examples/vue/with-tanstack-query', + 'TanStack/table:docs/framework/vue/guide/pagination.md', + ] + failure_modes: + - 'Unwrapping refs before constructing the query key so table-state changes are not dependencies.' + - 'Mirroring query data into an unnecessary second ref and allowing the two sources to drift.' + - 'Omitting server counts or expecting manual Table modes to process the returned page.' + + - slug: with-tanstack-virtual + package: '@tanstack/vue-table' + domain: framework-adapters + type: integration + framework: vue + purpose: 'Layer Vue Virtual rendering over the current Table row/column model and its CSS layout.' + sources: + [ + 'TanStack/table:docs/framework/vue/guide/virtualization.md', + 'TanStack/table:examples/vue/virtualized-rows', + 'TanStack/table:examples/vue/virtualized-columns', + 'TanStack/table:examples/vue/virtualized-infinite-scrolling', + ] + failure_modes: + - 'Passing non-reactive counts or scroll targets to the virtualizer.' + - 'Virtualizing raw query/data arrays rather than the current Table model.' + - 'Assuming virtualization supplies column sizes, sticky CSS, semantic markup, or dynamic-row measurement automatically.' + + - slug: getting-started + package: '@tanstack/angular-table' + domain: framework-adapters + type: framework + framework: angular + purpose: 'Create an Angular v9 table with injectTable, stable options, signals, and FlexRender directives.' + sources: + [ + 'TanStack/table:docs/framework/angular/guide/migrating.md', + 'TanStack/table:docs/framework/angular/guide/rendering.md', + 'TanStack/table:examples/angular/basic-inject-table', + 'TanStack/table:packages/angular-table/src/index.ts', + ] + failure_modes: + - 'Calling injectTable outside a valid Angular injection context.' + - 'Allocating columns or features inside the signal-tracked options initializer so every signal change rebuilds static inputs.' + - 'Treating render functions as Angular components or bypassing the documented structural directives and context injection rules.' + + - slug: table-state + package: '@tanstack/angular-table' + domain: framework-adapters + type: framework + framework: angular + purpose: 'Use Angular-signal-backed table atoms, computed selectors, controlled signals, and external Store atoms correctly.' + sources: + [ + 'TanStack/table:docs/framework/angular/guide/table-state.md', + 'TanStack/table:examples/angular/basic-external-state', + 'TanStack/table:packages/angular-table/src/injectTable.ts', + ] + failure_modes: + - 'Wrapping every atom read in computed just to make it reactive even though table atoms already read Angular signals.' + - 'Reading controlled signals in injectTable and overlooking that each write reruns the initializer and setOptions.' + - 'Assigning updater functions directly to signals instead of resolving Table value-or-updater callbacks.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/angular-table' + domain: framework-adapters + type: migration + framework: angular + purpose: 'Migrate createAngularTable code to injectTable, v9 features, signal-backed state, and current rendering directives.' + sources: + [ + 'TanStack/table:docs/framework/angular/guide/migrating.md', + 'TanStack/table:packages/angular-table/src/index.ts', + 'TanStack/table:examples/angular/basic-inject-table', + ] + failure_modes: + - 'Renaming createAngularTable to injectTable without moving construction into an injection context.' + - 'Retaining v8 get*RowModel options or unstable values inside the reactive initializer.' + - 'Missing FlexRender directive, prototype-method, helper, sorting, and logical-pinning changes.' + + - slug: create-table-hook + package: '@tanstack/angular-table' + domain: framework-adapters + type: framework + framework: angular + purpose: 'Create a typed injectAppTable abstraction with shared features, defaults, components, and DI context.' + sources: + [ + 'TanStack/table:docs/framework/angular/guide/composable-tables.md', + 'TanStack/table:examples/angular/composable-tables', + 'TanStack/table:packages/angular-table/src/helpers/createTableHook.ts', + ] + failure_modes: + - 'Calling injectAppTable or returned context injectors outside Angular injection context.' + - 'Prop-drilling table/cell/header values into registered components instead of using the returned typed injection helpers.' + - 'Treating arbitrary render values as Angular components and passing them to flexRenderComponent.' + + - slug: with-tanstack-query + package: '@tanstack/angular-table' + domain: framework-adapters + type: integration + framework: angular + purpose: 'Connect Angular Query to signal-owned Table state and manual server processing.' + sources: + [ + 'TanStack/table:examples/angular/with-tanstack-query', + 'TanStack/table:docs/framework/angular/guide/table-state.md', + 'TanStack/table:docs/framework/angular/guide/pagination.md', + ] + failure_modes: + - 'Reading query-key signals outside a reactive query options function and preventing refetches.' + - 'Duplicating query data into another signal without a defined editing/cache ownership model.' + - 'Expecting manual Table flags to fetch data or omitting row/page counts required for navigation.' + + - slug: with-tanstack-virtual + package: '@tanstack/angular-table' + domain: framework-adapters + type: integration + framework: angular + purpose: 'Render Angular Table models through Angular Virtual with correct signals, measurements, and infinite fetching.' + sources: + [ + 'TanStack/table:docs/framework/angular/guide/virtualization.md', + 'TanStack/table:examples/angular/virtualized-rows', + 'TanStack/table:examples/angular/virtualized-columns', + 'TanStack/table:examples/angular/virtualized-infinite-scrolling', + ] + failure_modes: + - 'Constructing the virtualizer outside required Angular injection/reactive context or from stale counts.' + - 'Virtualizing input data rather than the current Table row model.' + - 'Combining measured rows, transforms, sticky regions, and grid/flex sizing without the example layout contract.' + + - slug: getting-started + package: '@tanstack/lit-table' + domain: framework-adapters + type: framework + framework: lit + purpose: 'Create and render a v9 Lit table through TableController and reactive host updates.' + sources: + [ + 'TanStack/table:docs/framework/lit/guide/migrating.md', + 'TanStack/table:examples/lit/basic-table-controller', + 'TanStack/table:packages/lit-table/src/index.ts', + ] + failure_modes: + - 'Using the v8 TableController constructor-with-options thunk instead of passing options to controller.table in render.' + - 'Constructing a new controller or selector on every render rather than keeping stable host fields.' + - 'Expecting TableController to supply semantic table markup, CSS, or a component library.' + + - slug: table-state + package: '@tanstack/lit-table' + domain: framework-adapters + type: framework + framework: lit + purpose: 'Use TableController-selected table.state, stable subscriptions, controlled properties, and external atoms.' + sources: + [ + 'TanStack/table:docs/framework/lit/guide/table-state.md', + 'TanStack/table:examples/lit/basic-external-state', + 'TanStack/table:packages/lit-table/src/TableController.ts', + ] + failure_modes: + - 'Reading store snapshots imperatively and assuming the host is subscribed to a custom derived value not selected or subscribed.' + - 'Creating selector functions inside render and causing avoidable subscription/update churn.' + - 'Providing onSliceChange without reflecting the controlled state.slice back into the next controller.table options.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/lit-table' + domain: framework-adapters + type: migration + framework: lit + purpose: 'Migrate Lit v8 controller construction, row models, state, helpers, and rendering to v9.' + sources: + [ + 'TanStack/table:docs/framework/lit/guide/migrating.md', + 'TanStack/table:packages/lit-table/src/index.ts', + 'TanStack/table:examples/lit/basic-table-controller', + ] + failure_modes: + - 'Passing the options thunk to the v9 TableController constructor instead of controller.table during render.' + - 'Keeping v8 get*RowModel table options rather than v9 tableFeatures slots.' + - 'Missing shared v9 prototype-method, meta-helper, sorting, and start/end pinning changes.' + + - slug: create-table-hook + package: '@tanstack/lit-table' + domain: framework-adapters + type: framework + framework: lit + purpose: 'Create a reusable Lit app-table controller/helper layer and consume table context from custom elements.' + sources: + [ + 'TanStack/table:docs/framework/lit/guide/composable-tables.md', + 'TanStack/table:examples/lit/composable-tables', + 'TanStack/table:packages/lit-table/src/createTableHook.ts', + ] + failure_modes: + - 'Calling useAppTable without the Lit host or recreating the backing controller every update.' + - 'Assuming Lit tableComponents work exactly like JSX adapter registries; table-level controls may be custom elements using useTableContext.' + - 'Prop-drilling a stable table through custom elements instead of consuming the nearest typed app-table context.' + + - slug: with-tanstack-virtual + package: '@tanstack/lit-table' + domain: framework-adapters + type: integration + framework: lit + purpose: 'Layer Lit Virtual rendering and measurement over the current Table model.' + sources: + [ + 'TanStack/table:docs/framework/lit/guide/virtualization.md', + 'TanStack/table:examples/lit/virtualized-rows', + 'TanStack/table:examples/lit/virtualized-columns', + 'TanStack/table:examples/lit/virtualized-infinite-scrolling', + ] + failure_modes: + - 'Creating a virtualizer/controller with the wrong host lifecycle or a stale item count.' + - 'Virtualizing raw input data instead of table.getRowModel().rows.' + - 'Assuming Virtual or Table owns absolute positioning, measurement attributes, sticky CSS, or column widths.' + + - slug: getting-started + package: '@tanstack/octane-table' + domain: framework-adapters + type: framework + framework: octane + purpose: 'Create and render an Octane v9 table with application-authored TSRX, stable inputs, keyed lists, and component-scoped render helpers.' + sources: + [ + 'TanStack/table:docs/framework/octane/quick-start.md', + 'TanStack/table:examples/octane/basic-use-table', + 'TanStack/table:packages/octane-table/src/index.ts', + 'TanStack/table:packages/octane-table/src/useTable.tsrx', + ] + failure_modes: + - 'Importing another framework adapter or translating React component-return semantics directly instead of using Octane TSRX component bodies.' + - 'Invoking FlexRender or other component-scoped helpers as plain functions instead of rendering them as components.' + - 'Recreating stable features, columns, or data on each render and causing unnecessary option and row-model work.' + + - slug: table-state + package: '@tanstack/octane-table' + domain: framework-adapters + type: framework + framework: octane + purpose: 'Use selected table.state, component-scoped Subscribe islands, controlled slices, and synchronous Octane Store atom ownership with commit-safe publication.' + sources: + [ + 'TanStack/table:docs/framework/octane/guide/table-state.md', + 'TanStack/table:examples/octane/basic-subscribe', + 'TanStack/table:examples/octane/basic-external-atoms', + 'TanStack/table:packages/octane-table/src/useTable.tsrx', + 'TanStack/table:packages/octane-table/src/Subscribe.tsrx', + ] + failure_modes: + - 'Reading atom/store snapshots in render as though they establish an Octane subscription.' + - 'Calling table.Subscribe as a normal function and sharing compiler slots instead of mounting an independent component scope.' + - 'Expecting controlled options.state to publish during render, or mixing it with an external atom that has ownership precedence.' + + - slug: create-table-hook + package: '@tanstack/octane-table' + domain: framework-adapters + type: framework + framework: octane + purpose: 'Build a typed reusable Octane table hook with stable App wrappers, registered components, and default or isolated contexts.' + sources: + [ + 'TanStack/table:docs/framework/octane/guide/composable-tables.md', + 'TanStack/table:docs/framework/octane/guide/table-context.md', + 'TanStack/table:examples/octane/composable-tables', + 'TanStack/table:packages/octane-table/src/createTableHook.tsrx', + 'TanStack/table:packages/octane-table/src/createTableHookContexts.ts', + ] + failure_modes: + - 'Using the app hook for one table when standalone useTable is clearer.' + - 'Reading a factory context outside its matching AppTable/AppCell/AppHeader provider or invoking a wrapper as a plain function.' + - 'Creating the factory in render and destabilizing wrapper, context, and registered-component identities.' + + - slug: getting-started + package: '@tanstack/ember-table' + domain: framework-adapters + type: framework + framework: ember + purpose: 'Create an Ember v9 table through the tracked useTable options thunk and render it with Glimmer-native FlexRender components.' + sources: + [ + 'TanStack/table:docs/framework/ember/quick-start.md', + 'TanStack/table:examples/ember/basic-table', + 'TanStack/table:packages/ember-table/src/index.ts', + 'TanStack/table:packages/ember-table/src/use-table.ts', + 'TanStack/table:packages/ember-table/src/FlexRender.gts', + ] + failure_modes: + - 'Passing an options object instead of a thunk, or failing to read tracked data inside that thunk.' + - 'Passing extracted v9 prototype methods through Ember templates without preserving their receiver.' + - 'Recreating features, columns, or derived data whenever tracked options rerun.' + + - slug: table-state + package: '@tanstack/ember-table' + domain: framework-adapters + type: framework + framework: ember + purpose: 'Use Glimmer-tracked table reads and own slices internally, through Ember atoms, or through tracked controlled state.' + sources: + [ + 'TanStack/table:docs/framework/ember/guide/table-state.md', + 'TanStack/table:examples/ember/basic-external-atoms', + 'TanStack/table:examples/ember/basic-external-state', + 'TanStack/table:packages/ember-table/src/use-table.ts', + 'TanStack/table:packages/ember-table/src/reactivity.ts', + 'TanStack/table:packages/ember-table/src/signal.ts', + ] + failure_modes: + - 'Inventing table.Subscribe or using the intentionally no-op table.store.subscribe instead of Glimmer-tracked reads.' + - 'Using removed table.getState() rather than table.store.state or a slice atom.' + - 'Mixing atoms and state ownership, omitting updater resolution, or trying to replace construct-time atoms after table creation.' + + - slug: create-table-hook + package: '@tanstack/ember-table' + domain: framework-adapters + type: framework + framework: ember + purpose: 'Share Ember table features, row-model slots, defaults, and inferred column helpers without inventing a component registry.' + sources: + [ + 'TanStack/table:docs/framework/ember/guide/composable-tables.md', + 'TanStack/table:examples/ember/basic-app-table', + 'TanStack/table:packages/ember-table/src/create-table-hook.ts', + ] + failure_modes: + - 'Expecting AppTable/AppCell/context registries that the Ember factory does not implement.' + - 'Passing features at each createAppTable call or recreating the app hook in tracked scope.' + - 'Putting shared mutable state in factory defaults instead of keeping state ownership per table.' + + - slug: getting-started + package: '@tanstack/alpine-table' + domain: framework-adapters + type: framework + framework: alpine + purpose: 'Create an Alpine v9 table and render reactive headless markup through Alpine bindings.' + sources: + [ + 'TanStack/table:docs/framework/alpine/guide/table-state.md', + 'TanStack/table:examples/alpine/basic-create-table', + 'TanStack/table:packages/alpine-table/src/index.ts', + ] + failure_modes: + - 'Copying a React/Vue hook API instead of Alpine createTable and its reactive proxy.' + - 'Expecting x-html output to initialize nested Alpine directives; interactive controls must exist as real markup/bindings.' + - 'Expecting the adapter to provide semantic markup, styling, or component-library integration.' + + - slug: table-state + package: '@tanstack/alpine-table' + domain: framework-adapters + type: framework + framework: alpine + purpose: 'Use automatically reactive Alpine table reads and own controlled slices through Alpine.reactive or external atoms.' + sources: + [ + 'TanStack/table:docs/framework/alpine/guide/table-state.md', + 'TanStack/table:examples/alpine/basic-create-table', + 'TanStack/table:packages/alpine-table/src/createTable.ts', + ] + failure_modes: + - 'Adding a nonexistent table.Subscribe abstraction instead of reading APIs directly in Alpine bindings.' + - 'Passing controlled snapshots instead of getters over Alpine.reactive state.' + - 'Ignoring value-or-updater semantics in onSliceChange callbacks or combining state and atoms for the same slice without understanding atom precedence.' + + - slug: create-table-hook + package: '@tanstack/alpine-table' + domain: framework-adapters + type: framework + framework: alpine + purpose: 'Share Alpine table features, options, and typed column helpers without inventing a component registry.' + sources: + [ + 'TanStack/table:docs/framework/alpine/guide/composable-tables.md', + 'TanStack/table:examples/alpine/basic-app-table', + 'TanStack/table:packages/alpine-table/src/createTableHook.ts', + ] + failure_modes: + - 'Expecting React-style registered cell/header/table components from an Alpine hook that intentionally only shares features and defaults.' + - 'Using createTableHook for a single table where standalone createTable is clearer.' + - 'Embedding interactive Alpine directives in x-html strings instead of using real markup or Alpine.bind bundles.' + + - slug: devtools + package: '@tanstack/table-devtools' + domain: observability + type: integration + purpose: 'Register core table targets and inspect state, options, rows, columns, and feature coverage.' + sources: + [ + 'TanStack/table:docs/devtools.md', + 'TanStack/table:packages/table-devtools/src/index.ts', + 'TanStack/table:packages/table-devtools/src/tableTarget.ts', + 'TanStack/table:packages/table-devtools/src/production.ts', + ] + failure_modes: + - 'Registering a table with no non-empty options.key; registration is skipped and Devtools reports no connected table.' + - 'Reusing one key for different live tables and replacing the target unexpectedly.' + - 'Importing the development-gated entrypoint while expecting full Devtools behavior in production.' + + - slug: devtools + package: '@tanstack/react-table-devtools' + domain: observability + type: integration + framework: react + purpose: 'Connect and render React Table Devtools with stable table identity and development gating.' + sources: + [ + 'TanStack/table:docs/devtools.md', + 'TanStack/table:packages/react-table-devtools/src/index.ts', + 'TanStack/table:packages/react-table-devtools/src/useTanStackTableDevtools.ts', + ] + failure_modes: + - 'Omitting table options.key and seeing a rendered panel with no registered table.' + - 'Calling registration conditionally or with unstable table identity instead of using the adapter hook/component lifecycle.' + - 'Assuming the default entrypoint stays functional outside development rather than using or intentionally avoiding the production export.' + + - slug: devtools + package: '@tanstack/preact-table-devtools' + domain: observability + type: integration + framework: preact + purpose: 'Connect Preact tables to the Devtools target registry and render the native Preact panel.' + sources: + [ + 'TanStack/table:docs/devtools.md', + 'TanStack/table:packages/preact-table-devtools/src/index.ts', + 'TanStack/table:packages/preact-table-devtools/src/useTanStackTableDevtools.ts', + ] + failure_modes: + - 'Copying React Devtools imports instead of the Preact package API.' + - 'Omitting a stable options.key or failing to clean up/re-register when the table changes.' + - 'Expecting development-gated exports to inspect tables in a production build.' + + - slug: devtools + package: '@tanstack/solid-table-devtools' + domain: observability + type: integration + framework: solid + purpose: 'Connect Solid tables to Devtools with reactive registration and the correct development/production exports.' + sources: + [ + 'TanStack/table:docs/devtools.md', + 'TanStack/table:packages/solid-table-devtools/src/index.ts', + 'TanStack/table:packages/solid-table-devtools/src/useTanStackTableDevtools.ts', + ] + failure_modes: + - 'Passing a stale table snapshot to registration rather than evaluating it in the intended Solid reactive owner.' + - 'Omitting options.key and silently failing target registration after the logged warning.' + - 'Importing the no-op development-gated component/plugin in production and expecting a panel.' + + - slug: devtools + package: '@tanstack/vue-table-devtools' + domain: observability + type: integration + framework: vue + purpose: 'Connect Vue table refs to Devtools and preserve reactive target cleanup.' + sources: + [ + 'TanStack/table:docs/devtools.md', + 'TanStack/table:packages/vue-table-devtools/src/index.ts', + 'TanStack/table:packages/vue-table-devtools/src/useTanStackTableDevtools.ts', + ] + failure_modes: + - 'Unwrapping a table ref once and preventing registration from following later table changes.' + - 'Omitting the table key or reusing it across simultaneous tables.' + - 'Expecting the development-gated default entrypoint to remain active in production.' + + - slug: devtools + package: '@tanstack/angular-table-devtools' + domain: observability + type: integration + framework: angular + purpose: 'Register Angular signal-provided tables with Devtools inside injection context.' + sources: + [ + 'TanStack/table:docs/devtools.md', + 'TanStack/table:packages/angular-table-devtools/src/index.ts', + 'TanStack/table:packages/angular-table-devtools/src/injectTanStackTableDevtools.ts', + ] + failure_modes: + - 'Calling injectTanStackTableDevtools outside Angular injection context.' + - 'Omitting options.key or returning an undefined table without understanding registration is intentionally disabled.' + - 'Expecting Angular isDevMode-gated exports to render full Devtools in production.' + + - slug: fuzzy-ranking + package: '@tanstack/match-sorter-utils' + domain: utilities + type: core + purpose: 'Rank items, filter on passed, and compare stored ranking metadata without conflating fuzzy matching with Table itself.' + sources: + [ + 'TanStack/table:packages/match-sorter-utils/src/index.ts', + 'TanStack/table:docs/framework/react/guide/fuzzy-filtering.md', + 'TanStack/table:examples/react/filters-fuzzy', + ] + failure_modes: + - 'Using the numeric rank as a boolean instead of checking RankingInfo.passed against the configured threshold.' + - 'Calling rankItem again during sorting instead of storing RankingInfo as filter metadata and comparing it with compareItems.' + - 'Assuming accessors return arbitrary values; matching prepares string values and accessor configuration controls thresholds and ranking bounds.' + +cross_references: + - from: 'core' + to: 'table-features' + reason: 'Every non-core API and state slice depends on feature registration.' + - from: 'table-features' + to: 'api-not-found' + reason: 'A missing feature is the first diagnostic for a missing v9 API.' + - from: 'client-vs-server' + to: + [ + 'column-filtering', + 'global-filtering', + 'grouping', + 'sorting', + 'expanding', + 'pagination', + ] + reason: 'Manual flags bypass the matching client row-model stages.' + - from: 'typescript' + to: ['table-features', 'create-table-hook'] + reason: 'Feature and app-hook factories are the main sources of inferred v9 userland types.' + - from: 'column-faceting' + to: ['column-filtering', 'global-filtering'] + reason: 'Facet values depend on active filter context and eligible columns.' + - from: 'grouping' + to: ['expanding', 'pagination'] + reason: 'Grouped rows are expanded row trees and count as rows during pagination.' + - from: 'column-pinning' + to: ['column-sizing', 'column-resizing'] + reason: 'Sticky offsets are computed from the numeric column sizing model.' + - from: 'pagination' + to: ['row-selection', 'with-tanstack-query'] + reason: 'Manual pages change which selected IDs have loaded Row objects and usually drive a query key.' + - from: 'table-state' + to: ['create-table-hook', 'with-tanstack-query', 'with-tanstack-virtual'] + reason: 'Reusable components and integrations must read state through the adapter reactive model.' + - from: 'migrate-v8-to-v9' + to: ['table-features', 'table-state', 'typescript', 'api-not-found'] + reason: 'Migration problems span architecture, state, types, and renamed/gated APIs.' + +issue_evidence: + - cluster: 'Feature and API discovery' + references: ['discussion/5834', 'issue/6212', 'issue/6311'] + finding: 'V9 modularity, renamed registries, and type-gated APIs make installed-source lookup and feature registration essential.' + - cluster: 'Composable tables and context' + references: ['issue/6348', 'issue/6323', 'issue/6199', 'issue/2344'] + finding: 'Circular imports, context identity, and remount/stale-input failures justify dedicated createTableHook skills.' + - cluster: 'Framework state and compiler reactivity' + references: + ['issue/6224', 'issue/6236', 'issue/6374', 'issue/6117', 'issue/5903'] + finding: 'Snapshot reads, controlled updater semantics, auto resets, and compiler-hidden method reads are recurring silent failures.' + - cluster: 'Manual row models' + references: + [ + 'issue/4917', + 'issue/5110', + 'issue/4771', + 'issue/5850', + 'discussion/3552', + 'discussion/5137', + ] + finding: 'Users routinely expect manual flags to process rows or assume off-page rows remain available as Row objects.' + - cluster: 'TypeScript inference' + references: + [ + 'issue/4241', + 'issue/4382', + 'issue/4387', + 'discussion/4195', + 'discussion/4220', + ] + finding: 'Manual ColumnDef annotations and deep generics erase TValue inference or overload TypeScript; v9 helpers should lead.' + - cluster: 'Renderer-owned layout' + references: + [ + 'issue/5783', + 'issue/5986', + 'discussion/4179', + 'discussion/4439', + 'discussion/3259', + ] + finding: 'Pinning, width, alignment, RTL, and responsive behavior require explicit renderer CSS and stable sizing inputs.' + +documentation_read: + narrative_docs: 'All 187 narrative Markdown documents were inventoried by title, headings, and admonitions; foundational, feature, state, migration, composable, and virtualization guides were deep-read.' + generated_references: 'All 828 generated reference documents were inventoried for exports and API categories; exact API truth is delegated to installed dist declarations (.d.ts) in skills.' + examples: 'All 277 example directories were inventoried; basic, state, composable, Query, Virtual, feature, and Devtools-relevant examples were sampled or deep-read by skill.' + source: 'All public package entrypoints, tableFeatures prerequisites, core construction/state precedence, all 16 feature implementations/defaults, createTableHook implementations, Devtools registration, and match-sorter-utils source were inspected.' + community: 'Recent v9 issues plus recurring high-signal issues and GitHub discussions were reviewed for failure modes and misconceptions.' + +open_gaps: + - skill: 'table-features' + question: 'Should stockFeatures be framed only as a migration/kitchen-sink convenience, or also as an acceptable default for small applications unconcerned with bundle size?' + status: resolved + decision: 'Explicit features are the default; stockFeatures is migration and kitchen-sink convenience.' + - skill: 'client-vs-server' + question: 'Should mixed pipelines such as server filtering/sorting plus client pagination be presented neutrally, or discouraged unless all required rows are loaded?' + status: resolved + decision: 'Mixed pipelines are valid, but skills must name the owner and available dataset for every stage.' + - skill: 'create-table-hook' + question: 'Should createTableHook be recommended as the standard app-level abstraction once an application has multiple tables, with standalone table creation reserved for one-offs?' + status: resolved + decision: 'Recommend createTableHook for recurring app conventions and standalone creation for one-offs.' + - skill: 'create-table-hook' + question: 'How strongly should context/injection be preferred over prop drilling when registered components need the table, cell, or header?' + status: resolved + decision: 'Prefer the typed context or injection helpers returned by the same factory inside registered components.' + - skill: 'migrate-v8-to-v9' + question: 'Should useLegacyTable be omitted unless it already appears in user code, or included as a clearly deprecated emergency bridge in a Common Mistakes note?' + status: resolved + decision: 'Mention it only when encountered and identify it as a deprecated temporary bridge, never the migration target.' + - skill: 'with-tanstack-virtual' + question: 'Should the skills cover only the maintained examples, or also mention unsupported combinations such as drag-and-drop plus virtualization as user-owned composition?' + status: resolved + decision: 'Teach maintained examples; briefly label unsupported combinations as user-owned composition without prescribing an unmaintained recipe.' + - skill: 'devtools' + question: 'Should production entrypoints be taught as supported production inspection, or should normal guidance keep Devtools development-only?' + status: resolved + decision: 'Keep normal guidance development-only; explain production entrypoints only when explicitly requested.' + +maintainer_interview: + phase_2_completed: true + phase_2_summary: + - 'Primary journeys are first table setup, adding a feature, v8-to-v9 migration, server-data decisions, and performance debugging.' + - 'Table is headless and compatible with component libraries/design systems, but no library-specific skills are in scope.' + - 'Feature skills should be short, edge-case-first, and route exact API discovery to installed dist declarations (.d.ts).' + - 'Framework state guides, createTableHook, Query, and Virtual require dedicated adapter guidance where source exists.' + - 'Worker row models are excluded and useLegacyTable must not be promoted.' + phase_4_completed: true + phase_4_answers_accepted: true diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md new file mode 100644 index 0000000000..00de2f975d --- /dev/null +++ b/_artifacts/skill_spec.md @@ -0,0 +1,399 @@ +# TanStack Table v9 skill specification + +Status: reviewed
+Date: 2026-07-29
+Library target: TanStack Table v9, authored in stable-release voice
+Package metadata target: exact workspace package versions; release automation keeps every shipped skill synchronized + +This specification is the generation contract for a deliberately smaller, foot-gun-first TanStack Intent skill set. It is not a documentation outline. The complete evidence and failure-mode inventory is in domain_map.yaml. + +## Outcome + +Generate 80 short package-local skills across all 18 public packages. A loaded skill should quickly do three things: + +1. Correct the user or agent mental model. +2. Show the smallest reliable setup or decision pattern. +3. Route exact API discovery to installed package declarations (`dist/**/*.d.ts`, Ember `declarations/**/*.d.ts`, Angular `dist/types/*.d.ts`). + +The skills should not enumerate every option or method. That duplicates generated reference docs, ages badly, and encourages agents to recall the wrong major version. + +## Maintainer intent + +- TanStack Table is headless. It coordinates table state and row processing; the user owns markup, styles, accessibility, and component-library integration. +- V9 optional features are plugins. A feature API, state slice, row model, or function registry exists only when the matching feature is registered through tableFeatures. +- The client/server row-model boundary is a first-order architecture decision. Manual modes bypass Table processing; they do not perform server work. +- Most userland TypeScript should be inferred through helpers, features, options, and app-hook factories. Deep manual generics are a smell. +- createTableHook is important v9 guidance for reusable app-level table infrastructure. It deserves one dedicated skill in every framework package. +- Framework table-state guidance is fundamental and should retain substantially more depth than ordinary feature skills. +- Data and columns are model inputs and must retain stable references between meaningful changes in every adapter and composition example. +- V8-to-v9 migration is a primary route. Deprecated useLegacyTable is not the destination and must not be promoted. +- TanStack Query usually owns data before it reaches Table. TanStack Virtual is intertwined with Table rendering after the final row/column model exists. +- CSS/layout failure modes belong in the relevant pinning, sizing, resizing, and virtualization skills. Component-library-specific skills do not. +- Worker row models are excluded. + +## Source-of-truth hierarchy + +Use evidence in this order: + +1. Installed package declarations (`.d.ts`) for exact exports, type signatures, feature prerequisites, defaults, and instance APIs. +2. Current v9 guides for intended mental models and supported workflows. +3. Current examples for maintained composition and rendering patterns. +4. Recent and recurring GitHub issues/discussions for silent failures and misconceptions. + +Every skill that discusses APIs must tell the consuming agent how to inspect the matching installed declarations. Preferred routes: + +- Adapter API: node_modules/@tanstack/FRAMEWORK-table/dist/index.d.ts, then the matching exported `*.d.ts` file. +- Core API: node_modules/@tanstack/table-core/dist/index.d.ts. +- Stock feature API: node_modules/@tanstack/table-core/dist/features/FEATURE/. +- Ember API: node_modules/@tanstack/ember-table/declarations/index.d.ts (and sibling `declarations/*.d.ts`). +- Angular API: node_modules/@tanstack/angular-table/dist/types/\*.d.ts (bundled public API; do not expect `src/helpers/` under the published package). +- Octane API: node_modules/@tanstack/octane-table/src/index.d.ts, the matching `*.tsrx.d.ts` sidecar, and `src/types.ts` (the package intentionally distributes authored source). +- Devtools API: node_modules/@tanstack/FRAMEWORK-table-devtools/dist/index.d.ts or @tanstack/table-devtools/dist/index.d.ts. +- Fuzzy ranking API: node_modules/@tanstack/match-sorter-utils/dist/index.d.ts. + +Do not open package `src/` under `node_modules` unless the package intentionally publishes source, as `@tanstack/octane-table` does. Do not direct agents to a GitHub main-branch source file when an installed package is available. Installed declarations and published source keep guidance aligned with the consumer package version. + +## Skill writing contract + +### Frontmatter + +Each generated SKILL.md must satisfy the current TanStack Intent validator: + +- name is the leaf directory segment. +- description is a dense routing description no longer than 1024 characters. +- metadata contains type, library, library_version, and framework when applicable. +- sources remains top-level and lists only repo files/directories actually used by that skill. +- framework skills include a top-level requires array. +- no skill exceeds 500 lines. + +The metadata version must record the exact package version even though prose treats v9 as stable. Do not call ordinary v9 APIs experimental or advise waiting for stable. + +### Body shape + +Prefer 60-180 lines. Table-state, migration, createTableHook, and Virtual skills may be longer when the adapter genuinely differs. + +Use this default structure: + +1. One-paragraph mental model. +2. Setup: imports and the smallest valid configuration. +3. Two to four decision or implementation patterns. +4. Common mistakes: at least three concrete failures with correction. +5. API discovery: exact installed declaration route (`.d.ts`) and identifiers to inspect. +6. Cross-skill routing only when another skill owns the next decision. + +Do not add a reference folder by default. Add one only when a large migration mapping or framework-specific content cannot stay concise in SKILL.md. Progressive disclosure is a size tool, not permission to recreate all docs as references. + +### Migration skill exception + +The table-core and seven adapter migrate-v8-to-v9 skills are intentionally comprehensive. They may approach the 500-line limit and must list every breaking change in the maintained migration guide, not merely three common mistakes or a short route to the docs. + +Every adapter migration skill must be usable on its own and include: + +- its framework-version prerequisite, package change, and construction entrypoint mapping; +- the complete shared architecture changes for tableFeatures, all stock feature imports, row-model slots, function registries, and stockFeatures audit guidance; +- the full logical start/end column-pinning mapping; +- prototype-method binding and enumeration/spread consequences; +- state access, selector/subscription, controlled-state, external-atom, precedence, and onStateChange changes for that adapter; +- createColumnHelper/columns(), rendering, tableOptions, and createTableHook changes; +- pinning-option, sizing/resizing, sorting, removed-internal, row, and row-selection API changes; +- all TypeScript generic, meta, function-registry augmentation, StockFeatures, and RowData changes; +- an exhaustive checkbox audit at the end. + +Do not rely on the core migration skill to hide shared changes from an adapter migration. The requires relationship supplies context, but migration users commonly load only the adapter skill and need the full audit surface there. Keep detailed mappings in SKILL.md unless the file would exceed Intent's 500-line limit. + +### Table-state skill exception + +Every framework table-state skill must preserve substantially more of its guide than an ordinary feature skill because state coordination is the library's foundational behavior. Include: + +- internal state as the default and the reasons to hoist only selected slices; +- feature-gated state and typing; +- `baseAtoms`, readonly derived `atoms`, the flat `store`, and any adapter-selected `table.state` surface; +- snapshot reads versus the adapter's tracked/subscribed reads; +- one owner per slice across internal state, `initialState`, external `atoms`, and `state` plus `on[State]Change`; +- precedence, value-or-updater handling, and removal of the v8 global `onStateChange` option; +- preferred feature-method writes, low-level base-atom writes, initial/reset semantics, and externally owned reset limitations; +- feature-specific types and `TableState` inference; +- framework-specific selector, subscription, compiler, signal, rune, ref, controller, or proxy behavior. + +Retain concrete wrong-versus-correct examples for the adapter's most likely subscription and controlled-state mistakes. Table-state skills may exceed the normal 180-line target while remaining below Intent's 500-line limit. + +### Stable model-input invariant + +Treat stable `data` and `columns` references as a correctness and performance invariant, including in client/server and Query examples. Never place `.map()`, `.filter()`, `.slice()`, a column factory, or a fresh `[]` fallback inline in table options that can be reevaluated. Use module/component-lifetime constants, framework memo/computed primitives, stable reactive containers, or stable Query result arrays. “Manual” row processing changes ownership; it does not relax reference stability. + +### Custom-feature completeness exception + +The custom-features skill must enumerate all 10 public declaration-merge FeatureMaps: table state, table options, table, column definition, column, row, cell, header, row-model functions, and cached row models. Explain that `Plugins` registers the feature key and that declarations add types only; each advertised runtime surface needs matching lifecycle wiring. + +Enumerate both API utilities and every installation path: `assignTableAPIs` in `constructTableAPIs`, plus `assignPrototypeAPIs` in `assignColumnPrototype`, `assignRowPrototype`, `assignCellPrototype`, and `assignHeaderPrototype`. Include the static-name prefixes, prototype self argument, optional `memoDeps`, shared-prototype constraint, `initColumnInstanceData`/`initRowInstanceData`, and the fact that per-object `assignColumnAPIs`-style utilities do not exist. Clearly label row-model maps as advanced internal pipeline surfaces requiring explicit runtime/cache wiring. + +Use one annotated, authoritative feature example for the complete shape. Do not stack a minimal density example, a second FeatureMap example, a third API-installation example, and then repeat their distinction under Common Mistakes. Keep selection guidance and foot-guns as compact prose around the single example. + +### Code examples + +- Use v9 names and shapes only unless a migration skill is explicitly contrasting v8. +- Use the smallest feature set needed by the example. +- Keep features, data, columns, and other static inputs stable. Derive changing data with the adapter's memo/computed primitive and never use fresh inline fallback arrays in repeated option evaluation. +- Show row-model factories as slots in tableFeatures, after their prerequisite feature. +- Keep markup generic and unstyled unless demonstrating a CSS/layout foot-gun. +- Prefer helper inference over explicit Table feature generic plumbing. +- Never show useLegacyTable as the recommended solution. + +### Common Mistakes quality bar + +A mistake must be plausible, consequential, and grounded in source, docs, examples, or maintainer/community evidence. Prefer failures that compile or render but behave incorrectly: + +- missing feature registration; +- manual mode bypasses a row model; +- snapshot read is not a framework subscription; +- controlled callback does not write back the updater; +- unstable data/columns/features redo work; +- hidden columns rendered from non-visibility-aware APIs; +- pinning/sizing state not applied in CSS; +- off-page selected IDs mistaken for loaded Row objects; +- v8 or another adapter API hallucinated from memory. + +Avoid padding Common Mistakes with generic advice such as read the docs, handle errors, or add tests. + +Use Wrong/Correct only when the Wrong form is demonstrably broken or misleading. Do not place a valid default, canonical adapter pattern, or supported tradeoff in the Wrong slot. Present those cases as decisions with consequences instead. + +### Executable validation + +- `intent validate` checks structure, frontmatter, sources, requires, and artifacts. +- `skills:versions:check` compares each skill's `metadata.library_version` with its package and verifies artifact overrides. +- `test:skill-content` checks high-risk generated-content invariants, including Markdown table shape, package imports, feature gating, stable empty fallbacks, adapter subscription shapes, and resize input events. +- Add `` immediately before a self-contained TypeScript/TSX fence when its exact code is load-bearing. `test:skill-snippets` compiles each marked fence against workspace source. A marker may specify `prelude=path` or `tsconfig=path` when the snippet needs an explicit checked context. +- Virtual composition guidance must be copied from or kept structurally faithful to the maintained adapter guide/example. When evidence is absent, route to the documented supported composition instead of inventing an adapter package or API. + +These checks run in `pnpm test:skills`. They supplement review; they do not justify expanding skills into API summaries. + +### Release version synchronization + +Skill versions ship with package versions. After release tooling calculates package versions, run `pnpm skills:versions:fix` before publishing and `pnpm skills:versions:check` as a guard. The sync updates package-local skill frontmatter and repo-root artifact version overrides together. + +## Routing taxonomy + +### Foundations and migration — @tanstack/table-core (7) + +- core — headless philosophy, core model, stable inputs, renderer ownership. +- table-features — explicit registration, prerequisites, row-model/function slots, tree-shaking. +- client-vs-server — choose ownership for filtering/grouping/sorting/expanding/pagination. +- typescript — columnHelper, meta helpers, tableOptions, inference, avoid manual generics. +- api-not-found — inspect installed declarations, feature gating, version/adapter mismatch, prototypes. +- custom-features — plugin lifecycle after exhausting built-in APIs and meta. +- migrate-v8-to-v9 — shared breaking changes and adapter migration routing. + +### Stock feature plugins — @tanstack/table-core (16) + +- aggregation +- cell-selection +- column-faceting +- column-filtering +- grouping +- column-ordering +- column-pinning +- column-resizing +- column-sizing +- column-visibility +- global-filtering +- expanding +- pagination +- row-pinning +- row-selection +- sorting + +Each feature skill must: + +- name the feature import; +- name only row-model and registry slots relevant to that feature; +- state its tableFeatures prerequisites; +- distinguish state from row processing and renderer behavior; +- route exact API discovery to its shipped feature directory under `dist/features/`; +- include feature-specific edge cases from domain_map.yaml. + +Do not combine all column layout features into one summary. Their plugin prerequisites and CSS responsibilities differ enough to route independently. + +### Framework adapter set + +React, Preact, Solid, Svelte, Vue, and Angular each ship six skills: + +- getting-started +- table-state +- migrate-v8-to-v9 +- create-table-hook +- with-tanstack-query +- with-tanstack-virtual + +Lit ships five: + +- getting-started +- table-state +- migrate-v8-to-v9 +- create-table-hook +- with-tanstack-virtual + +Alpine ships three: + +- getting-started +- table-state +- create-table-hook + +Ember ships three: + +- getting-started +- table-state +- create-table-hook + +Octane ships three: + +- getting-started +- table-state +- create-table-hook + +Do not add Query where no maintained adapter example exists. Do not add Alpine, Ember, or Octane migration skills because none has a v8 adapter journey to teach. Do not add Ember or Octane Query or Virtual skills until maintained adapter examples exist. Do not invent Preact virtualization examples; its Virtual skill should rely on the maintained adapter guide and installed APIs. + +### Devtools set (6) + +Each Devtools package ships one skill named devtools: + +- @tanstack/table-devtools +- @tanstack/react-table-devtools +- @tanstack/preact-table-devtools +- @tanstack/solid-table-devtools +- @tanstack/vue-table-devtools +- @tanstack/angular-table-devtools + +All Devtools skills must emphasize the required non-empty table options.key, lifecycle-aware registration, unique keys, and development/production export behavior. Keep the framework-neutral package focused on target registration and inspection; keep adapters focused on their hook/injection/plugin lifecycle. + +### Utility set (1) + +@tanstack/match-sorter-utils ships fuzzy-ranking. Teach the three-stage pattern: rank with rankItem, filter with RankingInfo.passed, then sort stored metadata with compareItems. Route Table-specific filter metadata wiring to column-filtering/global-filtering rather than turning this utility skill into a Table feature summary. + +## Package coverage + +| Package | Skills | +| -------------------------------- | -----: | +| @tanstack/table-core | 23 | +| @tanstack/react-table | 6 | +| @tanstack/preact-table | 6 | +| @tanstack/octane-table | 3 | +| @tanstack/solid-table | 6 | +| @tanstack/svelte-table | 6 | +| @tanstack/vue-table | 6 | +| @tanstack/angular-table | 6 | +| @tanstack/lit-table | 5 | +| @tanstack/alpine-table | 3 | +| @tanstack/ember-table | 3 | +| @tanstack/table-devtools | 1 | +| @tanstack/react-table-devtools | 1 | +| @tanstack/preact-table-devtools | 1 | +| @tanstack/solid-table-devtools | 1 | +| @tanstack/vue-table-devtools | 1 | +| @tanstack/angular-table-devtools | 1 | +| @tanstack/match-sorter-utils | 1 | +| Total | 80 | + +## Framework distinctions that must survive generation + +### React + +- useTable returns selected table.state; the default selector selects all registered state. +- table.atoms.get and table.store.state are snapshot reads, not React subscriptions. +- Subscribe is the supported fine-grained boundary and React Compiler escape hatch for builder-method reads hidden in memoized children. +- Do not prescribe fine-grained subscription machinery until render cost or compiler behavior requires it. + +### Preact + +- Use the native Preact package, not React through preact/compat. +- State selection and Subscribe resemble React but must use Preact adapter/store imports. + +### Octane + +- The package distributes authored TypeScript and TSRX; consumer tooling compiles it for the current target and mode. Components use TSRX component bodies plus keyed `@for` loops where appropriate. +- `useTable` stages fresh options for same-render reads, selects `table.state`, and publishes controlled state only from an accepted layout commit; abandoned work cannot notify the store. +- Render `table.Subscribe` and the createTableHook App wrappers as components so each has an independent Octane hook/context scope; never invoke them as plain functions. +- Use `@tanstack/octane-store` for external atoms. External atoms are synchronous owners and take precedence over controlled `options.state`. +- Native text inputs update on `onInput`; `onChange` follows native change timing rather than React's input-event alias. + +### Solid + +- createTable atoms are backed by Solid primitives; reads are reactive only inside tracked scopes. +- Prefer native signals for framework-owned state and external TanStack Store atoms for cross-app atom ownership. + +### Svelte + +- V9 targets Svelte 5 and runes. +- Read a slice with `table.atoms..get()` and the complete state with `table.store.get()` inside templates, `$derived`, `$derived.by`, or `$effect`; reads outside tracked scopes are current snapshots. +- Starting in beta.59, Svelte has no table-creation selector, selected `table.state`, `subscribeTable`, or `SubscribeSource`; use native `$derived` projections instead. +- Prefer `$state` plus getter-backed controlled slices, or `createTableState` for an updater-compatible getter/setter pair. Keep `useSelector` only for raw external atoms consumed outside the table. +- Reactive data and controlled values commonly need getters; avoid passing snapshots. +- The shipped `createTableHook` implementation supplies rune semantics, so an app hook that calls it may live in a normal `.ts` module. + +### Vue + +- Preserve refs/computed/reactive option shapes rather than destructuring snapshots. +- In JSX, table.Subscribe receives children as an explicit prop. +- The composable component registry may require explicit exported context-hook types to break circular inference. + +### Angular + +- injectTable, injectAppTable, Devtools injection, and returned context helpers require Angular injection context. +- Signal reads inside the options initializer cause setOptions to run again; keep features/columns and other static values outside it. +- Preserve FlexRender directive and component-vs-function rendering distinctions. + +### Lit + +- TableController is a stable host field; v9 passes options to controller.table during render. +- Keep selector references stable. +- createTableHook table-level controls may consume context from custom elements rather than a JSX-style tableComponents registry. + +### Alpine + +- The table proxy automatically makes API reads reactive inside Alpine bindings; there is no table.Subscribe. +- x-html does not initialize nested Alpine directives. +- createTableHook shares features/options/helpers, not a reusable component registry. + +### Ember + +- `useTable` and `createAppTable` take options thunks; tracked values must be read inside the thunk while features, atoms, and columns remain stable. +- Glimmer tracks table API and Ember atom reads directly. There is no table.Subscribe, `table.store.subscribe` is intentionally a no-op, and v8 `table.getState()` is removed. +- V9 prototype methods need their receiver, so templates use getters or module helpers rather than extracted table/column/row methods. +- FlexRender components receive `@ctx` and optional `@options`. +- Ember createTableHook shares features/defaults and inferred column helpers; it does not provide component or context registries. + +## Cross-cutting placement rules + +- Performance: stable inputs in getting-started/core; adapter-specific fine-grained state reads or selectors in table-state; CSS variables in resizing; measurement/overscan in Virtual; row ownership in client-vs-server. +- CSS: pinning, sizing, resizing, and Virtual skills only. Core may state that CSS is user-owned. +- Accessibility: core/getting-started may remind that headless rendering leaves semantics and interaction accessibility with the renderer; do not create a component-library integration skill. +- Query: data source and manual processing boundaries, not Table rendering. +- Virtual: final Table models and renderer geometry, never tableFeatures. +- Context: createTableHook skills; mention context over prop drilling when a registered reusable component needs typed table/cell/header access. +- API lookup: api-not-found establishes the workflow; every other skill includes its direct installed declaration route. + +## Anti-patterns forbidden during generation + +- A skill that is primarily a list of every exported API. +- One giant all-features or all-frameworks skill. +- Per-component-library skills or shadcn/MUI/Mantine-specific code. +- Worker row-model instructions. +- A dedicated generic performance checklist divorced from the feature causing the work. +- V8 setup in non-migration examples. +- useLegacyTable as a recommended quick start. +- Deep explicit generic signatures copied from internal types when helpers can infer them. +- Claims that pinning, sizing, resizing, expansion, or virtualization render their UI/CSS automatically. +- Claims that a manual flag calls a backend. + +## Maintainer review decisions + +The maintainer accepted these generation positions on 2026-07-10: + +1. Explicit features are the default; stockFeatures is for migration and kitchen-sink convenience. +2. Mixed client/server pipelines are valid only when the skill names the owner and available dataset for every stage. +3. createTableHook is recommended for recurring app conventions; standalone construction remains appropriate for one-offs. +4. Typed context/injection helpers from createTableHook are preferred over prop drilling inside registered components. +5. useLegacyTable is mentioned only when encountered, as a deprecated temporary bridge rather than a migration target. +6. Virtual skills teach maintained examples and only identify unsupported combinations as user-owned composition. +7. Devtools guidance is development-only by default; production entrypoints are explained only when explicitly requested. + +Domain discovery is reviewed and tree generation may proceed. diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml new file mode 100644 index 0000000000..1f2af9f60b --- /dev/null +++ b/_artifacts/skill_tree.yaml @@ -0,0 +1,1201 @@ +library: + name: '@tanstack/table' + version: '9.1.2' + repository: 'https://github.com/TanStack/table' + description: 'Headless data-grid state and row processing with tree-shakeable v9 features and framework adapters.' + package_version_overrides: + '@tanstack/angular-table': '9.2.1' + '@tanstack/angular-table-devtools': '9.2.0' + '@tanstack/preact-table-devtools': '9.2.0' + '@tanstack/react-table-devtools': '9.2.0' + '@tanstack/solid-table-devtools': '9.2.0' + '@tanstack/table-devtools': '9.2.0' + '@tanstack/vue-table-devtools': '9.2.0' +generated_from: + domain_map: '_artifacts/domain_map.yaml' + skill_spec: '_artifacts/skill_spec.md' +generated_at: '2026-08-01' +status: reviewed +reviewed_at: '2026-07-29' +batch_review: true +structure: 'flat-per-package' +monorepo_layout: true +tree_decisions: + - 'Every domain-map entry maps one-to-one to a package-local SKILL.md.' + - 'Flat task-focused paths keep each feature and adapter workflow independently discoverable.' + - 'The table-core core skill is the lightweight router and philosophy skill; no additional synthetic router skills are added.' + - 'No reference files are planned initially. Generation may add one only when content cannot remain concise under the reviewed specification.' + - 'Cross-package requires use the package#skill identifier form; same-package requires use the local slug.' + - 'All migrate-v8-to-v9 skills are comprehensive exceptions to the compact default: each must enumerate every shared and adapter-specific breaking change and end with a complete audit checklist.' + - 'Wrong/Correct examples are used only for genuinely broken behavior; supported alternatives are presented as explicit decisions.' + - 'Adapter composition skills stay anchored to maintained examples and guides, especially where Virtual adapter APIs differ.' + - 'Skill source, content, Markdown structure, checked snippets, and package-version metadata are validated in CI.' + - 'All framework table-state skills are foundational depth exceptions: each preserves state surfaces, feature gating, ownership, precedence, updates/resets, typing, and adapter-specific subscription behavior from its guide.' + - 'Every generated example keeps data and columns stable; repeated option evaluation must not contain inline derivation, column factories, or fresh empty-array fallbacks.' + - 'The custom-features skill is a completeness exception expressed through one authoritative example: it inventories all 10 FeatureMaps and every table/column/row/cell/header API installation path without layering redundant examples or misconceptions.' + +skills: + - name: 'Core' + slug: core + type: core + domain: foundations + path: 'packages/table-core/skills/core/SKILL.md' + package: 'packages/table-core' + description: 'Use TanStack Table v9 as a headless data-grid state and row-processing engine. Routes first-table setup, stable data/columns, semantic rendering, feature plugins, framework adapters, and renderer-owned CSS or accessibility.' + sources: + - 'TanStack/table:docs/overview.md' + - 'TanStack/table:docs/guide/tables.md' + - 'TanStack/table:docs/guide/data.md' + - 'TanStack/table:packages/table-core/src/index.ts' + + - name: 'Table Features' + slug: table-features + type: sub-skill + domain: foundations + path: 'packages/table-core/skills/table-features/SKILL.md' + package: 'packages/table-core' + description: 'Register v9 tableFeatures, feature plugins, create*RowModel factories, and filterFns/sortFns/aggregationFns slots in prerequisite order while preserving tree-shaking. Load when an API or state slice is missing or when choosing explicit features versus stockFeatures.' + requires: ['core'] + sources: + - 'TanStack/table:docs/guide/row-models.md' + - 'TanStack/table:packages/table-core/src/types/TableFeatures.ts' + - 'TanStack/table:packages/table-core/src/features/stockFeatures.ts' + - 'TanStack/table:packages/table-core/src/core/table/constructTable.ts' + + - name: 'Client vs Server' + slug: client-vs-server + type: sub-skill + domain: foundations + path: 'packages/table-core/skills/client-vs-server/SKILL.md' + package: 'packages/table-core' + description: 'Choose client or server ownership for filtering, grouping, sorting, expanding, and pagination. Explains create*RowModel pipelines, manual* bypass flags, mixed pipelines, server counts, stable processed data, and the exact dataset available to every stage.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/guide/row-models.md' + - 'TanStack/table:packages/table-core/src/core/row-models/coreRowModelsFeature.utils.ts' + - 'TanStack/table:examples/react/with-tanstack-query' + + - name: 'TypeScript' + slug: typescript + type: sub-skill + domain: foundations + path: 'packages/table-core/skills/typescript/SKILL.md' + package: 'packages/table-core' + description: 'Preserve TanStack Table v9 inference with createColumnHelper, columns(), tableOptions, tableMeta/columnMeta/filterMeta helpers, typeof features, and createTableHook. Load for ColumnDef errors, reusable generic tables, or unnecessary manual feature generics.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/guide/helpers.md' + - 'TanStack/table:docs/guide/column-defs.md' + - 'TanStack/table:docs/guide/table-and-column-meta.md' + - 'TanStack/table:packages/table-core/src/helpers' + + - name: 'API Not Found' + slug: api-not-found + type: sub-skill + domain: foundations + path: 'packages/table-core/skills/api-not-found/SKILL.md' + package: 'packages/table-core' + description: 'Diagnose missing TanStack Table exports, options, state slices, and instance methods by checking the installed package declarations (dist/**/*.d.ts), adapter/version mismatches, tableFeatures registration, and v9 prototype APIs before inventing replacements.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:packages/table-core/src/index.ts' + - 'TanStack/table:packages/table-core/src/types/TableFeatures.ts' + - 'TanStack/table:docs/framework/react/guide/migrating.md' + + - name: 'Custom Features' + slug: custom-features + type: sub-skill + domain: foundations + path: 'packages/table-core/skills/custom-features/SKILL.md' + package: 'packages/table-core' + description: 'Author a TanStack Table v9 feature plugin across all 10 FeatureMaps and every table/column/row/cell/header API installation path, including prototypes, memoDeps, instance data, and advanced row-model maps, after checking built-ins and meta.' + requires: ['core', 'table-features', 'typescript'] + sources: + - 'TanStack/table:docs/framework/react/guide/custom-features.md' + - 'TanStack/table:packages/table-core/src/types' + - 'TanStack/table:packages/table-core/src/types/TableFeatures.ts' + - 'TanStack/table:packages/table-core/src/utils.ts' + - 'TanStack/table:packages/table-core/src/features' + - 'TanStack/table:examples/react/custom-plugin' + + - name: 'Migrate v8 to v9' + slug: migrate-v8-to-v9 + type: lifecycle + domain: foundations + path: 'packages/table-core/skills/migrate-v8-to-v9/SKILL.md' + package: 'packages/table-core' + description: 'Perform a complete TanStack Table v8-to-v9 migration audit: all 16 feature registrations, every row-model and registry slot, state/store changes, prototype methods, full pinning and resizing mappings, sorting and selection semantics, removed internals, helpers, meta typing, and generic changes. useLegacyTable is only a deprecated temporary bridge when already encountered.' + requires: ['core', 'table-features', 'typescript'] + sources: + - 'TanStack/table:docs/framework/react/guide/migrating.md' + - 'TanStack/table:docs/framework/preact/guide/migrating.md' + - 'TanStack/table:docs/framework/solid/guide/migrating.md' + - 'TanStack/table:docs/framework/svelte/guide/migrating.md' + - 'TanStack/table:docs/framework/vue/guide/migrating.md' + - 'TanStack/table:docs/framework/angular/guide/migrating.md' + - 'TanStack/table:docs/framework/lit/guide/migrating.md' + - 'TanStack/table:packages/table-core/src/index.ts' + - 'TanStack/table:packages/table-core/src/types/TableFeatures.ts' + - 'TanStack/table:packages/table-core/src/features/column-pinning/columnPinningFeature.types.ts' + - 'TanStack/table:packages/table-core/src/features/column-resizing/columnResizingFeature.types.ts' + - 'TanStack/table:packages/react-table/src/legacy.ts' + + - name: 'Column Faceting' + slug: column-faceting + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/column-faceting/SKILL.md' + package: 'packages/table-core' + description: 'Build faceted filter UIs with columnFacetingFeature, facetedRowModel, facetedUniqueValues, and facetedMinMaxValues. Covers own-filter exclusion, other-filter context, incomplete server-page counts, and installed feature-source API lookup.' + requires: ['core', 'table-features', 'column-filtering'] + sources: + - 'TanStack/table:docs/framework/react/guide/column-faceting.md' + - 'TanStack/table:packages/table-core/src/features/column-faceting' + - 'TanStack/table:examples/react/filters-faceted' + + - name: 'Column Filtering' + slug: column-filtering + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/column-filtering/SKILL.md' + package: 'packages/table-core' + description: 'Filter columns with columnFilteringFeature, filteredRowModel, filterFns, filterMeta, nested-row direction, and manualFiltering. Load for accessor/filter compatibility, controlled filter updaters, or client/server filter ownership.' + requires: ['core', 'table-features', 'client-vs-server'] + sources: + - 'TanStack/table:docs/framework/react/guide/column-filtering.md' + - 'TanStack/table:packages/table-core/src/features/column-filtering' + - 'TanStack/table:examples/react/filters' + + - name: 'Grouping' + slug: grouping + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/grouping/SKILL.md' + package: 'packages/table-core' + description: 'Group and aggregate rows with columnGroupingFeature, groupedRowModel, aggregationFns, groupedColumnMode, and manualGrouping. Covers grouped/placeholder/aggregated cells plus expansion and pagination semantics.' + requires: ['core', 'table-features', 'client-vs-server'] + sources: + - 'TanStack/table:docs/framework/react/guide/grouping.md' + - 'TanStack/table:packages/table-core/src/features/column-grouping' + - 'TanStack/table:examples/react/grouping' + + - name: 'Aggregation' + slug: aggregation + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/aggregation/SKILL.md' + package: 'packages/table-core' + description: 'Aggregate columns independently of grouping with rowAggregationFeature, built-in or custom aggregation functions, caller-selected row scopes, keyed results, grouped merges, and manual values.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/guide/aggregation.md' + - 'TanStack/table:docs/framework/react/guide/aggregation.md' + - 'TanStack/table:packages/table-core/src/features/row-aggregation' + - 'TanStack/table:examples/react/aggregation' + - 'TanStack/table:examples/react/grouped-aggregation' + + - name: 'Column Ordering' + slug: column-ordering + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/column-ordering/SKILL.md' + package: 'packages/table-core' + description: 'Control leaf columnOrder with stable IDs while respecting pinning regions, visibility, and groupedColumnMode precedence. Load for column drag-and-drop or rendered order that differs from state.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/framework/react/guide/column-ordering.md' + - 'TanStack/table:packages/table-core/src/features/column-ordering' + - 'TanStack/table:examples/react/column-dnd' + + - name: 'Column Pinning' + slug: column-pinning + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/column-pinning/SKILL.md' + package: 'packages/table-core' + description: 'Pin columns into logical start/center/end regions with columnPinningFeature and implement sticky CSS, RTL logical offsets, z-index, backgrounds, overflow, and widths without gaps or overlaps.' + requires: ['core', 'table-features', 'column-sizing'] + sources: + - 'TanStack/table:docs/framework/react/guide/column-pinning.md' + - 'TanStack/table:packages/table-core/src/features/column-pinning' + - 'TanStack/table:examples/react/column-pinning-sticky' + + - name: 'Column Resizing' + slug: column-resizing + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/column-resizing/SKILL.md' + package: 'packages/table-core' + description: 'Wire columnResizingFeature, header.getResizeHandler, resize modes/direction, pointer or touch events, and performant CSS-variable updates. Requires columnSizingFeature and renderer-owned handles/styles.' + requires: ['core', 'table-features', 'column-sizing'] + sources: + - 'TanStack/table:docs/framework/react/guide/column-resizing.md' + - 'TanStack/table:packages/table-core/src/features/column-resizing' + - 'TanStack/table:examples/react/column-resizing-performant' + + - name: 'Column Sizing' + slug: column-sizing + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/column-sizing/SKILL.md' + package: 'packages/table-core' + description: 'Use columnSizingFeature numeric size/minSize/maxSize state, getSize/getStart/getAfter offsets, and total sizes in table, grid, or flex CSS. Load for auto/percentage misconceptions or sizing/pinning layout mismatch.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/framework/react/guide/column-sizing.md' + - 'TanStack/table:packages/table-core/src/features/column-sizing' + - 'TanStack/table:examples/react/column-sizing' + + - name: 'Column Visibility' + slug: column-visibility + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/column-visibility/SKILL.md' + package: 'packages/table-core' + description: 'Hide columns with columnVisibilityFeature while rendering visibility-aware header, column, and cell collections. Covers false-versus-absent state, enableHiding semantics, and hidden columns remaining in DOM through getAll APIs.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/framework/react/guide/column-visibility.md' + - 'TanStack/table:packages/table-core/src/features/column-visibility' + - 'TanStack/table:examples/react/column-visibility' + + - name: 'Global Filtering' + slug: global-filtering + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/global-filtering/SKILL.md' + package: 'packages/table-core' + description: 'Apply globalFilter across eligible columns with globalFilteringFeature, columnFilteringFeature, filteredRowModel, globalFilterFn, and manual server filtering. Covers default string/number eligibility and explicit exclusions.' + requires: ['core', 'table-features', 'client-vs-server', 'column-filtering'] + sources: + - 'TanStack/table:docs/framework/react/guide/global-filtering.md' + - 'TanStack/table:packages/table-core/src/features/global-filtering' + - 'TanStack/table:examples/react/filters' + + - name: 'Expanding' + slug: expanding + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/expanding/SKILL.md' + package: 'packages/table-core' + description: 'Expand hierarchical subrows or custom detail panels with rowExpandingFeature, expandedRowModel, getSubRows, getRowCanExpand, manualExpanding, and paginateExpandedRows. Separates expansion state from user-rendered detail UI.' + requires: ['core', 'table-features', 'client-vs-server'] + sources: + - 'TanStack/table:docs/framework/react/guide/expanding.md' + - 'TanStack/table:packages/table-core/src/features/row-expanding' + - 'TanStack/table:examples/react/expanding' + + - name: 'Pagination' + slug: pagination + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/pagination/SKILL.md' + package: 'packages/table-core' + description: 'Paginate with rowPaginationFeature and paginatedRowModel or manualPagination. Covers pageIndex/pageSize, rowCount/pageCount, next-page limits, already-paginated server data, and autoResetPageIndex surprises.' + requires: ['core', 'table-features', 'client-vs-server'] + sources: + - 'TanStack/table:docs/framework/react/guide/pagination.md' + - 'TanStack/table:packages/table-core/src/features/row-pagination' + - 'TanStack/table:examples/react/pagination' + + - name: 'Row Pinning' + slug: row-pinning + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/row-pinning/SKILL.md' + package: 'packages/table-core' + description: 'Pin stable row IDs into top/center/bottom collections with rowPinningFeature and keepPinnedRows. Covers getRowId, filtering/pagination visibility, explicit region rendering, and renderer-owned sticky CSS.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/framework/react/guide/row-pinning.md' + - 'TanStack/table:packages/table-core/src/features/row-pinning' + - 'TanStack/table:examples/react/row-pinning' + + - name: 'Cell Selection' + slug: cell-selection + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/cell-selection/SKILL.md' + package: 'packages/table-core' + description: 'Select, add, and subtract rectangular cell ranges with cellSelectionFeature: ordered include/exclude operations, modifier dragging, final positive bounds, selection edges, and render-order resolution under pinning. Load for spreadsheet-style or “select all except” behavior.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/framework/react/guide/cell-selection.md' + - 'TanStack/table:packages/table-core/src/features/cell-selection' + - 'TanStack/table:examples/react/cell-selection' + + - name: 'Cell Spanning' + slug: cell-spanning + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/cell-spanning/SKILL.md' + package: 'packages/table-core' + description: 'Merge adjacent body cells with cellSpanningFeature: value-based rowSpan via spanRows, per-row colSpan via spanColumns, and the covered-cell convention where a span of 0 means skip the cell. Load for merged grids, spans lost after sorting or paging, or ragged rows.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/framework/react/guide/cell-spanning.md' + - 'TanStack/table:packages/table-core/src/features/cell-spanning' + - 'TanStack/table:examples/react/cell-spanning' + + - name: 'Row Selection' + slug: row-selection + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/row-selection/SKILL.md' + package: 'packages/table-core' + description: 'Maintain rowSelection ID state with stable getRowId, single/multi/subrow rules, current/filtered/grouped selected models, and manual-pagination semantics. Load when selected IDs outlive loaded Row objects or data removal.' + requires: ['core', 'table-features'] + sources: + - 'TanStack/table:docs/framework/react/guide/row-selection.md' + - 'TanStack/table:packages/table-core/src/features/row-selection' + - 'TanStack/table:examples/react/row-selection' + + - name: 'Sorting' + slug: sorting + type: sub-skill + domain: feature-plugins + path: 'packages/table-core/skills/sorting/SKILL.md' + package: 'packages/table-core' + description: 'Sort with rowSortingFeature, sortedRowModel, sortFns, multi-sort/removal options, sortUndefined, and manualSorting. Covers comparator direction, incoming server order, and explicit product sorting cycles.' + requires: ['core', 'table-features', 'client-vs-server'] + sources: + - 'TanStack/table:docs/framework/react/guide/sorting.md' + - 'TanStack/table:packages/table-core/src/features/row-sorting' + - 'TanStack/table:examples/react/sorting' + + - name: 'React Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/react-table/skills/getting-started/SKILL.md' + package: 'packages/react-table' + description: 'Create a TanStack React Table v9 table with useTable, tableFeatures, stable data/columns, header and row models, and FlexRender/flexRender. Load for a first React table or when adapting a kitchen-sink example without importing every feature.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/react/guide/migrating.md' + - 'TanStack/table:examples/react/basic-use-table' + - 'TanStack/table:packages/react-table/src/index.ts' + + - name: 'React Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/react-table/skills/table-state/SKILL.md' + package: 'packages/react-table' + description: 'Read and own React Table v9 state with selected table.state, useTable selectors, table.Subscribe/Subscribe, table.atoms, table.store, controlled state plus on*Change, and external TanStack Store atoms. Includes React Compiler builder-method subscription foot-guns.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/react/guide/table-state.md' + - 'TanStack/table:docs/framework/react/guide/react-compiler.md' + - 'TanStack/table:examples/react/basic-subscribe' + - 'TanStack/table:packages/react-table/src/Subscribe.ts' + - 'TanStack/table:packages/react-table/src/useTable.ts' + + - name: 'React Migrate v8 to v9' + slug: migrate-v8-to-v9 + type: lifecycle + domain: framework-adapters + path: 'packages/react-table/skills/migrate-v8-to-v9/SKILL.md' + package: 'packages/react-table' + description: 'Migrate @tanstack/react-table from v8 useReactTable to v9 useTable, explicit tableFeatures, create*RowModel slots, atomic state, FlexRender/Subscribe, helper changes, and composable tables. Treat useLegacyTable only as a deprecated bridge when already present.' + requires: + [ + '@tanstack/table-core#migrate-v8-to-v9', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:docs/framework/react/guide/migrating.md' + - 'TanStack/table:packages/react-table/src/index.ts' + - 'TanStack/table:examples/react/basic-use-table' + - 'TanStack/table:packages/react-table/src/legacy.ts' + + - name: 'React createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/react-table/skills/create-table-hook/SKILL.md' + package: 'packages/react-table' + description: 'Create an app-level React table factory with createTableHook, useAppTable, createAppColumnHelper, shared features/defaults, optional component registries, AppTable/AppCell/AppHeader wrappers, and typed context hooks. Covers scoped contexts, HMR circular imports, and context over prop drilling.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/react/guide/composable-tables.md' + - 'TanStack/table:docs/framework/react/guide/table-context.md' + - 'TanStack/table:examples/react/composable-tables' + - 'TanStack/table:packages/react-table/src/createTableHook.tsx' + - 'TanStack/table:packages/react-table/src/createTableHookContexts.tsx' + + - name: 'React with TanStack Query' + slug: with-tanstack-query + type: composition + domain: framework-adapters + path: 'packages/react-table/skills/with-tanstack-query/SKILL.md' + package: 'packages/react-table' + description: 'Compose React Table v9 with TanStack Query for server filtering, sorting, pagination, and infinite data. Covers table state in query keys, manual* boundaries, server counts, previous data, and using query results directly rather than duplicating state.' + requires: + [ + '@tanstack/table-core#client-vs-server', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:examples/react/with-tanstack-query' + - 'TanStack/table:examples/react/virtualized-infinite-scrolling' + - 'TanStack/table:docs/framework/react/guide/pagination.md' + + - name: 'React with TanStack Virtual' + slug: with-tanstack-virtual + type: composition + domain: framework-adapters + path: 'packages/react-table/skills/with-tanstack-virtual/SKILL.md' + package: 'packages/react-table' + description: 'Virtualize final React Table row or column models with TanStack Virtual. Covers scroll elements, stable row keys, data-index measurement, dynamic heights, sticky headers/columns, grid/flex geometry, infinite fetching, and Virtual as renderer composition rather than a Table feature.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/react/guide/virtualization.md' + - 'TanStack/table:examples/react/virtualized-rows' + - 'TanStack/table:examples/react/virtualized-columns' + - 'TanStack/table:examples/react/virtualized-infinite-scrolling' + + - name: 'Preact Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/preact-table/skills/getting-started/SKILL.md' + package: 'packages/preact-table' + description: 'Create a native @tanstack/preact-table v9 table with useTable, tableFeatures, stable inputs, and Preact render helpers. Load when replacing @tanstack/react-table through preact/compat or starting a Preact table.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/preact/guide/migrating.md' + - 'TanStack/table:examples/preact/basic-use-table' + - 'TanStack/table:packages/preact-table/src/index.ts' + + - name: 'Preact Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/preact-table/skills/table-state/SKILL.md' + package: 'packages/preact-table' + description: 'Read and own Preact Table state with selected table.state, useTable selectors, table.Subscribe, table.atoms/store, controlled slices, and external Preact Store atoms. Distinguishes current snapshots from reactive subscriptions and avoids React-package imports.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/preact/guide/table-state.md' + - 'TanStack/table:examples/preact/basic-subscribe' + - 'TanStack/table:packages/preact-table/src/useTable.ts' + + - name: 'Preact Migrate v8 to v9' + slug: migrate-v8-to-v9 + type: lifecycle + domain: framework-adapters + path: 'packages/preact-table/skills/migrate-v8-to-v9/SKILL.md' + package: 'packages/preact-table' + description: 'Move a Preact v8 app from the React adapter/preact/compat to native @tanstack/preact-table v9, useTable, tableFeatures row models, atom-backed state, and current render helpers while applying shared v9 breaking changes.' + requires: + [ + '@tanstack/table-core#migrate-v8-to-v9', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:docs/framework/preact/guide/migrating.md' + - 'TanStack/table:packages/preact-table/src/index.ts' + - 'TanStack/table:examples/preact/basic-use-table' + + - name: 'Preact createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/preact-table/skills/create-table-hook/SKILL.md' + package: 'packages/preact-table' + description: 'Create a reusable Preact useAppTable and createAppColumnHelper with shared features/options, optional App component registries, typed table/cell/header context hooks, scoped contexts, and correct provider subscription boundaries.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/preact/guide/composable-tables.md' + - 'TanStack/table:docs/framework/preact/guide/table-context.md' + - 'TanStack/table:examples/preact/composable-tables' + - 'TanStack/table:packages/preact-table/src/createTableHook.tsx' + - 'TanStack/table:packages/preact-table/src/createTableHookContexts.tsx' + + - name: 'Preact with TanStack Query' + slug: with-tanstack-query + type: composition + domain: framework-adapters + path: 'packages/preact-table/skills/with-tanstack-query/SKILL.md' + package: 'packages/preact-table' + description: 'Compose native Preact Table with TanStack Query using table state in query keys, already-processed server pages, manual filtering/sorting/pagination, server counts, and Preact-specific query/state APIs.' + requires: + [ + '@tanstack/table-core#client-vs-server', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:examples/preact/with-tanstack-query' + - 'TanStack/table:docs/framework/preact/guide/pagination.md' + + - name: 'Preact with TanStack Virtual' + slug: with-tanstack-virtual + type: composition + domain: framework-adapters + path: 'packages/preact-table/skills/with-tanstack-virtual/SKILL.md' + package: 'packages/preact-table' + description: 'Apply Preact Virtual rendering to the final Preact Table row or column model. Uses the maintained adapter guide and installed source for exact APIs; covers counts, scroll targets, keys, measurements, spacer geometry, and renderer-owned sticky/sizing CSS.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/preact/guide/virtualization.md' + - 'TanStack/table:packages/preact-table/src/index.ts' + + - name: 'Solid Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/solid-table/skills/getting-started/SKILL.md' + package: 'packages/solid-table' + description: 'Create a Solid Table v9 table with createTable, explicit tableFeatures, reactive data getters, stable static inputs, and Solid JSX rendering. Load when replacing createSolidTable or adapting React examples.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/solid/guide/migrating.md' + - 'TanStack/table:examples/solid/basic-use-table' + - 'TanStack/table:packages/solid-table/src/index.tsx' + + - name: 'Solid Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/solid-table/skills/table-state/SKILL.md' + package: 'packages/solid-table' + description: 'Read Solid-backed table.atoms inside JSX, createMemo, createEffect, or table.Subscribe; own slices with native signals or external TanStack Store atoms; and apply value-or-updater callbacks without React-style rerender workarounds.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/solid/guide/table-state.md' + - 'TanStack/table:examples/solid/basic-external-state' + - 'TanStack/table:packages/solid-table/src/createTable.ts' + + - name: 'Solid Migrate v8 to v9' + slug: migrate-v8-to-v9 + type: lifecycle + domain: framework-adapters + path: 'packages/solid-table/skills/migrate-v8-to-v9/SKILL.md' + package: 'packages/solid-table' + description: 'Migrate Solid from v8 createSolidTable to v9 createTable, explicit features and row-model slots, signal-backed atoms, current helpers/rendering, prototype methods, sortFn names, and logical start/end pinning.' + requires: + [ + '@tanstack/table-core#migrate-v8-to-v9', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:docs/framework/solid/guide/migrating.md' + - 'TanStack/table:packages/solid-table/src/index.tsx' + - 'TanStack/table:examples/solid/basic-use-table' + + - name: 'Solid createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/solid-table/skills/create-table-hook/SKILL.md' + package: 'packages/solid-table' + description: 'Create a reusable Solid createAppTable/createAppColumnHelper with shared features/defaults, reactive per-table getters, optional App component registries, and typed context hooks for registered table/cell/header UI.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/solid/guide/composable-tables.md' + - 'TanStack/table:examples/solid/composable-tables' + - 'TanStack/table:packages/solid-table/src/createTableHook.tsx' + + - name: 'Solid with TanStack Query' + slug: with-tanstack-query + type: composition + domain: framework-adapters + path: 'packages/solid-table/skills/with-tanstack-query/SKILL.md' + package: 'packages/solid-table' + description: 'Compose Solid Query reactive options with Solid Table manual processing. Covers tracked query-key signals, query result getters, server counts, and avoiding React Query patterns or duplicated data state.' + requires: + [ + '@tanstack/table-core#client-vs-server', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:examples/solid/with-tanstack-query' + - 'TanStack/table:docs/framework/solid/guide/pagination.md' + + - name: 'Solid with TanStack Virtual' + slug: with-tanstack-virtual + type: composition + domain: framework-adapters + path: 'packages/solid-table/skills/with-tanstack-virtual/SKILL.md' + package: 'packages/solid-table' + description: 'Virtualize Solid Table row/column models and infinite Query data with reactive counts, scroll targets, stable keys, dynamic measurement, transforms, sticky regions, and grid/flex sizing.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/solid/guide/virtualization.md' + - 'TanStack/table:examples/solid/virtualized-rows' + - 'TanStack/table:examples/solid/virtualized-columns' + - 'TanStack/table:examples/solid/virtualized-infinite-scrolling' + + - name: 'Svelte Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/svelte-table/skills/getting-started/SKILL.md' + package: 'packages/svelte-table' + description: 'Create a Svelte 5 TanStack Table v9 table with createTable, explicit tableFeatures, rune-backed data getters, stable static inputs, FlexRender, and headless markup. Load when replacing createSvelteTable or pre-rune patterns.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/svelte/guide/migrating.md' + - 'TanStack/table:examples/svelte/basic-create-table' + - 'TanStack/table:packages/svelte-table/src/index.ts' + + - name: 'Svelte Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/svelte-table/skills/table-state/SKILL.md' + package: 'packages/svelte-table' + description: 'Use Svelte 5 rune-aware table atoms and stores, $derived projections, reactive option getters, controlled $state or createTableState slices, external atoms, and auto-reset behavior without broad invalidation or snapshot mismatches.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/svelte/guide/table-state.md' + - 'TanStack/table:docs/framework/svelte/guide/pagination.md' + - 'TanStack/table:examples/svelte/basic-external-state' + - 'TanStack/table:packages/svelte-table/src/createTable.svelte.ts' + - 'TanStack/table:packages/svelte-table/src/createTableState.svelte.ts' + + - name: 'Svelte Migrate v8 to v9' + slug: migrate-v8-to-v9 + type: lifecycle + domain: framework-adapters + path: 'packages/svelte-table/skills/migrate-v8-to-v9/SKILL.md' + package: 'packages/svelte-table' + description: 'Complete Svelte v8-to-v9 migration reference: Svelte 5, createTable, beta.59 selector removal, explicit features and row-model slots, atom/rune state, rendering helpers, prototype methods, type generics, sorting, sizing, selection, and logical pinning.' + requires: + [ + '@tanstack/table-core#migrate-v8-to-v9', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:docs/framework/svelte/guide/migrating.md' + - 'TanStack/table:packages/svelte-table/src/index.ts' + - 'TanStack/table:examples/svelte/basic-create-table' + + - name: 'Svelte createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/svelte-table/skills/create-table-hook/SKILL.md' + package: 'packages/svelte-table' + description: "Define a Svelte createAppTable/createAppColumnHelper using the adapter's rune-capable createTableHook implementation, shared features/defaults, reactive per-table getters, optional App component registries, and typed table/cell/header context hooks." + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/svelte/guide/composable-tables.md' + - 'TanStack/table:examples/svelte/composable-tables' + - 'TanStack/table:packages/svelte-table/src/createTableHook.svelte.ts' + + - name: 'Svelte with TanStack Query' + slug: with-tanstack-query + type: composition + domain: framework-adapters + path: 'packages/svelte-table/skills/with-tanstack-query/SKILL.md' + package: 'packages/svelte-table' + description: 'Compose Svelte Query with Svelte Table manual filtering, sorting, and pagination using reactive query inputs, query-result data getters, server counts, and a single source of server-data truth.' + requires: + [ + '@tanstack/table-core#client-vs-server', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:examples/svelte/with-tanstack-query' + - 'TanStack/table:docs/framework/svelte/guide/pagination.md' + + - name: 'Svelte with TanStack Virtual' + slug: with-tanstack-virtual + type: composition + domain: framework-adapters + path: 'packages/svelte-table/skills/with-tanstack-virtual/SKILL.md' + package: 'packages/svelte-table' + description: 'Virtualize Svelte Table final row or column models with reactive counts and scroll targets, stable keys, dynamic measurement, absolute transforms, sticky regions, grid/flex sizing, and infinite data.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/svelte/guide/virtualization.md' + - 'TanStack/table:examples/svelte/virtualized-rows' + - 'TanStack/table:examples/svelte/virtualized-columns' + - 'TanStack/table:examples/svelte/virtualized-infinite-scrolling' + + - name: 'Vue Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/vue-table/skills/getting-started/SKILL.md' + package: 'packages/vue-table' + description: 'Create a Vue TanStack Table v9 table with useTable, explicit tableFeatures, stable columns/features, reactive ref or computed data, and Vue template/render helpers without destructuring reactive snapshots.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/vue/guide/migrating.md' + - 'TanStack/table:examples/vue/basic-use-table' + - 'TanStack/table:packages/vue-table/src/index.ts' + + - name: 'Vue Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/vue-table/skills/table-state/SKILL.md' + package: 'packages/vue-table' + description: 'Read Vue-backed table.atoms/store in templates, computed, watch, or table.Subscribe; own slices with refs/computed or external Vue Store atoms; and apply updater callbacks while preserving reactive option shapes.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/vue/guide/table-state.md' + - 'TanStack/table:examples/vue/basic-external-state' + - 'TanStack/table:packages/vue-table/src/useTable.ts' + + - name: 'Vue Migrate v8 to v9' + slug: migrate-v8-to-v9 + type: lifecycle + domain: framework-adapters + path: 'packages/vue-table/skills/migrate-v8-to-v9/SKILL.md' + package: 'packages/vue-table' + description: 'Migrate Vue v8 construction, implicit features, row-model options, state, helpers, rendering, instance methods, sorting names, meta typing, and physical pinning to the Vue v9 useTable architecture.' + requires: + [ + '@tanstack/table-core#migrate-v8-to-v9', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:docs/framework/vue/guide/migrating.md' + - 'TanStack/table:packages/vue-table/src/index.ts' + - 'TanStack/table:examples/vue/basic-use-table' + + - name: 'Vue createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/vue-table/skills/create-table-hook/SKILL.md' + package: 'packages/vue-table' + description: 'Create a reusable Vue useAppTable/createAppColumnHelper with shared features/defaults, reactive per-table options, optional component registries, dynamic App wrappers, typed context hooks, and explicit types that break circular inference.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/vue/guide/composable-tables.md' + - 'TanStack/table:examples/vue/composable-tables' + - 'TanStack/table:packages/vue-table/src/createTableHook.ts' + + - name: 'Vue with TanStack Query' + slug: with-tanstack-query + type: composition + domain: framework-adapters + path: 'packages/vue-table/skills/with-tanstack-query/SKILL.md' + package: 'packages/vue-table' + description: 'Compose reactive Vue Query keys and results with Vue Table manual row processing, refs/computed state, server counts, and already-processed pages without duplicating query data into a drifting local ref.' + requires: + [ + '@tanstack/table-core#client-vs-server', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:examples/vue/with-tanstack-query' + - 'TanStack/table:docs/framework/vue/guide/pagination.md' + + - name: 'Vue with TanStack Virtual' + slug: with-tanstack-virtual + type: composition + domain: framework-adapters + path: 'packages/vue-table/skills/with-tanstack-virtual/SKILL.md' + package: 'packages/vue-table' + description: 'Virtualize Vue Table final row or column models with reactive counts/scroll targets, stable keys, measurement, spacer geometry, sticky CSS, grid/flex widths, and infinite server data.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/vue/guide/virtualization.md' + - 'TanStack/table:examples/vue/virtualized-rows' + - 'TanStack/table:examples/vue/virtualized-columns' + - 'TanStack/table:examples/vue/virtualized-infinite-scrolling' + + - name: 'Angular Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/angular-table/skills/getting-started/SKILL.md' + package: 'packages/angular-table' + description: 'Create an Angular TanStack Table v9 table with injectTable inside injection context, explicit stable tableFeatures/columns, signal-backed data, and FlexRender structural directives or helpers.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/angular/guide/migrating.md' + - 'TanStack/table:docs/framework/angular/guide/rendering.md' + - 'TanStack/table:examples/angular/basic-inject-table' + - 'TanStack/table:packages/angular-table/src/index.ts' + + - name: 'Angular Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/angular-table/skills/table-state/SKILL.md' + package: 'packages/angular-table' + description: 'Use Angular-signal-backed table.atoms, direct template reads, computed selectors, controlled signals, value-or-updater callbacks, and external Angular Store atoms while accounting for injectTable initializer reruns.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/angular/guide/table-state.md' + - 'TanStack/table:examples/angular/basic-external-state' + - 'TanStack/table:packages/angular-table/src/injectTable.ts' + + - name: 'Angular Migrate v8 to v9' + slug: migrate-v8-to-v9 + type: lifecycle + domain: framework-adapters + path: 'packages/angular-table/skills/migrate-v8-to-v9/SKILL.md' + package: 'packages/angular-table' + description: 'Migrate createAngularTable v8 code to injectTable v9 in injection context, tableFeatures row models, signal-backed state, stable reactive initializers, current FlexRender directives, helpers, prototype methods, sorting, and logical pinning.' + requires: + [ + '@tanstack/table-core#migrate-v8-to-v9', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:docs/framework/angular/guide/migrating.md' + - 'TanStack/table:packages/angular-table/src/index.ts' + - 'TanStack/table:examples/angular/basic-inject-table' + + - name: 'Angular createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/angular-table/skills/create-table-hook/SKILL.md' + package: 'packages/angular-table' + description: 'Create an Angular injectAppTable/createAppColumnHelper abstraction with shared features/defaults, registered components, injectTableContext/injectTableCellContext/injectTableHeaderContext, and correct FlexRender component versus function handling.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/angular/guide/composable-tables.md' + - 'TanStack/table:examples/angular/composable-tables' + - 'TanStack/table:packages/angular-table/src/helpers/createTableHook.ts' + + - name: 'Angular with TanStack Query' + slug: with-tanstack-query + type: composition + domain: framework-adapters + path: 'packages/angular-table/skills/with-tanstack-query/SKILL.md' + package: 'packages/angular-table' + description: 'Compose Angular Query with signal-owned Table filtering, sorting, and pagination state. Covers reactive query options/keys, manual row-model boundaries, direct query data, server row/page counts, and injection context.' + requires: + [ + '@tanstack/table-core#client-vs-server', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:examples/angular/with-tanstack-query' + - 'TanStack/table:docs/framework/angular/guide/table-state.md' + - 'TanStack/table:docs/framework/angular/guide/pagination.md' + + - name: 'Angular with TanStack Virtual' + slug: with-tanstack-virtual + type: composition + domain: framework-adapters + path: 'packages/angular-table/skills/with-tanstack-virtual/SKILL.md' + package: 'packages/angular-table' + description: 'Virtualize Angular Table final row or column models inside the correct injection/reactive lifecycle. Covers signal counts, scroll elements, keys, measurement, transforms, sticky regions, grid/flex sizing, and infinite data.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/angular/guide/virtualization.md' + - 'TanStack/table:examples/angular/virtualized-rows' + - 'TanStack/table:examples/angular/virtualized-columns' + - 'TanStack/table:examples/angular/virtualized-infinite-scrolling' + + - name: 'Lit Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/lit-table/skills/getting-started/SKILL.md' + package: 'packages/lit-table' + description: 'Create a Lit TanStack Table v9 table with a stable TableController host field, explicit tableFeatures, controller.table(options, selector) during render, and headless Lit templates.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/lit/guide/migrating.md' + - 'TanStack/table:examples/lit/basic-table-controller' + - 'TanStack/table:packages/lit-table/src/index.ts' + + - name: 'Lit Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/lit-table/skills/table-state/SKILL.md' + package: 'packages/lit-table' + description: 'Use TableController-selected table.state, table.atoms/store, stable table.subscribe selectors, controlled reactive properties plus on*Change, and external TanStack Store atoms while preserving Lit host update behavior.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/lit/guide/table-state.md' + - 'TanStack/table:examples/lit/basic-external-state' + - 'TanStack/table:packages/lit-table/src/TableController.ts' + + - name: 'Lit Migrate v8 to v9' + slug: migrate-v8-to-v9 + type: lifecycle + domain: framework-adapters + path: 'packages/lit-table/skills/migrate-v8-to-v9/SKILL.md' + package: 'packages/lit-table' + description: 'Migrate Lit v8 controller construction to a stable v9 TableController plus controller.table(options), explicit feature/row-model slots, selected state, helpers, prototype methods, sortFn names, and logical start/end pinning.' + requires: + [ + '@tanstack/table-core#migrate-v8-to-v9', + 'getting-started', + 'table-state', + ] + sources: + - 'TanStack/table:docs/framework/lit/guide/migrating.md' + - 'TanStack/table:packages/lit-table/src/index.ts' + - 'TanStack/table:examples/lit/basic-table-controller' + + - name: 'Lit createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/lit-table/skills/create-table-hook/SKILL.md' + package: 'packages/lit-table' + description: 'Create a reusable Lit useAppTable/createAppColumnHelper layer with host-backed controllers, shared features/defaults, typed cell/header renderers, and useTableContext for custom-element controls instead of prop drilling.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/lit/guide/composable-tables.md' + - 'TanStack/table:examples/lit/composable-tables' + - 'TanStack/table:packages/lit-table/src/createTableHook.ts' + + - name: 'Lit with TanStack Virtual' + slug: with-tanstack-virtual + type: composition + domain: framework-adapters + path: 'packages/lit-table/skills/with-tanstack-virtual/SKILL.md' + package: 'packages/lit-table' + description: 'Virtualize Lit Table final row or column models with host lifecycle-aware virtualizers, stable counts/keys, dynamic measurement, scroll geometry, sticky CSS, grid/flex sizing, and infinite data.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/lit/guide/virtualization.md' + - 'TanStack/table:examples/lit/virtualized-rows' + - 'TanStack/table:examples/lit/virtualized-columns' + - 'TanStack/table:examples/lit/virtualized-infinite-scrolling' + + - name: 'Octane Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/octane-table/skills/getting-started/SKILL.md' + package: 'packages/octane-table' + description: 'Create an @tanstack/octane-table v9 table with useTable, tableFeatures, stable inputs, TSRX component bodies, keyed @for rendering, and FlexRender. Load when starting an Octane table or translating a React/Preact example without changing its behavior.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/octane/quick-start.md' + - 'TanStack/table:examples/octane/basic-use-table' + - 'TanStack/table:packages/octane-table/src/index.ts' + - 'TanStack/table:packages/octane-table/src/useTable.tsrx' + + - name: 'Octane Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/octane-table/skills/table-state/SKILL.md' + package: 'packages/octane-table' + description: 'Read and own Octane Table v9 state with useTable selectors, table.state, table.Subscribe, controlled slices, and @tanstack/octane-store atoms. Load for state ownership, render timing, snapshot-versus-subscription bugs, or fine-grained rendering.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/octane/guide/table-state.md' + - 'TanStack/table:examples/octane/basic-subscribe' + - 'TanStack/table:examples/octane/basic-external-atoms' + - 'TanStack/table:packages/octane-table/src/useTable.tsrx' + - 'TanStack/table:packages/octane-table/src/Subscribe.tsrx' + + - name: 'Octane createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/octane-table/skills/create-table-hook/SKILL.md' + package: 'packages/octane-table' + description: 'Create reusable Octane table infrastructure with createTableHook, useAppTable, createAppColumnHelper, registered components, stable App wrappers, and typed context hooks. Load for recurring conventions, scoped contexts, or prop drilling.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/octane/guide/composable-tables.md' + - 'TanStack/table:docs/framework/octane/guide/table-context.md' + - 'TanStack/table:examples/octane/composable-tables' + - 'TanStack/table:packages/octane-table/src/createTableHook.tsrx' + - 'TanStack/table:packages/octane-table/src/createTableHookContexts.ts' + + - name: 'Ember Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/ember-table/skills/getting-started/SKILL.md' + package: 'packages/ember-table' + description: "Create a TanStack Ember Table v9 table with useTable, a tracked options thunk, stable tableFeatures and columns, .gts templates, FlexRenderCell/Header/Footer, and correctly bound template helpers. Load for first-table setup, Glimmer reactivity, component cell renderers, or adapting another framework's example to Ember." + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/ember/quick-start.md' + - 'TanStack/table:examples/ember/basic-table' + - 'TanStack/table:packages/ember-table/src/index.ts' + - 'TanStack/table:packages/ember-table/src/use-table.ts' + - 'TanStack/table:packages/ember-table/src/FlexRender.gts' + + - name: 'Ember Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/ember-table/skills/table-state/SKILL.md' + package: 'packages/ember-table' + description: 'Read, track, initialize, control, and reset TanStack Ember Table v9 state through Glimmer-reactive table APIs, baseAtoms, atoms, store, Ember createAtom, or @tracked state plus on*Change. Load for ownership precedence, updater callbacks, stale template state, options-thunk reactivity, or incorrect table.Subscribe/store.subscribe usage.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/ember/guide/table-state.md' + - 'TanStack/table:examples/ember/basic-external-atoms' + - 'TanStack/table:examples/ember/basic-external-state' + - 'TanStack/table:packages/ember-table/src/use-table.ts' + - 'TanStack/table:packages/ember-table/src/reactivity.ts' + - 'TanStack/table:packages/ember-table/src/signal.ts' + + - name: 'Ember createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/ember-table/skills/create-table-hook/SKILL.md' + package: 'packages/ember-table' + description: 'Share TanStack Ember Table v9 features, row-model slots, defaults, and inferred column helpers with createTableHook, createAppTable, createAppColumnHelper, and appFeatures. Load for recurring Ember table conventions, per-table overrides, or confusion with component/context registries from other adapters.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/ember/guide/composable-tables.md' + - 'TanStack/table:examples/ember/basic-app-table' + - 'TanStack/table:packages/ember-table/src/create-table-hook.ts' + + - name: 'Alpine Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/alpine-table/skills/getting-started/SKILL.md' + package: 'packages/alpine-table' + description: 'Create an Alpine TanStack Table v9 table with createTable, explicit tableFeatures, Alpine.reactive data/getters, real x-for/x-text/x-html markup, and renderer-owned styles. Covers why x-html does not initialize nested Alpine directives.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/alpine/guide/table-state.md' + - 'TanStack/table:examples/alpine/basic-create-table' + - 'TanStack/table:packages/alpine-table/src/index.ts' + + - name: 'Alpine Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/alpine-table/skills/table-state/SKILL.md' + package: 'packages/alpine-table' + description: 'Read automatically reactive Alpine table APIs directly in bindings; own slices with Alpine.reactive getters plus on*Change or external TanStack Store atoms; and apply updater callbacks without inventing table.Subscribe.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/alpine/guide/table-state.md' + - 'TanStack/table:examples/alpine/basic-create-table' + - 'TanStack/table:packages/alpine-table/src/createTable.ts' + + - name: 'Alpine createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/alpine-table/skills/create-table-hook/SKILL.md' + package: 'packages/alpine-table' + description: 'Share Alpine tableFeatures, defaults, createAppTable, and createAppColumnHelper with createTableHook. Clarifies that Alpine has no registered component/context registry and interactive reuse belongs in real markup or Alpine.bind bundles.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/alpine/guide/composable-tables.md' + - 'TanStack/table:examples/alpine/basic-app-table' + - 'TanStack/table:packages/alpine-table/src/createTableHook.ts' + + - name: 'Table Devtools' + slug: devtools + type: core + domain: observability + path: 'packages/table-devtools/skills/devtools/SKILL.md' + package: 'packages/table-devtools' + description: 'Register TanStack Table targets and inspect options, state, features, columns, rows, and row models with @tanstack/table-devtools. Load for missing connections, required unique table options.key, target replacement/cleanup, or explicit production entrypoints.' + requires: ['@tanstack/table-core#core'] + sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/table-devtools/src/index.ts' + - 'TanStack/table:packages/table-devtools/src/tableTarget.ts' + - 'TanStack/table:packages/table-devtools/src/production.ts' + + - name: 'React Table Devtools' + slug: devtools + type: framework + domain: observability + path: 'packages/react-table-devtools/skills/devtools/SKILL.md' + package: 'packages/react-table-devtools' + description: 'Connect React Table v9 instances to Devtools with ReactTableDevtools, useTanStackTableDevtools, or the plugin lifecycle. Covers stable table identity, required unique options.key, cleanup, enabled state, and development-gated exports.' + requires: ['@tanstack/table-core#core', '@tanstack/table-devtools#devtools'] + sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/react-table-devtools/src/index.ts' + - 'TanStack/table:packages/react-table-devtools/src/useTanStackTableDevtools.ts' + + - name: 'Preact Table Devtools' + slug: devtools + type: framework + domain: observability + path: 'packages/preact-table-devtools/skills/devtools/SKILL.md' + package: 'packages/preact-table-devtools' + description: 'Connect native Preact Table instances to Devtools with the Preact component, hook, or plugin lifecycle. Covers correct package imports, required unique options.key, target cleanup, enabled state, and development gating.' + requires: ['@tanstack/table-core#core', '@tanstack/table-devtools#devtools'] + sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/preact-table-devtools/src/index.ts' + - 'TanStack/table:packages/preact-table-devtools/src/useTanStackTableDevtools.ts' + + - name: 'Solid Table Devtools' + slug: devtools + type: framework + domain: observability + path: 'packages/solid-table-devtools/skills/devtools/SKILL.md' + package: 'packages/solid-table-devtools' + description: 'Connect Solid Table instances to Devtools in the proper reactive owner using the Solid component, hook, or plugin. Covers required unique options.key, target cleanup, enabled state, and development versus explicit production exports.' + requires: ['@tanstack/table-core#core', '@tanstack/table-devtools#devtools'] + sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/solid-table-devtools/src/index.ts' + - 'TanStack/table:packages/solid-table-devtools/src/useTanStackTableDevtools.ts' + + - name: 'Vue Table Devtools' + slug: devtools + type: framework + domain: observability + path: 'packages/vue-table-devtools/skills/devtools/SKILL.md' + package: 'packages/vue-table-devtools' + description: 'Connect Vue Table refs to Devtools with reactive target tracking, required unique options.key, lifecycle cleanup, enabled state, and development-gated component/plugin exports.' + requires: ['@tanstack/table-core#core', '@tanstack/table-devtools#devtools'] + sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/vue-table-devtools/src/index.ts' + - 'TanStack/table:packages/vue-table-devtools/src/useTanStackTableDevtools.ts' + + - name: 'Angular Table Devtools' + slug: devtools + type: framework + domain: observability + path: 'packages/angular-table-devtools/skills/devtools/SKILL.md' + package: 'packages/angular-table-devtools' + description: 'Register Angular Table instances with injectTanStackTableDevtools inside injection context. Covers reactive options, enabled/undefined tables, required unique options.key, cleanup, Angular isDevMode gating, and explicit production exports.' + requires: ['@tanstack/table-core#core', '@tanstack/table-devtools#devtools'] + sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/angular-table-devtools/src/index.ts' + - 'TanStack/table:packages/angular-table-devtools/src/injectTanStackTableDevtools.ts' + + - name: 'Fuzzy Ranking' + slug: fuzzy-ranking + type: core + domain: utilities + path: 'packages/match-sorter-utils/skills/fuzzy-ranking/SKILL.md' + package: 'packages/match-sorter-utils' + description: 'Rank fuzzy matches with rankItem, filter with RankingInfo.passed, compare saved ranking metadata with compareItems, and configure rankings, thresholds, accessors, bounds, and diacritic handling. Routes TanStack Table filterMeta wiring to filtering skills.' + sources: + - 'TanStack/table:packages/match-sorter-utils/src/index.ts' + - 'TanStack/table:docs/framework/react/guide/fuzzy-filtering.md' + - 'TanStack/table:examples/react/filters-fuzzy' diff --git a/babel.config.cjs b/babel.config.cjs deleted file mode 100644 index 84a3ee4ebe..0000000000 --- a/babel.config.cjs +++ /dev/null @@ -1,35 +0,0 @@ -const { NODE_ENV, BABEL_ENV } = process.env -const cjs = NODE_ENV === 'test' || BABEL_ENV === 'commonjs' -const loose = true - -module.exports = { - targets: 'defaults, not ie 11, not ie_mob 11', - presets: [ - [ - '@babel/preset-env', - { - loose, - modules: false, - include: [ - '@babel/plugin-proposal-nullish-coalescing-operator', - '@babel/plugin-proposal-optional-chaining', - ], - // exclude: ['@babel/plugin-transform-regenerator'], - }, - ], - '@babel/react', - '@babel/preset-typescript', - ], - plugins: [ - cjs && ['@babel/transform-modules-commonjs', { loose }], - // [ - // '@babel/transform-runtime', - // { - // useESModules: !cjs, - // version: require('./package.json').dependencies[ - // '@babel/runtime' - // ].replace(/^[^0-9]*/, ''), - // }, - // ], - ].filter(Boolean), -} diff --git a/docs/agent-skills.md b/docs/agent-skills.md new file mode 100644 index 0000000000..37da780155 --- /dev/null +++ b/docs/agent-skills.md @@ -0,0 +1,129 @@ +--- +title: Agent Skills (TanStack Intent) +id: agent-skills +description: "Use TanStack Intent to wire TanStack Table's bundled Agent Skills into Claude Code, Cursor, GitHub Copilot, Codex, and other AI coding assistants." +keywords: + - tanstack table + - tanstack intent + - agent skills + - claude code + - cursor + - github copilot + - codex + - ai coding agents + - SKILL.md + - AGENTS.md +--- + +You're building with TanStack Table and using an AI coding agent such as Claude Code, Cursor, GitHub Copilot, or Codex. The agent keeps suggesting v8 APIs such as `useReactTable`, configures row models without explicit features, or renders with adapter patterns that no longer match v9. By the end of this guide, your agent will load TanStack Table's bundled skills automatically whenever you work on table code, and those skills will stay in sync with whichever TanStack Table version your project installs. + +## What are Agent Skills? + +Agent Skills are markdown documents (`SKILL.md`) that ship inside npm packages and tell AI coding agents how to use a library correctly: which functions to use, which patterns to avoid, and when to reach for a particular feature. The format is an open standard supported by Claude Code, Cursor, GitHub Copilot, Codex, and others. + +TanStack Table publishes skills inside its packages so the guidance travels with `npm update` instead of being pinned in a model's training data or copied into an agent configuration file manually. + +## Skills Shipped by TanStack Table + +The skills available to your agent depend on which packages your project installs: + +| Package | Skills | What they teach | +| ---------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@tanstack/table-core` | `core`, `table-features`, and focused feature skills | Headless table architecture, explicit feature registration, TypeScript, client/server boundaries, migration, and features such as sorting, filtering, grouping, pagination, pinning, sizing, selection, and aggregation | +| `@tanstack/-table` | Framework-specific setup and state skills | Creating, rendering, and controlling a table with your framework adapter; supported adapters also include migration and TanStack Query/Virtual composition skills | +| `@tanstack/table-devtools` and framework devtools adapters | `devtools` | Registering table instances and inspecting features, state, options, rows, and columns | +| `@tanstack/match-sorter-utils` | `fuzzy-ranking` | Fuzzy filtering, ranking metadata, and rank-aware sorting | + +Each skill lives under `node_modules//skills//SKILL.md` once the package is installed. Skills can declare prerequisites, so your agent can load the core guidance before a framework or feature-specific skill. + +## Step 1: Install TanStack Table + +If you haven't already, install the adapter for your framework. See [Installation](./installation) for the full package list. + +```bash +pnpm add @tanstack/react-table +``` + +Framework adapters install `@tanstack/table-core` as a dependency, so both the framework-specific and core skills are available to the installer. + +## Step 2: Run `intent install` + +From the root of your project, run: + +```bash +npx @tanstack/intent@latest install +``` + +The CLI writes lightweight skill-loading guidance into your agent's config file. That guidance tells the agent to discover skills from the packages installed in your project and load the most relevant one before it starts a substantial task. + +By default the guidance lands in `AGENTS.md`. The CLI can also update: + +- `CLAUDE.md` for Claude Code +- `.cursorrules` for Cursor +- `.github/copilot-instructions.md` for GitHub Copilot + +## Step 3: Review the Generated Guidance + +The install command appends (or creates) an `intent-skills` block that looks like this: + +```markdown + + +## Skill Loading + +Before editing files for a substantial task: + +- Run `npx @tanstack/intent@latest list` from the workspace root to see available local skills. +- If a listed skill matches the task, run `npx @tanstack/intent@latest load #` before changing files. +- Use the loaded `SKILL.md` guidance while making the change. +- Monorepos: when working across packages, run the skill check from the workspace root and prefer the local skill for the package being changed. +- Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns. + + +``` + +Keep the block near the top of the config file so your agent sees it before task-specific instructions. + +You can inspect and load Table skills yourself with the same commands: + +```bash +npx @tanstack/intent@latest list +npx @tanstack/intent@latest load @tanstack/react-table#getting-started +npx @tanstack/intent@latest load @tanstack/table-core#sorting +``` + +If you prefer explicit task-to-skill entries, run `npx @tanstack/intent@latest install --map`. Mapping mode scans your installed intent-enabled packages and writes compact `id`, `run`, and `for` entries into the managed block. + +## Step 4: Confirm It's Wired Up + +Open a fresh session in your coding agent and ask it to build something with TanStack Table, for example: _"Build a sortable, paginated React table with TanStack Table v9."_ + +You should see: + +- The agent uses `useTable()` instead of the v8 `useReactTable()` API. +- Features and row-model slots are declared explicitly with `tableFeatures()`. +- Static data, columns, and features keep stable references. +- The adapter's current rendering APIs are used instead of copied v8 rendering patterns. +- Table owns the headless model and state while your application owns markup, styles, interactions, and accessibility. + +If the agent still falls back to v8 patterns, reopen its config file and confirm the `intent-skills` block is present. You can also run `intent list` to confirm that the installed Table packages are detected and `intent load` to inspect the matching guidance directly. + +## Keeping Skills Current + +Skills are versioned with each package. When you update your TanStack Table packages, the `SKILL.md` files under `node_modules` update with them. No CLI rerun is needed. If you use explicit mappings, rerun `npx @tanstack/intent@latest install --map` after adding another intent-enabled package, such as a Table devtools adapter, or when you want to refresh the mappings. + +## Using Skills Without the CLI + +If you'd rather wire skills in yourself, reference them directly from `node_modules` in any agent config file. The minimum your agent needs is a pointer to the relevant file: + +```markdown +When working on TanStack React Table code, read and follow: +node_modules/@tanstack/react-table/skills/getting-started/SKILL.md +``` + +The CLI is recommended because it discovers installed packages automatically and stays consistent with the Agent Skills standard, but the underlying file paths are stable. + +## Learn More + +- [TanStack Intent documentation](https://tanstack.com/intent/latest/docs/overview), the CLI's full reference, including `scaffold`, `validate`, and CI setup for library maintainers. +- [Agent Skills registry](https://tanstack.com/intent/registry), where you can browse other intent-enabled packages. diff --git a/docs/api/core/cell.md b/docs/api/core/cell.md deleted file mode 100644 index e02aa1e59c..0000000000 --- a/docs/api/core/cell.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Cell APIs ---- - -These are **core** options and API properties for all cells. More options and API properties are available for other [table features](../../../guide/features.md). - -## Cell API - -All cell objects have the following properties: - -### `id` - -```tsx -id: string -``` - -The unique ID for the cell across the entire table. - -### `getValue` - -```tsx -getValue: () => any -``` - -Returns the value for the cell, accessed via the associated column's accessor key or accessor function. - -### `renderValue` - -```tsx -renderValue: () => any -``` - -Renders the value for a cell the same as `getValue`, but will return the `renderFallbackValue` if no value is found. - -### `row` - -```tsx -row: Row -``` - -The associated Row object for the cell. - -### `column` - -```tsx -column: Column -``` - -The associated Column object for the cell. - -### `getContext` - -```tsx -getContext: () => { - table: Table - column: Column - row: Row - cell: Cell - getValue: () => TTValue - renderValue: () => TTValue | null -} -``` - -Returns the rendering context (or props) for cell-based components like cells and aggregated cells. Use these props with your framework's `flexRender` utility to render these using the template of your choice: - -```tsx -flexRender(cell.column.columnDef.cell, cell.getContext()) -``` diff --git a/docs/api/core/column-def.md b/docs/api/core/column-def.md deleted file mode 100644 index 3522e9da62..0000000000 --- a/docs/api/core/column-def.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: ColumnDef APIs ---- - -Column definitions are plain objects with the following options: - -## Options - -### `id` - -```tsx -id: string -``` - -The unique identifier for the column. - -> 🧠 A column ID is optional when: -> -> - An accessor column is created with an object key accessor -> - The column header is defined as a string - -### `accessorKey` - -```tsx -accessorKey?: string & typeof TData -``` - -The key of the row object to use when extracting the value for the column. - -### `accessorFn` - -```tsx -accessorFn?: (originalRow: TData, index: number) => any -``` - -The accessor function to use when extracting the value for the column from each row. - -### `columns` - -```tsx -columns?: ColumnDef[] -``` - -The child column defs to include in a group column. - -### `header` - -```tsx -header?: - | string - | ((props: { - table: Table - header: Header - column: Column - }) => unknown) -``` - -The header to display for the column. If a string is passed, it can be used as a default for the column ID. If a function is passed, it will be passed a props object for the header and should return the rendered header value (the exact type depends on the adapter being used). - -### `footer` - -```tsx -footer?: - | string - | ((props: { - table: Table - header: Header - column: Column - }) => unknown) -``` - -The footer to display for the column. If a function is passed, it will be passed a props object for the footer and should return the rendered footer value (the exact type depends on the adapter being used). - -### `cell` - -```tsx -cell?: - | string - | ((props: { - table: Table - row: Row - column: Column - cell: Cell - getValue: () => any - renderValue: () => any - }) => unknown) -``` - -The cell to display each row for the column. If a function is passed, it will be passed a props object for the cell and should return the rendered cell value (the exact type depends on the adapter being used). - -### `meta` - -```tsx -meta?: ColumnMeta // This interface is extensible via declaration merging. See below! -``` - -The meta data to be associated with the column. We can access it anywhere when the column is available via `column.columnDef.meta`. This type is global to all tables and can be extended like so: - -```tsx -import '@tanstack/react-table' //or vue, svelte, solid, qwik, etc. - -declare module '@tanstack/react-table' { - interface ColumnMeta { - foo: string - } -} -``` diff --git a/docs/api/core/column.md b/docs/api/core/column.md deleted file mode 100644 index 18a11ac9ba..0000000000 --- a/docs/api/core/column.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Column APIs ---- - -These are **core** options and API properties for all columns. More options and API properties are available for other [table features](../../../guide/features.md). - -## Column API - -All column objects have the following properties: - -### `id` - -```tsx -id: string -``` - -The resolved unique identifier for the column resolved in this priority: - -- A manual `id` property from the column def -- The accessor key from the column def -- The header string from the column def - -### `depth` - -```tsx -depth: number -``` - -The depth of the column (if grouped) relative to the root column def array. - -### `accessorFn` - -```tsx -accessorFn?: AccessorFn -``` - -The resolved accessor function to use when extracting the value for the column from each row. Will only be defined if the column def has a valid accessor key or function defined. - -### `columnDef` - -```tsx -columnDef: ColumnDef -``` - -The original column def used to create the column. - -### `columns` - -```tsx -type columns = ColumnDef[] -``` - -The child column (if the column is a group column). Will be an empty array if the column is not a group column. - -### `parent` - -```tsx -parent?: Column -``` - -The parent column for this column. Will be undefined if this is a root column. - -### `getFlatColumns` - -```tsx -type getFlatColumns = () => Column[] -``` - -Returns the flattened array of this column and all child/grand-child columns for this column. - -### `getLeafColumns` - -```tsx -type getLeafColumns = () => Column[] -``` - -Returns an array of all leaf-node columns for this column. If a column has no children, it is considered the only leaf-node column. diff --git a/docs/api/core/header-group.md b/docs/api/core/header-group.md deleted file mode 100644 index c4881dd27c..0000000000 --- a/docs/api/core/header-group.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: HeaderGroup APIs ---- - -These are **core** options and API properties for all header groups. More options and API properties may be available for other [table features](../../../guide/features.md). - -## Header Group API - -All header group objects have the following properties: - -### `id` - -```tsx -id: string -``` - -The unique identifier for the header group. - -### `depth` - -```tsx -depth: number -``` - -The depth of the header group, zero-indexed based. - -### `headers` - -```tsx -type headers = Header[] -``` - -An array of [Header](../header.md) objects that belong to this header group diff --git a/docs/api/core/header.md b/docs/api/core/header.md deleted file mode 100644 index cc6db9f687..0000000000 --- a/docs/api/core/header.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: Header APIs ---- - -These are **core** options and API properties for all headers. More options and API properties may be available for other [table features](../../../guide/features.md). - -## Header API - -All header objects have the following properties: - -### `id` - -```tsx -id: string -``` - -The unique identifier for the header. - -### `index` - -```tsx -index: number -``` - -The index for the header within the header group. - -### `depth` - -```tsx -depth: number -``` - -The depth of the header, zero-indexed based. - -### `column` - -```tsx -column: Column -``` - -The header's associated [Column](../column.md) object - -### `headerGroup` - -```tsx -headerGroup: HeaderGroup -``` - -The header's associated [HeaderGroup](../header-group.md) object - -### `subHeaders` - -```tsx -type subHeaders = Header[] -``` - -The header's hierarchical sub/child headers. Will be empty if the header's associated column is a leaf-column. - -### `colSpan` - -```tsx -colSpan: number -``` - -The col-span for the header. - -### `rowSpan` - -```tsx -rowSpan: number -``` - -The row-span for the header. - -### `getLeafHeaders` - -```tsx -type getLeafHeaders = () => Header[] -``` - -Returns the leaf headers hierarchically nested under this header. - -### `isPlaceholder` - -```tsx -isPlaceholder: boolean -``` - -A boolean denoting if the header is a placeholder header - -### `placeholderId` - -```tsx -placeholderId?: string -``` - -If the header is a placeholder header, this will be a unique header ID that does not conflict with any other headers across the table - -### `getContext` - -```tsx -getContext: () => { - table: Table - header: Header - column: Column -} -``` - -Returns the rendering context (or props) for column-based components like headers, footers and filters. Use these props with your framework's `flexRender` utility to render these using the template of your choice: - -```tsx -flexRender(header.column.columnDef.header, header.getContext()) -``` - -## Table API - -### `getHeaderGroups` - -```tsx -type getHeaderGroups = () => HeaderGroup[] -``` - -Returns all header groups for the table. - -### `getLeftHeaderGroups` - -```tsx -type getLeftHeaderGroups = () => HeaderGroup[] -``` - -If pinning, returns the header groups for the left pinned columns. - -### `getCenterHeaderGroups` - -```tsx -type getCenterHeaderGroups = () => HeaderGroup[] -``` - -If pinning, returns the header groups for columns that are not pinned. - -### `getRightHeaderGroups` - -```tsx -type getRightHeaderGroups = () => HeaderGroup[] -``` - -If pinning, returns the header groups for the right pinned columns. - -### `getFooterGroups` - -```tsx -type getFooterGroups = () => HeaderGroup[] -``` - -Returns all footer groups for the table. - -### `getLeftFooterGroups` - -```tsx -type getLeftFooterGroups = () => HeaderGroup[] -``` - -If pinning, returns the footer groups for the left pinned columns. - -### `getCenterFooterGroups` - -```tsx -type getCenterFooterGroups = () => HeaderGroup[] -``` - -If pinning, returns the footer groups for columns that are not pinned. - -### `getRightFooterGroups` - -```tsx -type getRightFooterGroups = () => HeaderGroup[] -``` - -If pinning, returns the footer groups for the right pinned columns. - -### `getFlatHeaders` - -```tsx -type getFlatHeaders = () => Header[] -``` - -Returns headers for all columns in the table, including parent headers. - -### `getLeftFlatHeaders` - -```tsx -type getLeftFlatHeaders = () => Header[] -``` - -If pinning, returns headers for all left pinned columns in the table, including parent headers. - -### `getCenterFlatHeaders` - -```tsx -type getCenterFlatHeaders = () => Header[] -``` - -If pinning, returns headers for all columns that are not pinned, including parent headers. - -### `getRightFlatHeaders` - -```tsx -type getRightFlatHeaders = () => Header[] -``` - -If pinning, returns headers for all right pinned columns in the table, including parent headers. - -### `getLeafHeaders` - -```tsx -type getLeafHeaders = () => Header[] -``` - -Returns headers for all leaf columns in the table, (not including parent headers). - -### `getLeftLeafHeaders` - -```tsx -type getLeftLeafHeaders = () => Header[] -``` - -If pinning, returns headers for all left pinned leaf columns in the table, (not including parent headers). - -### `getCenterLeafHeaders` - -```tsx -type getCenterLeafHeaders = () => Header[] -``` - -If pinning, returns headers for all columns that are not pinned, (not including parent headers). - -### `getRightLeafHeaders` - -```tsx -type getRightLeafHeaders = () => Header[] -``` - -If pinning, returns headers for all right pinned leaf columns in the table, (not including parent headers). diff --git a/docs/api/core/row.md b/docs/api/core/row.md deleted file mode 100644 index c478062f56..0000000000 --- a/docs/api/core/row.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: Row APIs ---- - -These are **core** options and API properties for all rows. More options and API properties are available for other [table features](../../../guide/features.md). - -## Row API - -All row objects have the following properties: - -### `id` - -```tsx -id: string -``` - -The resolved unique identifier for the row resolved via the `options.getRowId` option. Defaults to the row's index (or relative index if it is a subRow) - -### `depth` - -```tsx -depth: number -``` - -The depth of the row (if nested or grouped) relative to the root row array. - -### `index` - -```tsx -index: number -``` - -The index of the row within its parent array (or the root data array) - -### `original` - -```tsx -original: TData -``` - -The original row object provided to the table. - -> 🧠 If the row is a grouped row, the original row object will be the first original in the group. - -### `parentId` - -```tsx -parentId?: string -``` - -If nested, this row's parent row id. - -### `getValue` - -```tsx -getValue: (columnId: string) => TValue -``` - -Returns the value from the row for a given columnId - -### `renderValue` - -```tsx -renderValue: (columnId: string) => TValue -``` - -Renders the value from the row for a given columnId, but will return the `renderFallbackValue` if no value is found. - -### `getUniqueValues` - -```tsx -getUniqueValues: (columnId: string) => TValue[] -``` - -Returns a unique array of values from the row for a given columnId. - -### `subRows` - -```tsx -type subRows = Row[] -``` - -An array of subRows for the row as returned and created by the `options.getSubRows` option. - -### `getParentRow` - -```tsx -type getParentRow = () => Row | undefined -``` - -Returns the parent row for the row, if it exists. - -### `getParentRows` - -```tsx -type getParentRows = () => Row[] -``` - -Returns the parent rows for the row, all the way up to a root row. - -### `getLeafRows` - -```tsx -type getLeafRows = () => Row[] -``` - -Returns the leaf rows for the row, not including any parent rows. - -### `originalSubRows` - -```tsx -originalSubRows?: TData[] -``` - -An array of the original subRows as returned by the `options.getSubRows` option. - -### `getAllCells` - -```tsx -type getAllCells = () => Cell[] -``` - -Returns all of the [Cells](../cell.md) for the row. diff --git a/docs/api/core/table.md b/docs/api/core/table.md deleted file mode 100644 index fbd1e06cf8..0000000000 --- a/docs/api/core/table.md +++ /dev/null @@ -1,385 +0,0 @@ ---- -title: Table APIs ---- - -## `useReactTable` / `createSolidTable` / `useQwikTable` / `useVueTable` / `createSvelteTable` - -```tsx -type useReactTable = ( - options: TableOptions -) => Table -``` - -These functions are used to create a table. Which one you use depends on which framework adapter you are using. - -## Options - -These are **core** options and API properties for the table. More options and API properties are available for other [table features](../../../guide/features.md). - -### `data` - -```tsx -data: TData[] -``` - -The data for the table to display. This array should match the type you provided to `table.setRowType<...>`, but in theory could be an array of anything. It's common for each item in the array to be an object of key/values but this is not required. Columns can access this data via string/index or a functional accessor to return anything they want. - -When the `data` option changes reference (compared via `Object.is`), the table will reprocess the data. Any other data processing that relies on the core data model (such as grouping, sorting, filtering, etc) will also be reprocessed. - -> 🧠 Make sure your `data` option is only changing when you want the table to reprocess. Providing an inline `[]` or constructing the data array as a new object every time you want to render the table will result in a _lot_ of unnecessary re-processing. This can easily go unnoticed in smaller tables, but you will likely notice it in larger tables. - -### `columns` - -```tsx -type columns = ColumnDef[] -``` - -The array of column defs to use for the table. See the [Column Def Guide](../../../guide/column-defs.md) for more information on creating column definitions. - -### `defaultColumn` - -```tsx -defaultColumn?: Partial> -``` - -Default column options to use for all column defs supplied to the table. This is useful for providing default cell/header/footer renderers, sorting/filtering/grouping options, etc. All column definitions passed to `options.columns` are merged with this default column definition to produce the final column definitions. - -### `initialState` - -```tsx -initialState?: Partial< - VisibilityTableState & - ColumnOrderTableState & - ColumnPinningTableState & - FiltersTableState & - SortingTableState & - ExpandedTableState & - GroupingTableState & - ColumnSizingTableState & - PaginationTableState & - RowSelectionTableState -> -``` - -Use this option to optionally pass initial state to the table. This state will be used when resetting various table states either automatically by the table (eg. `options.autoResetPageIndex`) or via functions like `table.resetRowSelection()`. Most reset function allow you optionally pass a flag to reset to a blank/default state instead of the initial state. - -> 🧠 Table state will not be reset when this object changes, which also means that the initial state object does not need to be stable. - -### `autoResetAll` - -```tsx -autoResetAll?: boolean -``` - -Set this option to override any of the `autoReset...` feature options. - -### `meta` - -```tsx -meta?: TableMeta // This interface is extensible via declaration merging. See below! -``` - -You can pass any object to `options.meta` and access it anywhere the `table` is available via `table.options.meta` This type is global to all tables and can be extended like so: - -```tsx -declare module '@tanstack/table-core' { - interface TableMeta { - foo: string - } -} -``` - -> 🧠 Think of this option as an arbitrary "context" for your table. This is a great way to pass arbitrary data or functions to your table without having to pass it to every thing the table touches. A good example is passing a locale object to your table to use for formatting dates, numbers, etc or even a function that can be used to update editable data like in the [editable-data](https://github.com/TanStack/table/tree/main/examples/react/editable-data) example. - -### `state` - -```tsx -state?: Partial< - VisibilityTableState & - ColumnOrderTableState & - ColumnPinningTableState & - FiltersTableState & - SortingTableState & - ExpandedTableState & - GroupingTableState & - ColumnSizingTableState & - PaginationTableState & - RowSelectionTableState -> -``` - -The `state` option can be used to optionally _control_ part or all of the table state. The state you pass here will merge with and overwrite the internal automatically-managed state to produce the final state for the table. You can also listen to state changes via the `onStateChange` option. - -### `onStateChange` - -```tsx -onStateChange: (updater: Updater) => void -``` - -The `onStateChange` option can be used to optionally listen to state changes within the table. If you provide this options, you will be responsible for controlling and updating the table state yourself. You can provide the state back to the table with the `state` option. - -### `debugAll` - -> ⚠️ Debugging is only available in development mode. - -```tsx -debugAll?: boolean -``` - -Set this option to true to output all debugging information to the console. - -### `debugTable` - -> ⚠️ Debugging is only available in development mode. - -```tsx -debugTable?: boolean -``` - -Set this option to true to output table debugging information to the console. - -### `debugHeaders` - -> ⚠️ Debugging is only available in development mode. - -```tsx -debugHeaders?: boolean -``` - -Set this option to true to output header debugging information to the console. - -### `debugColumns` - -> ⚠️ Debugging is only available in development mode. - -```tsx -debugColumns?: boolean -``` - -Set this option to true to output column debugging information to the console. - -### `debugRows` - -> ⚠️ Debugging is only available in development mode. - -```tsx -debugRows?: boolean -``` - -Set this option to true to output row debugging information to the console. - -### `_features` - -```tsx -_features?: TableFeature[] -``` - -An array of extra features that you can add to the table instance. - -### `render` - -> ⚠️ This option is only necessary if you are implementing a table adapter. - -```tsx -type render = (template: Renderable, props: TProps) => any -``` - -The `render` option provides a renderer implementation for the table. This implementation is used to turn a table's various column header and cell templates into a result that is supported by the user's framework. - -### `mergeOptions` - -> ⚠️ This option is only necessary if you are implementing a table adapter. - -```tsx -type mergeOptions = (defaultOptions: T, options: Partial) => T -``` - -This option is used to optionally implement the merging of table options. Some framework like solid-js use proxies to track reactivity and usage, so merging reactive objects needs to be handled carefully. This option inverts control of this process to the adapter. - -### `getCoreRowModel` - -```tsx -getCoreRowModel: (table: Table) => () => RowModel -``` - -This required option is a factory for a function that computes and returns the core row model for the table. It is called **once** per table and should return a **new function** which will calculate and return the row model for the table. - -A default implementation is provided via any table adapter's `{ getCoreRowModel }` export. - -### `getSubRows` - -```tsx -getSubRows?: ( - originalRow: TData, - index: number -) => undefined | TData[] -``` - -This optional function is used to access the sub rows for any given row. If you are using nested rows, you will need to use this function to return the sub rows object (or undefined) from the row. - -### `getRowId` - -```tsx -getRowId?: ( - originalRow: TData, - index: number, - parent?: Row -) => string -``` - -This optional function is used to derive a unique ID for any given row. If not provided the rows index is used (nested rows join together with `.` using their grandparents' index eg. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity eg. a userId, taskId, database ID field, etc. - -## Table API - -These properties and methods are available on the table object: - -### `initialState` - -```tsx -initialState: VisibilityTableState & - ColumnOrderTableState & - ColumnPinningTableState & - FiltersTableState & - SortingTableState & - ExpandedTableState & - GroupingTableState & - ColumnSizingTableState & - PaginationTableState & - RowSelectionTableState -``` - -This is the resolved initial state of the table. - -### `reset` - -```tsx -reset: () => void -``` - -Call this function to reset the table state to the initial state. - -### `getState` - -```tsx -getState: () => TableState -``` - -Call this function to get the table's current state. It's recommended to use this function and its state, especially when managing the table state manually. It is the exact same state used internally by the table for every feature and function it provides. - -> 🧠 The state returned by this function is the shallow-merged result of the automatically-managed internal table-state and any manually-managed state passed via `options.state`. - -### `setState` - -```tsx -setState: (updater: Updater) => void -``` - -Call this function to update the table state. It's recommended you pass an updater function in the form of `(prevState) => newState` to update the state, but a direct object can also be passed. - -> 🧠 If `options.onStateChange` is provided, it will be triggered by this function with the new state. - -### `options` - -```tsx -options: TableOptions -``` - -A read-only reference to the table's current options. - -> ⚠️ This property is generally used internally or by adapters. It can be updated by passing new options to your table. This is different per adapter. For adapters themselves, table options must be updated via the `setOptions` function. - -### `setOptions` - -```tsx -setOptions: (newOptions: Updater>) => void -``` - -> ⚠️ This function is generally used by adapters to update the table options. It can be used to update the table options directly, but it is generally not recommended to bypass your adapters strategy for updating table options. - -### `getCoreRowModel` - -```tsx -getCoreRowModel: () => { - rows: Row[], - flatRows: Row[], - rowsById: Record>, -} -``` - -Returns the core row model before any processing has been applied. - -### `getRowModel` - -```tsx -getRowModel: () => { - rows: Row[], - flatRows: Row[], - rowsById: Record>, -} -``` - -Returns the final model after all processing from other used features has been applied. - -### `getAllColumns` - -```tsx -type getAllColumns = () => Column[] -``` - -Returns all columns in the table in their normalized and nested hierarchy, mirrored from the column defs passed to the table. - -### `getAllFlatColumns` - -```tsx -type getAllFlatColumns = () => Column[] -``` - -Returns all columns in the table flattened to a single level. This includes parent column objects throughout the hierarchy. - -### `getAllLeafColumns` - -```tsx -type getAllLeafColumns = () => Column[] -``` - -Returns all leaf-node columns in the table flattened to a single level. This does not include parent columns. - -### `getColumn` - -```tsx -type getColumn = (id: string) => Column | undefined -``` - -Returns a single column by its ID. - -### `getHeaderGroups` - -```tsx -type getHeaderGroups = () => HeaderGroup[] -``` - -Returns the header groups for the table. - -### `getFooterGroups` - -```tsx -type getFooterGroups = () => HeaderGroup[] -``` - -Returns the footer groups for the table. - -### `getFlatHeaders` - -```tsx -type getFlatHeaders = () => Header[] -``` - -Returns a flattened array of Header objects for the table, including parent headers. - -### `getLeafHeaders` - -```tsx -type getLeafHeaders = () => Header[] -``` - -Returns a flattened array of leaf-node Header objects for the table. diff --git a/docs/api/features/column-faceting.md b/docs/api/features/column-faceting.md deleted file mode 100644 index 2a951da447..0000000000 --- a/docs/api/features/column-faceting.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Column Faceting APIs -id: column-faceting ---- - -## Column API - -### `getFacetedRowModel` - -```tsx -type getFacetedRowModel = () => RowModel -``` - -> ⚠️ Requires that you pass a valid `getFacetedRowModel` function to `options.facetedRowModel`. A default implementation is provided via the exported `getFacetedRowModel` function. - -Returns the row model with all other column filters applied, excluding its own filter. Useful for displaying faceted result counts. - -### `getFacetedUniqueValues` - -```tsx -getFacetedUniqueValues: () => Map -``` - -> ⚠️ Requires that you pass a valid `getFacetedUniqueValues` function to `options.getFacetedUniqueValues`. A default implementation is provided via the exported `getFacetedUniqueValues` function. - -A function that **computes and returns** a `Map` of unique values and their occurrences derived from `column.getFacetedRowModel`. Useful for displaying faceted result values. - -### `getFacetedMinMaxValues` - -```tsx -getFacetedMinMaxValues: () => Map -``` - -> ⚠️ Requires that you pass a valid `getFacetedMinMaxValues` function to `options.getFacetedMinMaxValues`. A default implementation is provided via the exported `getFacetedMinMaxValues` function. - -A function that **computes and returns** a min/max tuple derived from `column.getFacetedRowModel`. Useful for displaying faceted result values. - -## Table Options - -### `getColumnFacetedRowModel` - -```tsx -getColumnFacetedRowModel: (columnId: string) => RowModel -``` - -Returns the faceted row model for a given columnId. diff --git a/docs/api/features/column-filtering.md b/docs/api/features/column-filtering.md deleted file mode 100644 index b32d4f314c..0000000000 --- a/docs/api/features/column-filtering.md +++ /dev/null @@ -1,396 +0,0 @@ ---- -title: Column Filtering APIs -id: column-filtering ---- - -## Can-Filter - -The ability for a column to be **column** filtered is determined by the following: - -- The column was defined with a valid `accessorKey`/`accessorFn`. -- `column.enableColumnFilter` is not set to `false` -- `options.enableColumnFilters` is not set to `false` -- `options.enableFilters` is not set to `false` - -## State - -Filter state is stored on the table using the following shape: - -```tsx -export interface ColumnFiltersTableState { - columnFilters: ColumnFiltersState -} - -export type ColumnFiltersState = ColumnFilter[] - -export interface ColumnFilter { - id: string - value: unknown -} -``` - -## Filter Functions - -The following filter functions are built-in to the table core: - -- `includesString` - - Case-insensitive string inclusion -- `includesStringSensitive` - - Case-sensitive string inclusion -- `equalsString` - - Case-insensitive string equality -- `equalsStringSensitive` - - Case-sensitive string equality -- `arrIncludes` - - Item inclusion within an array -- `arrIncludesAll` - - All items included in an array -- `arrIncludesSome` - - Some items included in an array -- `equals` - - Object/referential equality `Object.is`/`===` -- `weakEquals` - - Weak object/referential equality `==` -- `inNumberRange` - - Number range inclusion - -Every filter function receives: - -- The row to filter -- The columnId to use to retrieve the row's value -- The filter value - -and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. - -This is the type signature for every filter function: - -```tsx -export type FilterFn = { - ( - row: Row, - columnId: string, - filterValue: any, - addMeta: (meta: any) => void - ): boolean - resolveFilterValue?: TransformFilterValueFn - autoRemove?: ColumnFilterAutoRemoveTestFn - addMeta?: (meta?: any) => void -} - -export type TransformFilterValueFn = ( - value: any, - column?: Column -) => unknown - -export type ColumnFilterAutoRemoveTestFn = ( - value: any, - column?: Column -) => boolean - -export type CustomFilterFns = Record< - string, - FilterFn -> -``` - -### `filterFn.resolveFilterValue` - -This optional "hanging" method on any given `filterFn` allows the filter function to transform/sanitize/format the filter value before it is passed to the filter function. - -### `filterFn.autoRemove` - -This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. - -#### Using Filter Functions - -Filter functions can be used/referenced/defined by passing the following to `columnDefinition.filterFn`: - -- A `string` that references a built-in filter function -- A function directly provided to the `columnDefinition.filterFn` option - -The final list of filter functions available for the `columnDef.filterFn` option use the following type: - -```tsx -export type FilterFnOption = - | 'auto' - | BuiltInFilterFn - | FilterFn -``` - -#### Filter Meta - -Filtering data can often expose additional information about the data that can be used to aid other future operations on the same data. A good example of this concept is a ranking-system like that of [`match-sorter`](https://github.com/kentcdodds/match-sorter) that simultaneously ranks, filters and sorts data. While utilities like `match-sorter` make a lot of sense for single-dimensional filter+sort tasks, the decoupled filtering/sorting architecture of building a table makes them very difficult and slow to use. - -To make a ranking/filtering/sorting system work with tables, `filterFn`s can optionally mark results with a **filter meta** value that can be used later to sort/group/etc the data to your liking. This is done by calling the `addMeta` function supplied to your custom `filterFn`. - -Below is an example using our own `match-sorter-utils` package (a utility fork of `match-sorter`) to rank, filter, and sort the data - -```tsx -import { sortingFns } from '@tanstack/react-table' - -import { rankItem, compareItems } from '@tanstack/match-sorter-utils' - -const fuzzyFilter = (row, columnId, value, addMeta) => { - // Rank the item - const itemRank = rankItem(row.getValue(columnId), value) - - // Store the ranking info - addMeta(itemRank) - - // Return if the item should be filtered in/out - return itemRank.passed -} - -const fuzzySort = (rowA, rowB, columnId) => { - let dir = 0 - - // Only sort by rank if the column has ranking information - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId]!, - rowB.columnFiltersMeta[columnId]! - ) - } - - // Provide an alphanumeric fallback for when the item ranks are equal - return dir === 0 ? sortingFns.alphanumeric(rowA, rowB, columnId) : dir -} -``` - -## Column Def Options - -### `filterFn` - -```tsx -filterFn?: FilterFn | keyof FilterFns | keyof BuiltInFilterFns -``` - -The filter function to use with this column. - -Options: - -- A `string` referencing a [built-in filter function](#filter-functions)) -- A [custom filter function](#filter-functions) - -### `enableColumnFilter` - -```tsx -enableColumnFilter?: boolean -``` - -Enables/disables the **column** filter for this column. - -## Column API - -### `getCanFilter` - -```tsx -getCanFilter: () => boolean -``` - -Returns whether or not the column can be **column** filtered. - -### `getFilterIndex` - -```tsx -getFilterIndex: () => number -``` - -Returns the index (including `-1`) of the column filter in the table's `state.columnFilters` array. - -### `getIsFiltered` - -```tsx -getIsFiltered: () => boolean -``` - -Returns whether or not the column is currently filtered. - -### `getFilterValue` - -```tsx -getFilterValue: () => unknown -``` - -Returns the current filter value of the column. - -### `setFilterValue` - -```tsx -setFilterValue: (updater: Updater) => void -``` - -A function that sets the current filter value for the column. You can pass it a value or an updater function for immutability-safe operations on existing values. - -### `getAutoFilterFn` - -```tsx -getAutoFilterFn: (columnId: string) => FilterFn | undefined -``` - -Returns an automatically calculated filter function for the column based off of the columns first known value. - -### `getFilterFn` - -```tsx -getFilterFn: (columnId: string) => FilterFn | undefined -``` - -Returns the filter function (either user-defined or automatic, depending on configuration) for the columnId specified. - -## Row API - -### `columnFilters` - -```tsx -columnFilters: Record -``` - -The column filters map for the row. This object tracks whether a row is passing/failing specific filters by their column ID. - -### `columnFiltersMeta` - -```tsx -columnFiltersMeta: Record -``` - -The column filters meta map for the row. This object tracks any filter meta for a row as optionally provided during the filtering process. - -## Table Options - -### `filterFns` - -```tsx -filterFns?: Record -``` - -This option allows you to define custom filter functions that can be referenced in a column's `filterFn` option by their key. -Example: - -```tsx -declare module '@tanstack/[adapter]-table' { - interface FilterFns { - myCustomFilter: FilterFn - } -} - -const column = columnHelper.data('key', { - filterFn: 'myCustomFilter', -}) - -const table = useReactTable({ - columns: [column], - filterFns: { - myCustomFilter: (rows, columnIds, filterValue) => { - // return the filtered rows - }, - }, -}) -``` - -### `filterFromLeafRows` - -```tsx -filterFromLeafRows?: boolean -``` - -By default, filtering is done from parent rows down (so if a parent row is filtered out, all of its children will be filtered out as well). Setting this option to `true` will cause filtering to be done from leaf rows up (which means parent rows will be included so long as one of their child or grand-child rows is also included). - -### `maxLeafRowFilterDepth` - -```tsx -maxLeafRowFilterDepth?: number -``` - -By default, filtering is done for all rows (max depth of 100), no matter if they are root level parent rows or the child leaf rows of a parent row. Setting this option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. - -This is useful for situations where you want a row's entire child hierarchy to be visible regardless of the applied filter. - -### `enableFilters` - -```tsx -enableFilters?: boolean -``` - -Enables/disables all filters for the table. - -### `manualFiltering` - -```tsx -manualFiltering?: boolean -``` - -Disables the `getFilteredRowModel` from being used to filter data. This may be useful if your table needs to dynamically support both client-side and server-side filtering. - -### `onColumnFiltersChange` - -```tsx -onColumnFiltersChange?: OnChangeFn -``` - -If provided, this function will be called with an `updaterFn` when `state.columnFilters` changes. This overrides the default internal state management, so you will need to persist the state change either fully or partially outside of the table. - -### `enableColumnFilters` - -```tsx -enableColumnFilters?: boolean -``` - -Enables/disables **all** column filters for the table. - -### `getFilteredRowModel` - -```tsx -getFilteredRowModel?: ( - table: Table -) => () => RowModel -``` - -If provided, this function is called **once** per table and should return a **new function** which will calculate and return the row model for the table when it's filtered. - -- For server-side filtering, this function is unnecessary and can be ignored since the server should already return the filtered row model. -- For client-side filtering, this function is required. A default implementation is provided via any table adapter's `{ getFilteredRowModel }` export. - -Example: - -```tsx -import { getFilteredRowModel } from '@tanstack/[adapter]-table' - - - getFilteredRowModel: getFilteredRowModel(), -}) -``` - -## Table API - -### `setColumnFilters` - -```tsx -setColumnFilters: (updater: Updater) => void -``` - -Sets or updates the `state.columnFilters` state. - -### `resetColumnFilters` - -```tsx -resetColumnFilters: (defaultState?: boolean) => void -``` - -Resets the **columnFilters** state to `initialState.columnFilters`, or `true` can be passed to force a default blank state reset to `[]`. - -### `getPreFilteredRowModel` - -```tsx -getPreFilteredRowModel: () => RowModel -``` - -Returns the row model for the table before any **column** filtering has been applied. - -### `getFilteredRowModel` - -```tsx -getFilteredRowModel: () => RowModel -``` - -Returns the row model for the table after **column** filtering has been applied. diff --git a/docs/api/features/column-ordering.md b/docs/api/features/column-ordering.md deleted file mode 100644 index 37bfb95337..0000000000 --- a/docs/api/features/column-ordering.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Column Ordering APIs -id: column-ordering ---- - -## State - -Column ordering state is stored on the table using the following shape: - -```tsx -export type ColumnOrderTableState = { - columnOrder: ColumnOrderState -} - -export type ColumnOrderState = string[] -``` - -## Table Options - -### `onColumnOrderChange` - -```tsx -onColumnOrderChange?: OnChangeFn -``` - -If provided, this function will be called with an `updaterFn` when `state.columnOrder` changes. This overrides the default internal state management, so you will need to persist the state change either fully or partially outside of the table. - -## Table API - -### `setColumnOrder` - -```tsx -setColumnOrder: (updater: Updater) => void -``` - -Sets or updates the `state.columnOrder` state. - -### `resetColumnOrder` - -```tsx -resetColumnOrder: (defaultState?: boolean) => void -``` - -Resets the **columnOrder** state to `initialState.columnOrder`, or `true` can be passed to force a default blank state reset to `[]`. - -## Column API - -### `getIndex` - -```tsx -getIndex: (position?: ColumnPinningPosition) => number -``` - -Returns the index of the column in the order of the visible columns. Optionally pass a `position` parameter to get the index of the column in a sub-section of the table. - -### `getIsFirstColumn` - -```tsx -getIsFirstColumn: (position?: ColumnPinningPosition) => boolean -``` - -Returns `true` if the column is the first column in the order of the visible columns. Optionally pass a `position` parameter to check if the column is the first in a sub-section of the table. - -### `getIsLastColumn` - -```tsx -getIsLastColumn: (position?: ColumnPinningPosition) => boolean -``` - -Returns `true` if the column is the last column in the order of the visible columns. Optionally pass a `position` parameter to check if the column is the last in a sub-section of the table. \ No newline at end of file diff --git a/docs/api/features/column-pinning.md b/docs/api/features/column-pinning.md deleted file mode 100644 index a312b33823..0000000000 --- a/docs/api/features/column-pinning.md +++ /dev/null @@ -1,266 +0,0 @@ ---- -title: Column Pinning APIs -id: column-pinning ---- - -## Can-Pin - -The ability for a column to be **pinned** is determined by the following: - -- `options.enablePinning` is not set to `false` -- `options.enableColumnPinning` is not set to `false` -- `columnDefinition.enablePinning` is not set to `false` - -## State - -Pinning state is stored on the table using the following shape: - -```tsx -export type ColumnPinningPosition = false | 'left' | 'right' - -export type ColumnPinningState = { - left?: string[] - right?: string[] -} - - -export type ColumnPinningTableState = { - columnPinning: ColumnPinningState -} -``` - -## Table Options - -### `enableColumnPinning` - -```tsx -enableColumnPinning?: boolean -``` - -Enables/disables column pinning for all columns in the table. - -### `onColumnPinningChange` - -```tsx -onColumnPinningChange?: OnChangeFn -``` - -If provided, this function will be called with an `updaterFn` when `state.columnPinning` changes. This overrides the default internal state management, so you will also need to supply `state.columnPinning` from your own managed state. - -## Column Def Options - -### `enablePinning` - -```tsx -enablePinning?: boolean -``` - -Enables/disables pinning for the column. - -## Table API - -### `setColumnPinning` - -```tsx -setColumnPinning: (updater: Updater) => void -``` - -Sets or updates the `state.columnPinning` state. - -### `resetColumnPinning` - -```tsx -resetColumnPinning: (defaultState?: boolean) => void -``` - -Resets the **columnPinning** state to `initialState.columnPinning`, or `true` can be passed to force a default blank state reset to `{ left: [], right: [], }`. - -### `getIsSomeColumnsPinned` - -```tsx -getIsSomeColumnsPinned: (position?: ColumnPinningPosition) => boolean -``` - -Returns whether or not any columns are pinned. Optionally specify to only check for pinned columns in either the `left` or `right` position. - -_Note: Does not account for column visibility_ - -### `getLeftHeaderGroups` - -```tsx -getLeftHeaderGroups: () => HeaderGroup[] -``` - -Returns the left pinned header groups for the table. - -### `getCenterHeaderGroups` - -```tsx -getCenterHeaderGroups: () => HeaderGroup[] -``` - -Returns the unpinned/center header groups for the table. - -### `getRightHeaderGroups` - -```tsx -getRightHeaderGroups: () => HeaderGroup[] -``` - -Returns the right pinned header groups for the table. - -### `getLeftFooterGroups` - -```tsx -getLeftFooterGroups: () => HeaderGroup[] -``` - -Returns the left pinned footer groups for the table. - -### `getCenterFooterGroups` - -```tsx -getCenterFooterGroups: () => HeaderGroup[] -``` - -Returns the unpinned/center footer groups for the table. - -### `getRightFooterGroups` - -```tsx -getRightFooterGroups: () => HeaderGroup[] -``` - -Returns the right pinned footer groups for the table. - -### `getLeftFlatHeaders` - -```tsx -getLeftFlatHeaders: () => Header[] -``` - -Returns a flat array of left pinned headers for the table, including parent headers. - -### `getCenterFlatHeaders` - -```tsx -getCenterFlatHeaders: () => Header[] -``` - -Returns a flat array of unpinned/center headers for the table, including parent headers. - -### `getRightFlatHeaders` - -```tsx -getRightFlatHeaders: () => Header[] -``` - -Returns a flat array of right pinned headers for the table, including parent headers. - -### `getLeftLeafHeaders` - -```tsx -getLeftLeafHeaders: () => Header[] -``` - -Returns a flat array of leaf-node left pinned headers for the table. - -### `getCenterLeafHeaders` - -```tsx -getCenterLeafHeaders: () => Header[] -``` - -Returns a flat array of leaf-node unpinned/center headers for the table. - -### `getRightLeafHeaders` - -```tsx -getRightLeafHeaders: () => Header[] -``` - -Returns a flat array of leaf-node right pinned headers for the table. - -### `getLeftLeafColumns` - -```tsx -getLeftLeafColumns: () => Column[] -``` - -Returns all left pinned leaf columns. - -### `getRightLeafColumns` - -```tsx -getRightLeafColumns: () => Column[] -``` - -Returns all right pinned leaf columns. - -### `getCenterLeafColumns` - -```tsx -getCenterLeafColumns: () => Column[] -``` - -Returns all center pinned (unpinned) leaf columns. - -## Column API - -### `getCanPin` - -```tsx -getCanPin: () => boolean -``` - -Returns whether or not the column can be pinned. - -### `getPinnedIndex` - -```tsx -getPinnedIndex: () => number -``` - -Returns the numeric pinned index of the column within a pinned column group. - -### `getIsPinned` - -```tsx -getIsPinned: () => ColumnPinningPosition -``` - -Returns the pinned position of the column. (`'left'`, `'right'` or `false`) - -### `pin` - -```tsx -pin: (position: ColumnPinningPosition) => void -``` - -Pins a column to the `'left'` or `'right'`, or unpins the column to the center if `false` is passed. - -## Row API - -### `getLeftVisibleCells` - -```tsx -getLeftVisibleCells: () => Cell[] -``` - -Returns all left pinned leaf cells in the row. - -### `getRightVisibleCells` - -```tsx -getRightVisibleCells: () => Cell[] -``` - -Returns all right pinned leaf cells in the row. - -### `getCenterVisibleCells` - -```tsx -getCenterVisibleCells: () => Cell[] -``` - -Returns all center pinned (unpinned) leaf cells in the row. diff --git a/docs/api/features/column-sizing.md b/docs/api/features/column-sizing.md deleted file mode 100644 index 0bf7631be8..0000000000 --- a/docs/api/features/column-sizing.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -title: Column Sizing APIs -id: column-sizing ---- - -## State - -Column sizing state is stored on the table using the following shape: - -```tsx -export type ColumnSizingTableState = { - columnSizing: ColumnSizing - columnSizingInfo: ColumnSizingInfoState -} - -export type ColumnSizing = Record - -export type ColumnSizingInfoState = { - startOffset: null | number - startSize: null | number - deltaOffset: null | number - deltaPercentage: null | number - isResizingColumn: false | string - columnSizingStart: [string, number][] -} -``` - -## Column Def Options - -### `enableResizing` - -```tsx -enableResizing?: boolean -``` - -Enables or disables column resizing for the column. - -### `size` - -```tsx -size?: number -``` - -The desired size for the column - -### `minSize` - -```tsx -minSize?: number -``` - -The minimum allowed size for the column - -### `maxSize` - -```tsx -maxSize?: number -``` - -The maximum allowed size for the column - -## Column API - -### `getSize` - -```tsx -getSize: () => number -``` - -Returns the current size of the column - -### `getStart` - -```tsx -getStart: (position?: ColumnPinningPosition) => number -``` - -Returns the offset measurement along the row-axis (usually the x-axis for standard tables) for the column, measuring the size of all preceding columns. - -Useful for sticky or absolute positioning of columns. (e.g. `left` or `transform`) - -### `getAfter` - -```tsx -getAfter: (position?: ColumnPinningPosition) => number -``` - -Returns the offset measurement along the row-axis (usually the x-axis for standard tables) for the column, measuring the size of all succeeding columns. - -Useful for sticky or absolute positioning of columns. (e.g. `right` or `transform`) - -### `getCanResize` - -```tsx -getCanResize: () => boolean -``` - -Returns `true` if the column can be resized. - -### `getIsResizing` - -```tsx -getIsResizing: () => boolean -``` - -Returns `true` if the column is currently being resized. - -### `resetSize` - -```tsx -resetSize: () => void -``` - -Resets the column size to its initial size. - -## Header API - -### `getSize` - -```tsx -getSize: () => number -``` - -Returns the size for the header, calculated by summing the size of all leaf-columns that belong to it. - -### `getStart` - -```tsx -getStart: (position?: ColumnPinningPosition) => number -``` - -Returns the offset measurement along the row-axis (usually the x-axis for standard tables) for the header. This is effectively a sum of the offset measurements of all preceding headers. - -### `getResizeHandler` - -```tsx -getResizeHandler: () => (event: unknown) => void -``` - -Returns an event handler function that can be used to resize the header. It can be used as an: - -- `onMouseDown` handler -- `onTouchStart` handler - -The dragging and release events are automatically handled for you. - -## Table Options - -### `enableColumnResizing` - -```tsx -enableColumnResizing?: boolean -``` - -Enables/disables column resizing for \*all columns\*\*. - -### `columnResizeMode` - -```tsx -columnResizeMode?: 'onChange' | 'onEnd' -``` - -Determines when the columnSizing state is updated. `onChange` updates the state when the user is dragging the resize handle. `onEnd` updates the state when the user releases the resize handle. - -### `columnResizeDirection` - -```tsx -columnResizeDirection?: 'ltr' | 'rtl' -``` - -Enables or disables right-to-left support for resizing the column. defaults to 'ltr'. - -### `onColumnSizingChange` - -```tsx -onColumnSizingChange?: OnChangeFn -``` - -This optional function will be called when the columnSizing state changes. If you provide this function, you will be responsible for maintaining its state yourself. You can pass this state back to the table via the `state.columnSizing` table option. - -### `onColumnSizingInfoChange` - -```tsx -onColumnSizingInfoChange?: OnChangeFn -``` - -This optional function will be called when the columnSizingInfo state changes. If you provide this function, you will be responsible for maintaining its state yourself. You can pass this state back to the table via the `state.columnSizingInfo` table option. - -## Table API - -### `setColumnSizing` - -```tsx -setColumnSizing: (updater: Updater) => void -``` - -Sets the column sizing state using an updater function or a value. This will trigger the underlying `onColumnSizingChange` function if one is passed to the table options, otherwise the state will be managed automatically by the table. - -### `setColumnSizingInfo` - -```tsx -setColumnSizingInfo: (updater: Updater) => void -``` - -Sets the column sizing info state using an updater function or a value. This will trigger the underlying `onColumnSizingInfoChange` function if one is passed to the table options, otherwise the state will be managed automatically by the table. - -### `resetColumnSizing` - -```tsx -resetColumnSizing: (defaultState?: boolean) => void -``` - -Resets column sizing to its initial state. If `defaultState` is `true`, the default state for the table will be used instead of the initialValue provided to the table. - -### `resetHeaderSizeInfo` - -```tsx -resetHeaderSizeInfo: (defaultState?: boolean) => void -``` - -Resets column sizing info to its initial state. If `defaultState` is `true`, the default state for the table will be used instead of the initialValue provided to the table. - -### `getTotalSize` - -```tsx -getTotalSize: () => number -``` - -Returns the total size of the table by calculating the sum of the sizes of all leaf-columns. - -### `getLeftTotalSize` - -```tsx -getLeftTotalSize: () => number -``` - -If pinning, returns the total size of the left portion of the table by calculating the sum of the sizes of all left leaf-columns. - -### `getCenterTotalSize` - -```tsx -getCenterTotalSize: () => number -``` - -If pinning, returns the total size of the center portion of the table by calculating the sum of the sizes of all unpinned/center leaf-columns. - -### `getRightTotalSize` - -```tsx -getRightTotalSize: () => number -``` - -If pinning, returns the total size of the right portion of the table by calculating the sum of the sizes of all right leaf-columns. diff --git a/docs/api/features/column-visibility.md b/docs/api/features/column-visibility.md deleted file mode 100644 index e1280e7c03..0000000000 --- a/docs/api/features/column-visibility.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -title: Column Visibility APIs -id: column-visibility ---- - -## State - -Column visibility state is stored on the table using the following shape: - -```tsx -export type VisibilityState = Record - -export type VisibilityTableState = { - columnVisibility: VisibilityState -} -``` - -## Column Def Options - -### `enableHiding` - -```tsx -enableHiding?: boolean -``` - -Enables/disables hiding the column - -## Column API - -### `getCanHide` - -```tsx -getCanHide: () => boolean -``` - -Returns whether the column can be hidden - -### `getIsVisible` - -```tsx -getIsVisible: () => boolean -``` - -Returns whether the column is visible - -### `toggleVisibility` - -```tsx -toggleVisibility: (value?: boolean) => void -``` - -Toggles the column visibility - -### `getToggleVisibilityHandler` - -```tsx -getToggleVisibilityHandler: () => (event: unknown) => void -``` - -Returns a function that can be used to toggle the column visibility. This function can be used to bind to an event handler to a checkbox. - -## Table Options - -### `onColumnVisibilityChange` - -```tsx -onColumnVisibilityChange?: OnChangeFn -``` - -If provided, this function will be called with an `updaterFn` when `state.columnVisibility` changes. This overrides the default internal state management, so you will need to persist the state change either fully or partially outside of the table. - -### `enableHiding` - -```tsx -enableHiding?: boolean -``` - -Enables/disables hiding of columns. - -## Table API - -### `getVisibleFlatColumns` - -```tsx -getVisibleFlatColumns: () => Column[] -``` - -Returns a flat array of columns that are visible, including parent columns. - -### `getVisibleLeafColumns` - -```tsx -getVisibleLeafColumns: () => Column[] -``` - -Returns a flat array of leaf-node columns that are visible. - -### `getLeftVisibleLeafColumns` - -```tsx -getLeftVisibleLeafColumns: () => Column[] -``` - -If column pinning, returns a flat array of leaf-node columns that are visible in the left portion of the table. - -### `getRightVisibleLeafColumns` - -```tsx -getRightVisibleLeafColumns: () => Column[] -``` - -If column pinning, returns a flat array of leaf-node columns that are visible in the right portion of the table. - -### `getCenterVisibleLeafColumns` - -```tsx -getCenterVisibleLeafColumns: () => Column[] -``` - -If column pinning, returns a flat array of leaf-node columns that are visible in the unpinned/center portion of the table. - -### `setColumnVisibility` - -```tsx -setColumnVisibility: (updater: Updater) => void -``` - -Updates the column visibility state via an updater function or value - -### `resetColumnVisibility` - -```tsx -resetColumnVisibility: (defaultState?: boolean) => void -``` - -Resets the column visibility state to the initial state. If `defaultState` is provided, the state will be reset to `{}` - -### `toggleAllColumnsVisible` - -```tsx -toggleAllColumnsVisible: (value?: boolean) => void -``` - -Toggles the visibility of all columns - -### `getIsAllColumnsVisible` - -```tsx -getIsAllColumnsVisible: () => boolean -``` - -Returns whether all columns are visible - -### `getIsSomeColumnsVisible` - -```tsx -getIsSomeColumnsVisible: () => boolean -``` - -Returns whether some columns are visible - -### `getToggleAllColumnsVisibilityHandler` - -```tsx -getToggleAllColumnsVisibilityHandler: () => ((event: unknown) => void) -``` - -Returns a handler for toggling the visibility of all columns, meant to be bound to a `input[type=checkbox]` element. - -## Row API - -### `getVisibleCells` - -```tsx -getVisibleCells: () => Cell[] -``` - -Returns an array of cells that account for column visibility for the row. \ No newline at end of file diff --git a/docs/api/features/expanding.md b/docs/api/features/expanding.md deleted file mode 100644 index af7ab0db43..0000000000 --- a/docs/api/features/expanding.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -title: Expanding APIs -id: expanding ---- - -## State - -Expanding state is stored on the table using the following shape: - -```tsx -export type ExpandedState = true | Record - -export type ExpandedTableState = { - expanded: ExpandedState -} -``` - -## Row API - -### `toggleExpanded` - -```tsx -toggleExpanded: (expanded?: boolean) => void -``` - -Toggles the expanded state (or sets it if `expanded` is provided) for the row. - -### `getIsExpanded` - -```tsx -getIsExpanded: () => boolean -``` - -Returns whether the row is expanded. - -### `getIsAllParentsExpanded` - -```tsx -getIsAllParentsExpanded: () => boolean -``` - -Returns whether all parent rows of the row are expanded. - -### `getCanExpand` - -```tsx -getCanExpand: () => boolean -``` - -Returns whether the row can be expanded. - -### `getToggleExpandedHandler` - -```tsx -getToggleExpandedHandler: () => () => void -``` - -Returns a function that can be used to toggle the expanded state of the row. This function can be used to bind to an event handler to a button. - -## Table Options - -### `manualExpanding` - -```tsx -manualExpanding?: boolean -``` - -Enables manual row expansion. If this is set to `true`, `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. This is useful if you are doing server-side expansion. - -### `onExpandedChange` - -```tsx -onExpandedChange?: OnChangeFn -``` - -This function is called when the `expanded` table state changes. If a function is provided, you will be responsible for managing this state on your own. To pass the managed state back to the table, use the `tableOptions.state.expanded` option. - -### `autoResetExpanded` - -```tsx -autoResetExpanded?: boolean -``` - -Enable this setting to automatically reset the expanded state of the table when expanding state changes. - -### `enableExpanding` - -```tsx -enableExpanding?: boolean -``` - -Enable/disable expanding for all rows. - -### `getExpandedRowModel` - -```tsx -getExpandedRowModel?: (table: Table) => () => RowModel -``` - -This function is responsible for returning the expanded row model. If this function is not provided, the table will not expand rows. You can use the default exported `getExpandedRowModel` function to get the expanded row model or implement your own. - -### `getIsRowExpanded` - -```tsx -getIsRowExpanded?: (row: Row) => boolean -``` - -If provided, allows you to override the default behavior of determining whether a row is currently expanded. - -### `getRowCanExpand` - -```tsx -getRowCanExpand?: (row: Row) => boolean -``` - -If provided, allows you to override the default behavior of determining whether a row can be expanded. - -### `paginateExpandedRows` - -```tsx -paginateExpandedRows?: boolean -``` - -If `true` expanded rows will be paginated along with the rest of the table (which means expanded rows may span multiple pages). - -If `false` expanded rows will not be considered for pagination (which means expanded rows will always render on their parents page. This also means more rows will be rendered than the set page size) - -## Table API - -### `setExpanded` - -```tsx -setExpanded: (updater: Updater) => void -``` - -Updates the expanded state of the table via an update function or value - -### `toggleAllRowsExpanded` - -```tsx -toggleAllRowsExpanded: (expanded?: boolean) => void -``` - -Toggles the expanded state for all rows. Optionally, provide a value to set the expanded state to. - -### `resetExpanded` - -```tsx -resetExpanded: (defaultState?: boolean) => void -``` - -Reset the expanded state of the table to the initial state. If `defaultState` is provided, the expanded state will be reset to `{}` - -### `getCanSomeRowsExpand` - -```tsx -getCanSomeRowsExpand: () => boolean -``` - -Returns whether there are any rows that can be expanded. - -### `getToggleAllRowsExpandedHandler` - -```tsx -getToggleAllRowsExpandedHandler: () => (event: unknown) => void -``` - -Returns a handler that can be used to toggle the expanded state of all rows. This handler is meant to be used with an `input[type=checkbox]` element. - -### `getIsSomeRowsExpanded` - -```tsx -getIsSomeRowsExpanded: () => boolean -``` - -Returns whether there are any rows that are currently expanded. - -### `getIsAllRowsExpanded` - -```tsx -getIsAllRowsExpanded: () => boolean -``` - -Returns whether all rows are currently expanded. - -### `getExpandedDepth` - -```tsx -getExpandedDepth: () => number -``` - -Returns the maximum depth of the expanded rows. - -### `getExpandedRowModel` - -```tsx -getExpandedRowModel: () => RowModel -``` - -Returns the row model after expansion has been applied. - -### `getPreExpandedRowModel` - -```tsx -getPreExpandedRowModel: () => RowModel -``` - -Returns the row model before expansion has been applied. diff --git a/docs/api/features/filters.md b/docs/api/features/filters.md deleted file mode 100644 index 167fec059f..0000000000 --- a/docs/api/features/filters.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Filter APIs -id: filters ---- - - - -The Filtering API docs are now split into multiple API doc pages: - -- [Column Faceting](../../../guide/column-faceting.md) -- [Global Faceting](../../../guide/global-faceting.md) -- [Column Filtering](../../../guide/column-filtering.md) -- [Global Filtering](../../../guide/global-filtering.md) \ No newline at end of file diff --git a/docs/api/features/global-faceting.md b/docs/api/features/global-faceting.md deleted file mode 100644 index 820df889ff..0000000000 --- a/docs/api/features/global-faceting.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Global Faceting APIs -id: global-faceting ---- - -## Table API - -### `getGlobalFacetedRowModel` - -```tsx -getGlobalFacetedRowModel: () => RowModel -``` - -Returns the faceted row model for the global filter. - -### `getGlobalFacetedUniqueValues` - -```tsx -getGlobalFacetedUniqueValues: () => Map -``` - -Returns the faceted unique values for the global filter. - -### `getGlobalFacetedMinMaxValues` - -```tsx -getGlobalFacetedMinMaxValues: () => [number, number] -``` - -Returns the faceted min and max values for the global filter. diff --git a/docs/api/features/global-filtering.md b/docs/api/features/global-filtering.md deleted file mode 100644 index 47b79d15bc..0000000000 --- a/docs/api/features/global-filtering.md +++ /dev/null @@ -1,291 +0,0 @@ ---- -title: Global Filtering APIs -id: global-filtering ---- - -## Can-Filter - -The ability for a column to be **globally** filtered is determined by the following: - -- The column was defined a valid `accessorKey`/`accessorFn`. -- If provided, `options.getColumnCanGlobalFilter` returns `true` for the given column. If it is not provided, the column is assumed to be globally filterable if the value in the first row is a `string` or `number` type. -- `column.enableColumnFilter` is not set to `false` -- `options.enableColumnFilters` is not set to `false` -- `options.enableFilters` is not set to `false` - -## State - -Filter state is stored on the table using the following shape: - -```tsx -export interface GlobalFilterTableState { - globalFilter: any -} -``` - -## Filter Functions - -You can use the same filter functions that are available for column filtering for global filtering. See the [Column Filtering](../../../guide/column-filtering.md) to learn more about filter functions. - -#### Using Filter Functions - -Filter functions can be used/referenced/defined by passing the following to `options.globalFilterFn`: - -- A `string` that references a built-in filter function -- A function directly provided to the `options.globalFilterFn` option - -The final list of filter functions available for the `tableOptions.globalFilterFn` options use the following type: - -```tsx -export type FilterFnOption = - | 'auto' - | BuiltInFilterFn - | FilterFn -``` - -#### Filter Meta - -Filtering data can often expose additional information about the data that can be used to aid other future operations on the same data. A good example of this concept is a ranking-system like that of [`match-sorter`](https://github.com/kentcdodds/match-sorter) that simultaneously ranks, filters and sorts data. While utilities like `match-sorter` make a lot of sense for single-dimensional filter+sort tasks, the decoupled filtering/sorting architecture of building a table makes them very difficult and slow to use. - -To make a ranking/filtering/sorting system work with tables, `filterFn`s can optionally mark results with a **filter meta** value that can be used later to sort/group/etc the data to your liking. This is done by calling the `addMeta` function supplied to your custom `filterFn`. - -Below is an example using our own `match-sorter-utils` package (a utility fork of `match-sorter`) to rank, filter, and sort the data - -```tsx -import { sortingFns } from '@tanstack/[adapter]-table' - -import { rankItem, compareItems } from '@tanstack/match-sorter-utils' - -const fuzzyFilter = (row, columnId, value, addMeta) => { - // Rank the item - const itemRank = rankItem(row.getValue(columnId), value) - - // Store the ranking info - addMeta(itemRank) - - // Return if the item should be filtered in/out - return itemRank.passed -} - -const fuzzySort = (rowA, rowB, columnId) => { - let dir = 0 - - // Only sort by rank if the column has ranking information - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId]!, - rowB.columnFiltersMeta[columnId]! - ) - } - - // Provide an alphanumeric fallback for when the item ranks are equal - return dir === 0 ? sortingFns.alphanumeric(rowA, rowB, columnId) : dir -} -``` - -## Column Def Options - -### `enableGlobalFilter` - -```tsx -enableGlobalFilter?: boolean -``` - -Enables/disables the **global** filter for this column. - -## Column API - -### `getCanGlobalFilter` - -```tsx -getCanGlobalFilter: () => boolean -``` - -Returns whether or not the column can be **globally** filtered. Set to `false` to disable a column from being scanned during global filtering. - -## Row API - -### `columnFiltersMeta` - -```tsx -columnFiltersMeta: Record -``` - -The column filters meta map for the row. This object tracks any filter meta for a row as optionally provided during the filtering process. - -## Table Options - -### `filterFns` - -```tsx -filterFns?: Record -``` - -This option allows you to define custom filter functions that can be referenced in a column's `filterFn` option by their key. -Example: - -```tsx -declare module '@tanstack/table-core' { - interface FilterFns { - myCustomFilter: FilterFn - } -} - -const column = columnHelper.data('key', { - filterFn: 'myCustomFilter', -}) - -const table = useReactTable({ - columns: [column], - filterFns: { - myCustomFilter: (rows, columnIds, filterValue) => { - // return the filtered rows - }, - }, -}) -``` - -### `filterFromLeafRows` - -```tsx -filterFromLeafRows?: boolean -``` - -By default, filtering is done from parent rows down (so if a parent row is filtered out, all of its children will be filtered out as well). Setting this option to `true` will cause filtering to be done from leaf rows up (which means parent rows will be included so long as one of their child or grand-child rows is also included). - -### `maxLeafRowFilterDepth` - -```tsx -maxLeafRowFilterDepth?: number -``` - -By default, filtering is done for all rows (max depth of 100), no matter if they are root level parent rows or the child leaf rows of a parent row. Setting this option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. - -This is useful for situations where you want a row's entire child hierarchy to be visible regardless of the applied filter. - -### `enableFilters` - -```tsx -enableFilters?: boolean -``` - -Enables/disables all filters for the table. - -### `manualFiltering` - -```tsx -manualFiltering?: boolean -``` - -Disables the `getFilteredRowModel` from being used to filter data. This may be useful if your table needs to dynamically support both client-side and server-side filtering. - -### `getFilteredRowModel` - -```tsx -getFilteredRowModel?: ( - table: Table -) => () => RowModel -``` - -If provided, this function is called **once** per table and should return a **new function** which will calculate and return the row model for the table when it's filtered. - -- For server-side filtering, this function is unnecessary and can be ignored since the server should already return the filtered row model. -- For client-side filtering, this function is required. A default implementation is provided via any table adapter's `{ getFilteredRowModel }` export. - -Example: - -```tsx -import { getFilteredRowModel } from '@tanstack/[adapter]-table' - - getFilteredRowModel: getFilteredRowModel(), -}) -``` - -### `globalFilterFn` - -```tsx -globalFilterFn?: FilterFn | keyof FilterFns | keyof BuiltInFilterFns -``` - -The filter function to use for global filtering. - -Options: - -- A `string` referencing a [built-in filter function](#filter-functions)) -- A `string` that references a custom filter functions provided via the `tableOptions.filterFns` option -- A [custom filter function](#filter-functions) - -### `onGlobalFilterChange` - -```tsx -onGlobalFilterChange?: OnChangeFn -``` - -If provided, this function will be called with an `updaterFn` when `state.globalFilter` changes. This overrides the default internal state management, so you will need to persist the state change either fully or partially outside of the table. - -### `enableGlobalFilter` - -```tsx -enableGlobalFilter?: boolean -``` - -Enables/disables the global filter for the table. - -### `getColumnCanGlobalFilter` - -```tsx -getColumnCanGlobalFilter?: (column: Column) => boolean -``` - -If provided, this function will be called with the column and should return `true` or `false` to indicate whether this column should be used for global filtering. -This is useful if the column can contain data that is not `string` or `number` (i.e. `undefined`). - -## Table API - -### `getPreFilteredRowModel` - -```tsx -getPreFilteredRowModel: () => RowModel -``` - -Returns the row model for the table before any **column** filtering has been applied. - -### `getFilteredRowModel` - -```tsx -getFilteredRowModel: () => RowModel -``` - -Returns the row model for the table after **column** filtering has been applied. - -### `setGlobalFilter` - -```tsx -setGlobalFilter: (updater: Updater) => void -``` - -Sets or updates the `state.globalFilter` state. - -### `resetGlobalFilter` - -```tsx -resetGlobalFilter: (defaultState?: boolean) => void -``` - -Resets the **globalFilter** state to `initialState.globalFilter`, or `true` can be passed to force a default blank state reset to `undefined`. - -### `getGlobalAutoFilterFn` - -```tsx -getGlobalAutoFilterFn: (columnId: string) => FilterFn | undefined -``` - -Currently, this function returns the built-in `includesString` filter function. In future releases, it may return more dynamic filter functions based on the nature of the data provided. - -### `getGlobalFilterFn` - -```tsx -getGlobalFilterFn: (columnId: string) => FilterFn | undefined -``` - -Returns the global filter function (either user-defined or automatic, depending on configuration) for the table. diff --git a/docs/api/features/grouping.md b/docs/api/features/grouping.md deleted file mode 100644 index b9c21631fc..0000000000 --- a/docs/api/features/grouping.md +++ /dev/null @@ -1,353 +0,0 @@ ---- -title: Grouping APIs -id: grouping ---- - -## State - -Grouping state is stored on the table using the following shape: - -```tsx -export type GroupingState = string[] - -export type GroupingTableState = { - grouping: GroupingState -} -``` - -## Aggregation Functions - -The following aggregation functions are built-in to the table core: - -- `sum` - - Sums the values of a group of rows -- `min` - - Finds the minimum value of a group of rows -- `max` - - Finds the maximum value of a group of rows -- `extent` - - Finds the minimum and maximum values of a group of rows -- `mean` - - Finds the mean/average value of a group of rows -- `median` - - Finds the median value of a group of rows -- `unique` - - Finds the unique values of a group of rows -- `uniqueCount` - - Finds the number of unique values of a group of rows -- `count` - - Calculates the number of rows in a group - -Every grouping function receives: - -- A function to retrieve the leaf values of the groups rows -- A function to retrieve the immediate-child values of the groups rows - -and should return a value (usually primitive) to build the aggregated row model. - -This is the type signature for every aggregation function: - -```tsx -export type AggregationFn = ( - getLeafRows: () => Row[], - getChildRows: () => Row[] -) => any -``` - -#### Using Aggregation Functions - -Aggregation functions can be used/referenced/defined by passing the following to `columnDefinition.aggregationFn`: - -- A `string` that references a built-in aggregation function -- A `string` that references a custom aggregation functions provided via the `tableOptions.aggregationFns` option -- A function directly provided to the `columnDefinition.aggregationFn` option - -The final list of aggregation functions available for the `columnDef.aggregationFn` use the following type: - -```tsx -export type AggregationFnOption = - | 'auto' - | keyof AggregationFns - | BuiltInAggregationFn - | AggregationFn -``` - -## Column Def Options - -### `aggregationFn` - -```tsx -aggregationFn?: AggregationFn | keyof AggregationFns | keyof BuiltInAggregationFns -``` - -The aggregation function to use with this column. - -Options: - -- A `string` referencing a [built-in aggregation function](#aggregation-functions)) -- A [custom aggregation function](#aggregation-functions) - -### `aggregatedCell` - -```tsx -aggregatedCell?: Renderable< - { - table: Table - row: Row - column: Column - cell: Cell - getValue: () => any - renderValue: () => any - } -> -``` - -The cell to display each row for the column if the cell is an aggregate. If a function is passed, it will be passed a props object with the context of the cell and should return the property type for your adapter (the exact type depends on the adapter being used). - -### `enableGrouping` - -```tsx -enableGrouping?: boolean -``` - -Enables/disables grouping for this column. - -### `getGroupingValue` - -```tsx -getGroupingValue?: (row: TData) => any -``` - -Specify a value to be used for grouping rows on this column. If this option is not specified, the value derived from `accessorKey` / `accessorFn` will be used instead. - -## Column API - -### `aggregationFn` - -```tsx -aggregationFn?: AggregationFnOption -``` - -The resolved aggregation function for the column. - -### `getCanGroup` - -```tsx -getCanGroup: () => boolean -``` - -Returns whether or not the column can be grouped. - -### `getIsGrouped` - -```tsx -getIsGrouped: () => boolean -``` - -Returns whether or not the column is currently grouped. - -### `getGroupedIndex` - -```tsx -getGroupedIndex: () => number -``` - -Returns the index of the column in the grouping state. - -### `toggleGrouping` - -```tsx -toggleGrouping: () => void -``` - -Toggles the grouping state of the column. - -### `getToggleGroupingHandler` - -```tsx -getToggleGroupingHandler: () => () => void -``` - -Returns a function that toggles the grouping state of the column. This is useful for passing to the `onClick` prop of a button. - -### `getAutoAggregationFn` - -```tsx -getAutoAggregationFn: () => AggregationFn | undefined -``` - -Returns the automatically inferred aggregation function for the column. - -### `getAggregationFn` - -```tsx -getAggregationFn: () => AggregationFn | undefined -``` - -Returns the aggregation function for the column. - -## Row API - -### `groupingColumnId` - -```tsx -groupingColumnId?: string -``` - -If this row is grouped, this is the id of the column that this row is grouped by. - -### `groupingValue` - -```tsx -groupingValue?: any -``` - -If this row is grouped, this is the unique/shared value for the `groupingColumnId` for all of the rows in this group. - -### `getIsGrouped` - -```tsx -getIsGrouped: () => boolean -``` - -Returns whether or not the row is currently grouped. - -### `getGroupingValue` - -```tsx -getGroupingValue: (columnId: string) => unknown -``` - -Returns the grouping value for any row and column (including leaf rows). - -## Table Options - -### `aggregationFns` - -```tsx -aggregationFns?: Record -``` - -This option allows you to define custom aggregation functions that can be referenced in a column's `aggregationFn` option by their key. -Example: - -```tsx -declare module '@tanstack/table-core' { - interface AggregationFns { - myCustomAggregation: AggregationFn - } -} - -const column = columnHelper.data('key', { - aggregationFn: 'myCustomAggregation', -}) - -const table = useReactTable({ - columns: [column], - aggregationFns: { - myCustomAggregation: (columnId, leafRows, childRows) => { - // return the aggregated value - }, - }, -}) -``` - -### `manualGrouping` - -```tsx -manualGrouping?: boolean -``` - -Enables manual grouping. If this option is set to `true`, the table will not automatically group rows using `getGroupedRowModel()` and instead will expect you to manually group the rows before passing them to the table. This is useful if you are doing server-side grouping and aggregation. - -### `onGroupingChange` - -```tsx -onGroupingChange?: OnChangeFn -``` - -If this function is provided, it will be called when the grouping state changes and you will be expected to manage the state yourself. You can pass the managed state back to the table via the `tableOptions.state.grouping` option. - -### `enableGrouping` - -```tsx -enableGrouping?: boolean -``` - -Enables/disables grouping for all columns. - -### `getGroupedRowModel` - -```tsx -getGroupedRowModel?: (table: Table) => () => RowModel -``` - -Returns the row model after grouping has taken place, but no further. - -### `groupedColumnMode` - -```tsx -groupedColumnMode?: false | 'reorder' | 'remove' // default: `reorder` -``` - -Grouping columns are automatically reordered by default to the start of the columns list. If you would rather remove them or leave them as-is, set the appropriate mode here. - -## Table API - -### `setGrouping` - -```tsx -setGrouping: (updater: Updater) => void -``` - -Sets or updates the `state.grouping` state. - -### `resetGrouping` - -```tsx -resetGrouping: (defaultState?: boolean) => void -``` - -Resets the **grouping** state to `initialState.grouping`, or `true` can be passed to force a default blank state reset to `[]`. - -### `getPreGroupedRowModel` - -```tsx -getPreGroupedRowModel: () => RowModel -``` - -Returns the row model for the table before any grouping has been applied. - -### `getGroupedRowModel` - -```tsx -getGroupedRowModel: () => RowModel -``` - -Returns the row model for the table after grouping has been applied. - -## Cell API - -### `getIsAggregated` - -```tsx -getIsAggregated: () => boolean -``` - -Returns whether or not the cell is currently aggregated. - -### `getIsGrouped` - -```tsx -getIsGrouped: () => boolean -``` - -Returns whether or not the cell is currently grouped. - -### `getIsPlaceholder` - -```tsx -getIsPlaceholder: () => boolean -``` - -Returns whether or not the cell is currently a placeholder. \ No newline at end of file diff --git a/docs/api/features/pagination.md b/docs/api/features/pagination.md deleted file mode 100644 index 5e80d5a7a2..0000000000 --- a/docs/api/features/pagination.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: Pagination APIs -id: pagination ---- - -## State - -Pagination state is stored on the table using the following shape: - -```tsx -export type PaginationState = { - pageIndex: number - pageSize: number -} - -export type PaginationTableState = { - pagination: PaginationState -} - -export type PaginationInitialTableState = { - pagination?: Partial -} -``` - -## Table Options - -### `manualPagination` - -```tsx -manualPagination?: boolean -``` - -Enables manual pagination. If this option is set to `true`, the table will not automatically paginate rows using `getPaginationRowModel()` and instead will expect you to manually paginate the rows before passing them to the table. This is useful if you are doing server-side pagination and aggregation. - -### `pageCount` - -```tsx -pageCount?: number -``` - -When manually controlling pagination, you can supply a total `pageCount` value to the table if you know it. If you do not know how many pages there are, you can set this to `-1`. Alternatively, you can provide a `rowCount` value and the table will calculate the `pageCount` internally. - -### `rowCount` - -```tsx -rowCount?: number -``` - -When manually controlling pagination, you can supply a total `rowCount` value to the table if you know it. `pageCount` will be calculated internally from `rowCount` and `pageSize`. - -### `autoResetPageIndex` - -```tsx -autoResetPageIndex?: boolean -``` - -If set to `true`, pagination will be reset to the first page when page-altering state changes eg. `data` is updated, filters change, grouping changes, etc. - -> 🧠 Note: This option defaults to `false` if `manualPagination` is set to `true` - -### `onPaginationChange` - -```tsx -onPaginationChange?: OnChangeFn -``` - -If this function is provided, it will be called when the pagination state changes and you will be expected to manage the state yourself. You can pass the managed state back to the table via the `tableOptions.state.pagination` option. - -### `getPaginationRowModel` - -```tsx -getPaginationRowModel?: (table: Table) => () => RowModel -``` - -Returns the row model after pagination has taken place, but no further. - -Pagination columns are automatically reordered by default to the start of the columns list. If you would rather remove them or leave them as-is, set the appropriate mode here. - -## Table API - -### `setPagination` - -```tsx -setPagination: (updater: Updater) => void -``` - -Sets or updates the `state.pagination` state. - -### `resetPagination` - -```tsx -resetPagination: (defaultState?: boolean) => void -``` - -Resets the **pagination** state to `initialState.pagination`, or `true` can be passed to force a default blank state reset to `[]`. - -### `setPageIndex` - -```tsx -setPageIndex: (updater: Updater) => void -``` - -Updates the page index using the provided function or value. - -### `resetPageIndex` - -```tsx -resetPageIndex: (defaultState?: boolean) => void -``` - -Resets the page index to its initial state. If `defaultState` is `true`, the page index will be reset to `0` regardless of initial state. - -### `setPageSize` - -```tsx -setPageSize: (updater: Updater) => void -``` - -Updates the page size using the provided function or value. - -### `resetPageSize` - -```tsx -resetPageSize: (defaultState?: boolean) => void -``` - -Resets the page size to its initial state. If `defaultState` is `true`, the page size will be reset to `10` regardless of initial state. - -### `getPageOptions` - -```tsx -getPageOptions: () => number[] -``` - -Returns an array of page options (zero-index-based) for the current page size. - -### `getCanPreviousPage` - -```tsx -getCanPreviousPage: () => boolean -``` - -Returns whether the table can go to the previous page. - -### `getCanNextPage` - -```tsx -getCanNextPage: () => boolean -``` - -Returns whether the table can go to the next page. - -### `previousPage` - -```tsx -previousPage: () => void -``` - -Decrements the page index by one, if possible. - -### `nextPage` - -```tsx -nextPage: () => void -``` - -Increments the page index by one, if possible. - -### `firstPage` - -```tsx -firstPage: () => void -``` - -Sets the page index to `0`. - -### `lastPage` - -```tsx -lastPage: () => void -``` - -Sets the page index to the last available page. - -### `getPageCount` - -```tsx -getPageCount: () => number -``` - -Returns the page count. If manually paginating or controlling the pagination state, this will come directly from the `options.pageCount` table option, otherwise it will be calculated from the table data using the total row count and current page size. - -### `getPrePaginationRowModel` - -```tsx -getPrePaginationRowModel: () => RowModel -``` - -Returns the row model for the table before any pagination has been applied. - -### `getPaginationRowModel` - -```tsx -getPaginationRowModel: () => RowModel -``` - -Returns the row model for the table after pagination has been applied. diff --git a/docs/api/features/pinning.md b/docs/api/features/pinning.md deleted file mode 100644 index 15f18316df..0000000000 --- a/docs/api/features/pinning.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Pinning APIs -id: pinning ---- - - - -The pinning apis are now split into multiple api pages: - -- [Column Pinning](../../../guide/column-pinning.md) -- [Row Pinning](../../../guide/row-pinning.md) \ No newline at end of file diff --git a/docs/api/features/row-pinning.md b/docs/api/features/row-pinning.md deleted file mode 100644 index 52e46d5628..0000000000 --- a/docs/api/features/row-pinning.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Row Pinning APIs -id: row-pinning ---- - -## Can-Pin - -The ability for a row to be **pinned** is determined by the following: - -- `options.enableRowPinning` resolves to `true` -- `options.enablePinning` is not set to `false` - -## State - -Pinning state is stored on the table using the following shape: - -```tsx -export type RowPinningPosition = false | 'top' | 'bottom' - -export type RowPinningState = { - top?: string[] - bottom?: string[] -} - -export type RowPinningRowState = { - rowPinning: RowPinningState -} -``` - -## Table Options - -### `enableRowPinning` - -```tsx -enableRowPinning?: boolean | ((row: Row) => boolean) -``` - -Enables/disables row pinning for all rows in the table. - -### `keepPinnedRows` - -```tsx -keepPinnedRows?: boolean -``` - -When `false`, pinned rows will not be visible if they are filtered or paginated out of the table. When `true`, pinned rows will always be visible regardless of filtering or pagination. Defaults to `true`. - -### `onRowPinningChange` - -```tsx -onRowPinningChange?: OnChangeFn -``` - -If provided, this function will be called with an `updaterFn` when `state.rowPinning` changes. This overrides the default internal state management, so you will also need to supply `state.rowPinning` from your own managed state. - -## Table API - -### `setRowPinning` - -```tsx -setRowPinning: (updater: Updater) => void -``` - -Sets or updates the `state.rowPinning` state. - -### `resetRowPinning` - -```tsx -resetRowPinning: (defaultState?: boolean) => void -``` - -Resets the **rowPinning** state to `initialState.rowPinning`, or `true` can be passed to force a default blank state reset to `{}`. - -### `getIsSomeRowsPinned` - -```tsx -getIsSomeRowsPinned: (position?: RowPinningPosition) => boolean -``` - -Returns whether or not any rows are pinned. Optionally specify to only check for pinned rows in either the `top` or `bottom` position. - -### `getTopRows` - -```tsx -getTopRows: () => Row[] -``` - -Returns all top pinned rows. - -### `getBottomRows` - -```tsx -getBottomRows: () => Row[] -``` - -Returns all bottom pinned rows. - -### `getCenterRows` - -```tsx -getCenterRows: () => Row[] -``` - -Returns all rows that are not pinned to the top or bottom. - -## Row API - -### `pin` - -```tsx -pin: (position: RowPinningPosition) => void -``` - -Pins a row to the `'top'` or `'bottom'`, or unpins the row to the center if `false` is passed. - -### `getCanPin` - -```tsx -getCanPin: () => boolean -``` - -Returns whether or not the row can be pinned. - -### `getIsPinned` - -```tsx -getIsPinned: () => RowPinningPosition -``` - -Returns the pinned position of the row. (`'top'`, `'bottom'` or `false`) - -### `getPinnedIndex` - -```tsx -getPinnedIndex: () => number -``` - -Returns the numeric pinned index of the row within a pinned row group. \ No newline at end of file diff --git a/docs/api/features/row-selection.md b/docs/api/features/row-selection.md deleted file mode 100644 index e21cb6e5bc..0000000000 --- a/docs/api/features/row-selection.md +++ /dev/null @@ -1,228 +0,0 @@ ---- -title: Row Selection APIs -id: row-selection ---- - -## State - -Row selection state is stored on the table using the following shape: - -```tsx -export type RowSelectionState = Record - -export type RowSelectionTableState = { - rowSelection: RowSelectionState -} -``` - -By default, the row selection state uses the index of each row as the row identifiers. Row selection state can instead be tracked with a custom unique row id by passing in a custom [getRowId](../../core/table.md#getrowid) function to the the table. - -## Table Options - -### `enableRowSelection` - -```tsx -enableRowSelection?: boolean | ((row: Row) => boolean) -``` - -- Enables/disables row selection for all rows in the table OR -- A function that given a row, returns whether to enable/disable row selection for that row - -### `enableMultiRowSelection` - -```tsx -enableMultiRowSelection?: boolean | ((row: Row) => boolean) -``` - -- Enables/disables multiple row selection for all rows in the table OR -- A function that given a row, returns whether to enable/disable multiple row selection for that row's children/grandchildren - -### `enableSubRowSelection` - -```tsx -enableSubRowSelection?: boolean | ((row: Row) => boolean) -``` - -Enables/disables automatic sub-row selection when a parent row is selected, or a function that enables/disables automatic sub-row selection for each row. - -(Use in combination with expanding or grouping features) - -### `onRowSelectionChange` - -```tsx -onRowSelectionChange?: OnChangeFn -``` - -If provided, this function will be called with an `updaterFn` when `state.rowSelection` changes. This overrides the default internal state management, so you will need to persist the state change either fully or partially outside of the table. - -## Table API - -### `getToggleAllRowsSelectedHandler` - -```tsx -getToggleAllRowsSelectedHandler: () => (event: unknown) => void -``` - -Returns a handler that can be used to toggle all rows in the table. - -### `getToggleAllPageRowsSelectedHandler` - -```tsx -getToggleAllPageRowsSelectedHandler: () => (event: unknown) => void -``` - -Returns a handler that can be used to toggle all rows on the current page. - -### `setRowSelection` - -```tsx -setRowSelection: (updater: Updater) => void -``` - -Sets or updates the `state.rowSelection` state. - -### `resetRowSelection` - -```tsx -resetRowSelection: (defaultState?: boolean) => void -``` - -Resets the **rowSelection** state to the `initialState.rowSelection`, or `true` can be passed to force a default blank state reset to `{}`. - -### `getIsAllRowsSelected` - -```tsx -getIsAllRowsSelected: () => boolean -``` - -Returns whether or not all rows in the table are selected. - -### `getIsAllPageRowsSelected` - -```tsx -getIsAllPageRowsSelected: () => boolean -``` - -Returns whether or not all rows on the current page are selected. - -### `getIsSomeRowsSelected` - -```tsx -getIsSomeRowsSelected: () => boolean -``` - -Returns whether or not any rows in the table are selected. - -### `getIsSomePageRowsSelected` - -```tsx -getIsSomePageRowsSelected: () => boolean -``` - -Returns whether or not any rows on the current page are selected. - -### `toggleAllRowsSelected` - -```tsx -toggleAllRowsSelected: (value: boolean) => void -``` - -Selects/deselects all rows in the table. - -### `toggleAllPageRowsSelected` - -```tsx -toggleAllPageRowsSelected: (value: boolean) => void -``` - -Selects/deselects all rows on the current page. - -### `getPreSelectedRowModel` - -```tsx -getPreSelectedRowModel: () => RowModel -``` - -### `getSelectedRowModel` - -```tsx -getSelectedRowModel: () => RowModel -``` - -### `getFilteredSelectedRowModel` - -```tsx -getFilteredSelectedRowModel: () => RowModel -``` - -### `getGroupedSelectedRowModel` - -```tsx -getGroupedSelectedRowModel: () => RowModel -``` - -## Row API - -### `getIsSelected` - -```tsx -getIsSelected: () => boolean -``` - -Returns whether or not the row is selected. - -### `getIsSomeSelected` - -```tsx -getIsSomeSelected: () => boolean -``` - -Returns whether or not some of the row's sub rows are selected. - -### `getIsAllSubRowsSelected` - -```tsx -getIsAllSubRowsSelected: () => boolean -``` - -Returns whether or not all of the row's sub rows are selected. - -### `getCanSelect` - -```tsx -getCanSelect: () => boolean -``` - -Returns whether or not the row can be selected. - -### `getCanMultiSelect` - -```tsx -getCanMultiSelect: () => boolean -``` - -Returns whether or not the row can multi-select. - -### `getCanSelectSubRows` - -```tsx -getCanSelectSubRows: () => boolean -``` - -Returns whether or not the row can select sub rows automatically when the parent row is selected. - -### `toggleSelected` - -```tsx -toggleSelected: (value?: boolean) => void -``` - -Selects/deselects the row. - -### `getToggleSelectedHandler` - -```tsx -getToggleSelectedHandler: () => (event: unknown) => void -``` - -Returns a handler that can be used to toggle the row. diff --git a/docs/api/features/sorting.md b/docs/api/features/sorting.md deleted file mode 100644 index e5b60eea50..0000000000 --- a/docs/api/features/sorting.md +++ /dev/null @@ -1,385 +0,0 @@ ---- -title: Sorting APIs -id: sorting ---- - -## State - -Sorting state is stored on the table using the following shape: - -```tsx -export type SortDirection = 'asc' | 'desc' - -export type ColumnSort = { - id: string - desc: boolean -} - -export type SortingState = ColumnSort[] - -export type SortingTableState = { - sorting: SortingState -} -``` - -## Sorting Functions - -The following sorting functions are built-in to the table core: - -- `alphanumeric` - - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. -- `alphanumericCaseSensitive` - - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. -- `text` - - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. -- `textCaseSensitive` - - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. -- `datetime` - - Sorts by time, use this if your values are `Date` objects. -- `basic` - - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. - -Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: - -| Return | Ascending Order | -| ------ | --------------- | -| `-1` | `a < b` | -| `0` | `a === b` | -| `1` | `a > b` | - -This is the type signature for every sorting function: - -```tsx -export type SortingFn = { - (rowA: Row, rowB: Row, columnId: string): number -} -``` - -#### Using Sorting Functions - -Sorting functions can be used/referenced/defined by passing the following to `columnDefinition.sortingFn`: - -- A `string` that references a built-in sorting function -- A `string` that references a custom sorting functions provided via the `tableOptions.sortingFns` option -- A function directly provided to the `columnDefinition.sortingFn` option - -The final list of sorting functions available for the `columnDef.sortingFn` use the following type: - -```tsx -export type SortingFnOption = - | 'auto' - | SortingFns - | BuiltInSortingFns - | SortingFn -``` - -## Column Def Options - -### `sortingFn` - -```tsx -sortingFn?: SortingFn | keyof SortingFns | keyof BuiltInSortingFns -``` - -The sorting function to use with this column. - -Options: - -- A `string` referencing a [built-in sorting function](#sorting-functions)) -- A [custom sorting function](#sorting-functions) - -### `sortDescFirst` - -```tsx -sortDescFirst?: boolean -``` - -Set to `true` for sorting toggles on this column to start in the descending direction. - -### `enableSorting` - -```tsx -enableSorting?: boolean -``` - -Enables/Disables sorting for this column. - -### `enableMultiSort` - -```tsx -enableMultiSort?: boolean -``` - -Enables/Disables multi-sorting for this column. - -### `invertSorting` - -```tsx -invertSorting?: boolean -``` - -Inverts the order of the sorting for this column. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring - -### `sortUndefined` - -```tsx -sortUndefined?: 'first' | 'last' | false | -1 | 1 // defaults to 1 -``` - -- `'first'` - - Undefined values will be pushed to the beginning of the list -- `'last'` - - Undefined values will be pushed to the end of the list -- `false` - - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) -- `-1` - - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) -- `1` - - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) - -> NOTE: `'first'` and `'last'` options are new in v8.16.0 - -## Column API - -### `getAutoSortingFn` - -```tsx -getAutoSortingFn: () => SortingFn -``` - -Returns a sorting function automatically inferred based on the columns values. - -### `getAutoSortDir` - -```tsx -getAutoSortDir: () => SortDirection -``` - -Returns a sort direction automatically inferred based on the columns values. - -### `getSortingFn` - -```tsx -getSortingFn: () => SortingFn -``` - -Returns the resolved sorting function to be used for this column - -### `getNextSortingOrder` - -```tsx -getNextSortingOrder: () => SortDirection | false -``` - -Returns the next sorting order. - -### `getCanSort` - -```tsx -getCanSort: () => boolean -``` - -Returns whether this column can be sorted. - -### `getCanMultiSort` - -```tsx -getCanMultiSort: () => boolean -``` - -Returns whether this column can be multi-sorted. - -### `getSortIndex` - -```tsx -getSortIndex: () => number -``` - -Returns the index position of this column's sorting within the sorting state - -### `getIsSorted` - -```tsx -getIsSorted: () => false | SortDirection -``` - -Returns whether this column is sorted. - -### `getFirstSortDir` - -```tsx -getFirstSortDir: () => SortDirection -``` - -Returns the first direction that should be used when sorting this column. - -### `clearSorting` - -```tsx -clearSorting: () => void -``` - -Removes this column from the table's sorting state - -### `toggleSorting` - -```tsx -toggleSorting: (desc?: boolean, isMulti?: boolean) => void -``` - -Toggles this columns sorting state. If `desc` is provided, it will force the sort direction to that value. If `isMulti` is provided, it will additivity multi-sort the column (or toggle it if it is already sorted). - -### `getToggleSortingHandler` - -```tsx -getToggleSortingHandler: () => undefined | ((event: unknown) => void) -``` - -Returns a function that can be used to toggle this column's sorting state. This is useful for attaching a click handler to the column header. - -## Table Options - -### `sortingFns` - -```tsx -sortingFns?: Record -``` - -This option allows you to define custom sorting functions that can be referenced in a column's `sortingFn` option by their key. -Example: - -```tsx -declare module '@tanstack/table-core' { - interface SortingFns { - myCustomSorting: SortingFn - } -} - -const column = columnHelper.data('key', { - sortingFn: 'myCustomSorting', -}) - -const table = useReactTable({ - columns: [column], - sortingFns: { - myCustomSorting: (rowA: any, rowB: any, columnId: any): number => - rowA.getValue(columnId).value < rowB.getValue(columnId).value ? 1 : -1, - }, -}) -``` - -### `manualSorting` - -```tsx -manualSorting?: boolean -``` - -Enables manual sorting for the table. If this is `true`, you will be expected to sort your data before it is passed to the table. This is useful if you are doing server-side sorting. - -### `onSortingChange` - -```tsx -onSortingChange?: OnChangeFn -``` - -If provided, this function will be called with an `updaterFn` when `state.sorting` changes. This overrides the default internal state management, so you will need to persist the state change either fully or partially outside of the table. - -### `enableSorting` - -```tsx -enableSorting?: boolean -``` - -Enables/Disables sorting for the table. - -### `enableSortingRemoval` - -```tsx -enableSortingRemoval?: boolean -``` - -Enables/Disables the ability to remove sorting for the table. -- If `true` then changing sort order will circle like: 'none' -> 'desc' -> 'asc' -> 'none' -> ... -- If `false` then changing sort order will circle like: 'none' -> 'desc' -> 'asc' -> 'desc' -> 'asc' -> ... - -### `enableMultiRemove` - -```tsx -enableMultiRemove?: boolean -``` - -Enables/disables the ability to remove multi-sorts - -### `enableMultiSort` - -```tsx -enableMultiSort?: boolean -``` - -Enables/Disables multi-sorting for the table. - -### `sortDescFirst` - -```tsx -sortDescFirst?: boolean -``` - -If `true`, all sorts will default to descending as their first toggle state. - -### `getSortedRowModel` - -```tsx -getSortedRowModel?: (table: Table) => () => RowModel -``` - -This function is used to retrieve the sorted row model. If using server-side sorting, this function is not required. To use client-side sorting, pass the exported `getSortedRowModel()` from your adapter to your table or implement your own. - -### `maxMultiSortColCount` - -```tsx -maxMultiSortColCount?: number -``` - -Set a maximum number of columns that can be multi-sorted. - -### `isMultiSortEvent` - -```tsx -isMultiSortEvent?: (e: unknown) => boolean -``` - -Pass a custom function that will be used to determine if a multi-sort event should be triggered. It is passed the event from the sort toggle handler and should return `true` if the event should trigger a multi-sort. - -## Table API - -### `setSorting` - -```tsx -setSorting: (updater: Updater) => void -``` - -Sets or updates the `state.sorting` state. - -### `resetSorting` - -```tsx -resetSorting: (defaultState?: boolean) => void -``` - -Resets the **sorting** state to `initialState.sorting`, or `true` can be passed to force a default blank state reset to `[]`. - -### `getPreSortedRowModel` - -```tsx -getPreSortedRowModel: () => RowModel -``` - -Returns the row model for the table before any sorting has been applied. - -### `getSortedRowModel` - -```tsx -getSortedRowModel: () => RowModel -``` - -Returns the row model for the table after sorting has been applied. diff --git a/docs/config.json b/docs/config.json index 19d268775f..97d77cf0ab 100644 --- a/docs/config.json +++ b/docs/config.json @@ -9,98 +9,84 @@ { "label": "Getting Started", "children": [ + { "label": "Overview", "to": "overview" }, + { "label": "Installation", "to": "installation" }, + { "label": "Devtools", "to": "devtools" }, + { "label": "Agent Skills (TanStack Intent)", "to": "agent-skills" } + ], + "frameworks": [ { - "label": "Introduction", - "to": "introduction" - }, - { - "label": "Overview", - "to": "overview" + "label": "alpine", + "children": [ + { "label": "Quick Start", "to": "framework/alpine/quick-start" } + ] }, { - "label": "Installation", - "to": "installation" + "label": "angular", + "children": [ + { "label": "Quick Start", "to": "framework/angular/quick-start" }, + { "label": "Migrating to V9", "to": "framework/angular/guide/migrating" } + ] }, { - "label": "Migrating to V8", - "to": "guide/migrating" + "label": "ember", + "children": [ + { "label": "Quick Start", "to": "framework/ember/quick-start" } + ] }, { - "label": "FAQ", - "to": "faq" - } - ], - "frameworks": [ - { - "label": "angular", + "label": "lit", "children": [ - { - "label": "Angular Table Adapter", - "to": "framework/angular/angular-table" - } + { "label": "Quick Start", "to": "framework/lit/quick-start" }, + { "label": "Migrating to V9", "to": "framework/lit/guide/migrating" } ] }, { - "label": "lit", + "label": "react", "children": [ - { - "label": "Lit Table Adapter", - "to": "framework/lit/lit-table" - } + { "label": "Quick Start", "to": "framework/react/quick-start" }, + { "label": "Migrating to V9", "to": "framework/react/guide/migrating" }, + { "label": "useLegacyTable Guide", "to": "framework/react/guide/use-legacy-table" } ] }, { - "label": "qwik", + "label": "preact", "children": [ - { - "label": "Qwik Table Adapter", - "to": "framework/qwik/qwik-table" - } + { "label": "Quick Start", "to": "framework/preact/quick-start" }, + { "label": "Migrating to V9", "to": "framework/preact/guide/migrating" } ] }, { - "label": "react", + "label": "octane", "children": [ - { - "label": "React Table Adapter", - "to": "framework/react/react-table" - } + { "label": "Quick Start", "to": "framework/octane/quick-start" } ] }, { "label": "solid", "children": [ - { - "label": "Solid Table Adapter", - "to": "framework/solid/solid-table" - } + { "label": "Quick Start", "to": "framework/solid/quick-start" }, + { "label": "Migrating to V9", "to": "framework/solid/guide/migrating" } ] }, { "label": "svelte", "children": [ - { - "label": "Svelte Table Adapter", - "to": "framework/svelte/svelte-table" - } + { "label": "Quick Start", "to": "framework/svelte/quick-start" }, + { "label": "Migrating to V9", "to": "framework/svelte/guide/migrating" } ] }, { "label": "vue", "children": [ - { - "label": "Vue Table Adapter", - "to": "framework/vue/vue-table" - } + { "label": "Quick Start", "to": "framework/vue/quick-start" }, + { "label": "Migrating to V9", "to": "framework/vue/guide/migrating" } ] }, { "label": "vanilla", "children": [ - { - "label": "Vanilla JS (No Framework)", - "to": "vanilla" - } + { "label": "Quick Start", "to": "framework/vanilla/quick-start" } ] } ] @@ -108,680 +94,1541 @@ { "label": "Core Guides", "children": [ + { "label": "Features", "to": "guide/features" }, + { "label": "Data", "to": "guide/data" }, + { "label": "Client-Side vs Server-Side", "to": "guide/client-side-vs-server-side" }, + { "label": "Column Definitions", "to": "guide/column-defs" }, + { "label": "Table Instance", "to": "guide/tables" }, + { "label": "Row Models", "to": "guide/row-models" }, + { "label": "Worker Row Models (Experimental)", "to": "guide/worker-row-models" }, + { "label": "Rows", "to": "guide/rows" }, + { "label": "Cells", "to": "guide/cells" }, + { "label": "Header Groups", "to": "guide/header-groups" }, + { "label": "Headers", "to": "guide/headers" }, + { "label": "Columns", "to": "guide/columns" }, + { "label": "Table and Column Meta", "to": "guide/table-and-column-meta" }, + { "label": "Type Helpers", "to": "guide/helpers" } + ], + "frameworks": [ + { + "label": "alpine", + "children": [ + { + "label": "Table State", + "to": "framework/alpine/guide/table-state" + }, + { + "label": "Composable Tables (createTableHook)", + "to": "framework/alpine/guide/composable-tables" + }, + { "label": "FlexRender", "to": "framework/alpine/guide/flex-render" }, + { + "label": "Custom Plugins", + "to": "framework/alpine/guide/custom-features" + } + ] + }, + { + "label": "angular", + "children": [ + { "label": "Table State", "to": "framework/angular/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/angular/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/angular/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/angular/guide/custom-features" } + ] + }, { - "label": "Data", - "to": "guide/data" + "label": "ember", + "children": [ + { "label": "Table State", "to": "framework/ember/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/ember/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/ember/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/ember/guide/custom-features" } + ] }, { - "label": "Column Defs", - "to": "guide/column-defs" + "label": "lit", + "children": [ + { "label": "Table State", "to": "framework/lit/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/lit/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/lit/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/lit/guide/custom-features" } + ] }, { - "label": "Table Instance", - "to": "guide/tables" + "label": "react", + "children": [ + { "label": "Table State", "to": "framework/react/guide/table-state" }, + { "label": "React Compiler", "to": "framework/react/guide/react-compiler" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/react/guide/composable-tables" }, + { "label": "Table Context", "to": "framework/react/guide/table-context" }, + { "label": "FlexRender", "to": "framework/react/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/react/guide/custom-features" } + ] }, { - "label": "Row Models", - "to": "guide/row-models" + "label": "preact", + "children": [ + { "label": "Table State", "to": "framework/preact/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/preact/guide/composable-tables" }, + { "label": "Table Context", "to": "framework/preact/guide/table-context" }, + { "label": "FlexRender", "to": "framework/preact/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/preact/guide/custom-features" } + ] }, { - "label": "Rows", - "to": "guide/rows" + "label": "octane", + "children": [ + { "label": "Table State", "to": "framework/octane/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/octane/guide/composable-tables" }, + { "label": "Table Context", "to": "framework/octane/guide/table-context" }, + { "label": "FlexRender", "to": "framework/octane/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/octane/guide/custom-features" } + ] }, { - "label": "Cells", - "to": "guide/cells" + "label": "solid", + "children": [ + { "label": "Table State", "to": "framework/solid/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/solid/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/solid/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/solid/guide/custom-features" } + ] }, { - "label": "Header Groups", - "to": "guide/header-groups" + "label": "svelte", + "children": [ + { "label": "Table State", "to": "framework/svelte/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/svelte/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/svelte/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/svelte/guide/custom-features" } + ] }, { - "label": "Headers", - "to": "guide/headers" + "label": "vue", + "children": [ + { "label": "Table State", "to": "framework/vue/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/vue/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/vue/guide/flex-render" }, + { "label": "Custom Plugins", "to": "framework/vue/guide/custom-features" } + ] }, { - "label": "Columns", - "to": "guide/columns" + "label": "vanilla", + "children": [ + { "label": "Table State", "to": "framework/vanilla/guide/table-state" }, + { "label": "FlexRender", "to": "framework/vanilla/guide/flex-render" } + ] } - ], + ] + }, + { + "label": "Feature Guides", + "children": [], "frameworks": [ + { + "label": "alpine", + "children": [ + { "label": "Cell Selection", "to": "framework/alpine/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/alpine/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/alpine/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/alpine/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/alpine/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/alpine/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/alpine/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/alpine/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/alpine/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/alpine/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/alpine/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/alpine/guide/aggregation" }, + { "label": "Grouping", "to": "framework/alpine/guide/grouping" }, + { "label": "Expanding", "to": "framework/alpine/guide/expanding" }, + { "label": "Pagination", "to": "framework/alpine/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/alpine/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/alpine/guide/row-selection" }, + { "label": "Sorting", "to": "framework/alpine/guide/sorting" } + ] + }, { "label": "angular", "children": [ - { - "label": "Table State", - "to": "framework/angular/guide/table-state" - } + { "label": "Cell Selection", "to": "framework/angular/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/angular/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/angular/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/angular/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/angular/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/angular/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/angular/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/angular/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/angular/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/angular/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/angular/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/angular/guide/aggregation" }, + { "label": "Grouping", "to": "framework/angular/guide/grouping" }, + { "label": "Expanding", "to": "framework/angular/guide/expanding" }, + { "label": "Pagination", "to": "framework/angular/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/angular/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/angular/guide/row-selection" }, + { "label": "Sorting", "to": "framework/angular/guide/sorting" }, + { "label": "Virtualization", "to": "framework/angular/guide/virtualization" } ] }, { - "label": "lit", + "label": "ember", "children": [ - { - "label": "Table State", - "to": "framework/lit/guide/table-state" - } + { "label": "Cell Selection", "to": "framework/ember/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/ember/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/ember/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/ember/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/ember/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/ember/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/ember/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/ember/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/ember/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/ember/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/ember/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/ember/guide/aggregation" }, + { "label": "Grouping", "to": "framework/ember/guide/grouping" }, + { "label": "Expanding", "to": "framework/ember/guide/expanding" }, + { "label": "Pagination", "to": "framework/ember/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/ember/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/ember/guide/row-selection" }, + { "label": "Sorting", "to": "framework/ember/guide/sorting" } ] }, { - "label": "qwik", + "label": "lit", "children": [ - { - "label": "Table State", - "to": "framework/qwik/guide/table-state" - } + { "label": "Cell Selection", "to": "framework/lit/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/lit/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/lit/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/lit/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/lit/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/lit/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/lit/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/lit/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/lit/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/lit/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/lit/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/lit/guide/aggregation" }, + { "label": "Grouping", "to": "framework/lit/guide/grouping" }, + { "label": "Expanding", "to": "framework/lit/guide/expanding" }, + { "label": "Pagination", "to": "framework/lit/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/lit/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/lit/guide/row-selection" }, + { "label": "Sorting", "to": "framework/lit/guide/sorting" }, + { "label": "Virtualization", "to": "framework/lit/guide/virtualization" } ] }, { "label": "react", "children": [ - { - "label": "Table State", - "to": "framework/react/guide/table-state" - } + { "label": "Cell Selection", "to": "framework/react/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/react/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/react/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/react/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/react/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/react/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/react/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/react/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/react/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/react/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/react/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/react/guide/aggregation" }, + { "label": "Grouping", "to": "framework/react/guide/grouping" }, + { "label": "Expanding", "to": "framework/react/guide/expanding" }, + { "label": "Pagination", "to": "framework/react/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/react/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/react/guide/row-selection" }, + { "label": "Sorting", "to": "framework/react/guide/sorting" }, + { "label": "Virtualization", "to": "framework/react/guide/virtualization" } + ] + }, + { + "label": "preact", + "children": [ + { "label": "Cell Selection", "to": "framework/preact/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/preact/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/preact/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/preact/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/preact/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/preact/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/preact/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/preact/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/preact/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/preact/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/preact/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/preact/guide/aggregation" }, + { "label": "Grouping", "to": "framework/preact/guide/grouping" }, + { "label": "Expanding", "to": "framework/preact/guide/expanding" }, + { "label": "Pagination", "to": "framework/preact/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/preact/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/preact/guide/row-selection" }, + { "label": "Sorting", "to": "framework/preact/guide/sorting" }, + { "label": "Virtualization", "to": "framework/preact/guide/virtualization" } + ] + }, + { + "label": "octane", + "children": [ + { "label": "Cell Selection", "to": "framework/octane/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/octane/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/octane/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/octane/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/octane/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/octane/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/octane/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/octane/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/octane/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/octane/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/octane/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/octane/guide/aggregation" }, + { "label": "Grouping", "to": "framework/octane/guide/grouping" }, + { "label": "Expanding", "to": "framework/octane/guide/expanding" }, + { "label": "Pagination", "to": "framework/octane/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/octane/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/octane/guide/row-selection" }, + { "label": "Sorting", "to": "framework/octane/guide/sorting" } ] }, { "label": "solid", "children": [ - { - "label": "Table State", - "to": "framework/solid/guide/table-state" - } + { "label": "Cell Selection", "to": "framework/solid/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/solid/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/solid/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/solid/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/solid/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/solid/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/solid/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/solid/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/solid/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/solid/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/solid/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/solid/guide/aggregation" }, + { "label": "Grouping", "to": "framework/solid/guide/grouping" }, + { "label": "Expanding", "to": "framework/solid/guide/expanding" }, + { "label": "Pagination", "to": "framework/solid/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/solid/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/solid/guide/row-selection" }, + { "label": "Sorting", "to": "framework/solid/guide/sorting" }, + { "label": "Virtualization", "to": "framework/solid/guide/virtualization" } ] }, { "label": "svelte", "children": [ - { - "label": "Table State", - "to": "framework/svelte/guide/table-state" - } + { "label": "Cell Selection", "to": "framework/svelte/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/svelte/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/svelte/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/svelte/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/svelte/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/svelte/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/svelte/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/svelte/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/svelte/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/svelte/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/svelte/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/svelte/guide/aggregation" }, + { "label": "Grouping", "to": "framework/svelte/guide/grouping" }, + { "label": "Expanding", "to": "framework/svelte/guide/expanding" }, + { "label": "Pagination", "to": "framework/svelte/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/svelte/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/svelte/guide/row-selection" }, + { "label": "Sorting", "to": "framework/svelte/guide/sorting" }, + { "label": "Virtualization", "to": "framework/svelte/guide/virtualization" } ] }, { "label": "vue", "children": [ - { - "label": "Table State", - "to": "framework/vue/guide/table-state" - } + { "label": "Cell Selection", "to": "framework/vue/guide/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/vue/guide/cell-spanning" }, + { "label": "Column Ordering", "to": "framework/vue/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/vue/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/vue/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/vue/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/vue/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/vue/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/vue/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/vue/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/vue/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/vue/guide/aggregation" }, + { "label": "Grouping", "to": "framework/vue/guide/grouping" }, + { "label": "Expanding", "to": "framework/vue/guide/expanding" }, + { "label": "Pagination", "to": "framework/vue/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/vue/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/vue/guide/row-selection" }, + { "label": "Sorting", "to": "framework/vue/guide/sorting" }, + { "label": "Virtualization", "to": "framework/vue/guide/virtualization" } ] }, { "label": "vanilla", "children": [ - { - "label": "Table State", - "to": "framework/vanilla/guide/table-state" - } + { "label": "Aggregation", "to": "framework/vanilla/guide/aggregation" } ] } ] }, { - "label": "Feature Guides", + "label": "API Reference", "children": [ + { "label": "Core API Reference", "to": "reference/index" } + ], + "frameworks": [ { - "label": "Column Ordering", - "to": "guide/column-ordering" - }, - { - "label": "Column Pinning", - "to": "guide/column-pinning" - }, - { - "label": "Column Sizing", - "to": "guide/column-sizing" - }, - { - "label": "Column Visibility", - "to": "guide/column-visibility" - }, - { - "label": "Column Filtering", - "to": "guide/column-filtering" - }, - { - "label": "Global Filtering", - "to": "guide/global-filtering" - }, - { - "label": "Fuzzy Filtering", - "to": "guide/fuzzy-filtering" - }, - { - "label": "Column Faceting", - "to": "guide/column-faceting" + "label": "alpine", + "children": [ + { "label": "Alpine API Reference", "to": "framework/alpine/reference/index" } + ] }, { - "label": "Global Faceting", - "to": "guide/global-faceting" + "label": "angular", + "children": [ + { "label": "Angular API Reference", "to": "framework/angular/reference/index" } + ] }, { - "label": "Grouping", - "to": "guide/grouping" + "label": "ember", + "children": [ + { "label": "Ember API Reference", "to": "framework/ember/reference/index" } + ] }, { - "label": "Expanding", - "to": "guide/expanding" + "label": "react", + "children": [ + { "label": "React API Reference", "to": "framework/react/reference/index" } + ] }, { - "label": "Pagination", - "to": "guide/pagination" + "label": "preact", + "children": [ + { "label": "Preact API Reference", "to": "framework/preact/reference/index" } + ] }, { - "label": "Row Pinning", - "to": "guide/row-pinning" + "label": "octane", + "children": [ + { "label": "Octane API Reference", "to": "framework/octane/reference/index" } + ] }, { - "label": "Row Selection", - "to": "guide/row-selection" + "label": "solid", + "children": [ + { "label": "Solid API Reference", "to": "framework/solid/reference/index" } + ] }, { - "label": "Sorting", - "to": "guide/sorting" + "label": "svelte", + "children": [ + { "label": "Svelte API Reference", "to": "framework/svelte/reference/index" } + ] }, { - "label": "Virtualization", - "to": "guide/virtualization" + "label": "vue", + "children": [ + { "label": "Vue API Reference", "to": "framework/vue/reference/index" } + ] }, { - "label": "Custom Features", - "to": "guide/custom-features" + "label": "lit", + "children": [ + { "label": "Lit API Reference", "to": "framework/lit/reference/index" } + ] } ] }, { - "label": "Core APIs", + "collapsible": true, + "defaultCollapsed": true, + "label": "Table API Reference", "children": [ + { "label": "Table", "to": "reference/index/type-aliases/Table" }, + { "label": "TableOptions", "to": "reference/index/type-aliases/TableOptions" }, + { "label": "TableState", "to": "reference/index/type-aliases/TableState" }, + { "label": "TableMeta", "to": "reference/index/interfaces/TableMeta" }, + { "label": "TableFeature", "to": "reference/index/interfaces/TableFeature" }, + { "label": "TableFeatures", "to": "reference/index/interfaces/TableFeatures" }, + { "label": "StockFeatures", "to": "reference/index/interfaces/StockFeatures" }, + { "label": "CoreFeatures", "to": "reference/index/interfaces/CoreFeatures" }, + { "label": "BaseAtoms", "to": "reference/index/type-aliases/BaseAtoms" }, + { "label": "Atoms", "to": "reference/index/type-aliases/Atoms" }, + { "label": "ExternalAtoms", "to": "reference/index/type-aliases/ExternalAtoms" }, + { "label": "constructTable", "to": "reference/index/functions/constructTable" }, + { "label": "tableOptions", "to": "reference/index/functions/tableOptions" }, + { "label": "tableFeatures", "to": "reference/index/functions/tableFeatures" }, + { "label": "getInitialTableState", "to": "reference/index/functions/getInitialTableState" }, + { "label": "OnChangeFn", "to": "reference/index/type-aliases/OnChangeFn" }, + { "label": "Updater", "to": "reference/index/type-aliases/Updater" }, + { "label": "DebugOptions", "to": "reference/index/type-aliases/DebugOptions" } + ], + "frameworks": [ { - "label": "Column Def", - "to": "api/core/column-def" + "label": "alpine", + "children": [ + { "label": "createTable", "to": "framework/alpine/reference/functions/createTable" }, + { "label": "createTableHook", "to": "framework/alpine/reference/functions/createTableHook" }, + { "label": "AlpineTable", "to": "framework/alpine/reference/type-aliases/AlpineTable" }, + { "label": "AppAlpineTable", "to": "framework/alpine/reference/type-aliases/AppAlpineTable" }, + { "label": "CreateTableHookOptions", "to": "framework/alpine/reference/type-aliases/CreateTableHookOptions" }, + { "label": "FlexRender", "to": "framework/alpine/reference/functions/FlexRender-1" }, + { "label": "flexRender", "to": "framework/alpine/reference/functions/flexRender" } + ] }, { - "label": "Table", - "to": "api/core/table" + "label": "react", + "children": [ + { "label": "useTable", "to": "framework/react/reference/index/functions/useTable" }, + { "label": "createTableHook", "to": "framework/react/reference/index/functions/createTableHook" }, + { "label": "ReactTable", "to": "framework/react/reference/index/type-aliases/ReactTable" }, + { "label": "AppReactTable", "to": "framework/react/reference/index/type-aliases/AppReactTable" }, + { "label": "CreateTableHookOptions", "to": "framework/react/reference/index/type-aliases/CreateTableHookOptions" }, + { "label": "Subscribe", "to": "framework/react/reference/index/functions/Subscribe" }, + { "label": "FlexRender", "to": "framework/react/reference/index/functions/FlexRender-1" }, + { "label": "flexRender", "to": "framework/react/reference/index/functions/flexRender" } + ] }, { - "label": "Column", - "to": "api/core/column" + "label": "angular", + "children": [ + { "label": "injectTable", "to": "framework/angular/reference/functions/injectTable" }, + { "label": "createTableHook", "to": "framework/angular/reference/functions/createTableHook" }, + { "label": "AngularTable", "to": "framework/angular/reference/type-aliases/AngularTable" }, + { "label": "AppAngularTable", "to": "framework/angular/reference/type-aliases/AppAngularTable" }, + { "label": "CreateTableHookResult", "to": "framework/angular/reference/type-aliases/CreateTableHookResult" }, + { "label": "CreateTableContextOptions", "to": "framework/angular/reference/type-aliases/CreateTableContextOptions" }, + { "label": "flexRenderComponent", "to": "framework/angular/reference/functions/flexRenderComponent" }, + { "label": "FlexRender", "to": "framework/angular/reference/variables/FlexRender" }, + { "label": "TanStackTable", "to": "framework/angular/reference/classes/TanStackTable" }, + { "label": "injectTableContext", "to": "framework/angular/reference/functions/injectTableContext" } + ] }, { - "label": "Header Group", - "to": "api/core/header-group" + "label": "ember", + "children": [ + { "label": "useTable", "to": "framework/ember/reference/functions/useTable" }, + { "label": "createTableHook", "to": "framework/ember/reference/functions/createTableHook" }, + { "label": "AppEmberTable", "to": "framework/ember/reference/type-aliases/AppEmberTable" }, + { "label": "CreateTableHookOptions", "to": "framework/ember/reference/type-aliases/CreateTableHookOptions" }, + { "label": "FlexRenderCell", "to": "framework/ember/reference/classes/FlexRenderCell" }, + { "label": "FlexRenderHeader", "to": "framework/ember/reference/classes/FlexRenderHeader" }, + { "label": "FlexRenderFooter", "to": "framework/ember/reference/classes/FlexRenderFooter" }, + { "label": "flexRenderComponent", "to": "framework/ember/reference/functions/flexRenderComponent" }, + { "label": "FlexRenderableSignature", "to": "framework/ember/reference/interfaces/FlexRenderableSignature" }, + { "label": "CellRenderableSignature", "to": "framework/ember/reference/interfaces/CellRenderableSignature" }, + { "label": "emberReactivity", "to": "framework/ember/reference/functions/emberReactivity" }, + { "label": "createAtom", "to": "framework/ember/reference/functions/createAtom" }, + { "label": "signal", "to": "framework/ember/reference/functions/signal" }, + { "label": "computed", "to": "framework/ember/reference/functions/computed" } + ] }, { - "label": "Header", - "to": "api/core/header" + "label": "preact", + "children": [ + { "label": "useTable", "to": "framework/preact/reference/functions/useTable" }, + { "label": "createTableHook", "to": "framework/preact/reference/functions/createTableHook" }, + { "label": "PreactTable", "to": "framework/preact/reference/type-aliases/PreactTable" }, + { "label": "AppPreactTable", "to": "framework/preact/reference/type-aliases/AppPreactTable" }, + { "label": "CreateTableHookOptions", "to": "framework/preact/reference/type-aliases/CreateTableHookOptions" }, + { "label": "Subscribe", "to": "framework/preact/reference/functions/Subscribe" }, + { "label": "FlexRender", "to": "framework/preact/reference/functions/FlexRender-1" }, + { "label": "flexRender", "to": "framework/preact/reference/functions/flexRender" } + ] }, { - "label": "Row", - "to": "api/core/row" + "label": "octane", + "children": [ + { "label": "useTable", "to": "framework/octane/reference/functions/useTable" }, + { "label": "createTableHook", "to": "framework/octane/reference/functions/createTableHook" }, + { "label": "createTableHookContexts", "to": "framework/octane/reference/functions/createTableHookContexts" }, + { "label": "OctaneTable", "to": "framework/octane/reference/type-aliases/OctaneTable" }, + { "label": "AppOctaneTable", "to": "framework/octane/reference/type-aliases/AppOctaneTable" }, + { "label": "CreateTableHookOptions", "to": "framework/octane/reference/type-aliases/CreateTableHookOptions" }, + { "label": "Subscribe", "to": "framework/octane/reference/variables/Subscribe" }, + { "label": "FlexRender", "to": "framework/octane/reference/functions/FlexRender-1" }, + { "label": "flexRender", "to": "framework/octane/reference/functions/flexRender" } + ] }, { - "label": "Cell", - "to": "api/core/cell" - } - ] - }, - { - "label": "Feature APIs", - "children": [ - { - "label": "Column Filtering", - "to": "api/features/column-filtering" + "label": "solid", + "children": [ + { "label": "createTable", "to": "framework/solid/reference/functions/createTable" }, + { "label": "createTableHook", "to": "framework/solid/reference/functions/createTableHook" }, + { "label": "SolidTable", "to": "framework/solid/reference/type-aliases/SolidTable" }, + { "label": "AppSolidTable", "to": "framework/solid/reference/type-aliases/AppSolidTable" }, + { "label": "CreateTableHookOptions", "to": "framework/solid/reference/type-aliases/CreateTableHookOptions" }, + { "label": "FlexRender", "to": "framework/solid/reference/functions/FlexRender-1" }, + { "label": "flexRender", "to": "framework/solid/reference/functions/flexRender" } + ] }, { - "label": "Column Faceting", - "to": "api/features/column-faceting" + "label": "svelte", + "children": [ + { "label": "createTable", "to": "framework/svelte/reference/functions/createTable" }, + { "label": "createTableHook", "to": "framework/svelte/reference/functions/createTableHook" }, + { "label": "createTableState", "to": "framework/svelte/reference/functions/createTableState" }, + { "label": "SvelteTable", "to": "framework/svelte/reference/type-aliases/SvelteTable" }, + { "label": "AppSvelteTable", "to": "framework/svelte/reference/type-aliases/AppSvelteTable" }, + { "label": "renderComponent", "to": "framework/svelte/reference/functions/renderComponent" }, + { "label": "renderSnippet", "to": "framework/svelte/reference/functions/renderSnippet" }, + { "label": "FlexRender", "to": "framework/svelte/reference/variables/FlexRender" } + ] }, { - "label": "Column Ordering", - "to": "api/features/column-ordering" + "label": "vue", + "children": [ + { "label": "useTable", "to": "framework/vue/reference/functions/useTable" }, + { "label": "createTableHook", "to": "framework/vue/reference/functions/createTableHook" }, + { "label": "VueTable", "to": "framework/vue/reference/type-aliases/VueTable" }, + { "label": "AppVueTable", "to": "framework/vue/reference/type-aliases/AppVueTable" }, + { "label": "CreateTableHookOptions", "to": "framework/vue/reference/type-aliases/CreateTableHookOptions" }, + { "label": "FlexRender", "to": "framework/vue/reference/variables/FlexRender" }, + { "label": "flexRender", "to": "framework/vue/reference/functions/flexRender" } + ] }, { - "label": "Column Pinning", - "to": "api/features/column-pinning" - }, + "label": "lit", + "children": [ + { "label": "TableController", "to": "framework/lit/reference/classes/TableController" }, + { "label": "createTableHook", "to": "framework/lit/reference/functions/createTableHook" }, + { "label": "LitTable", "to": "framework/lit/reference/type-aliases/LitTable" }, + { "label": "AppLitTable", "to": "framework/lit/reference/type-aliases/AppLitTable" }, + { "label": "CreateTableHookOptions", "to": "framework/lit/reference/type-aliases/CreateTableHookOptions" }, + { "label": "subscribe", "to": "framework/lit/reference/variables/subscribe" }, + { "label": "SubscribeDirective", "to": "framework/lit/reference/classes/SubscribeDirective" }, + { "label": "SelectionSource", "to": "framework/lit/reference/type-aliases/SelectionSource" }, + { "label": "FlexRender", "to": "framework/lit/reference/functions/FlexRender-1" }, + { "label": "flexRender", "to": "framework/lit/reference/functions/flexRender" } + ] + } + ] + }, + { + "collapsible": true, + "defaultCollapsed": true, + "label": "Column API Reference", + "children": [ + { "label": "Column", "to": "reference/index/type-aliases/Column" }, + { "label": "ColumnDef", "to": "reference/index/type-aliases/ColumnDef" }, + { "label": "ColumnDefBase", "to": "reference/index/type-aliases/ColumnDefBase" }, + { "label": "AccessorColumnDef", "to": "reference/index/type-aliases/AccessorColumnDef" }, + { "label": "DisplayColumnDef", "to": "reference/index/type-aliases/DisplayColumnDef" }, + { "label": "GroupColumnDef", "to": "reference/index/type-aliases/GroupColumnDef" }, + { "label": "ColumnDefTemplate", "to": "reference/index/type-aliases/ColumnDefTemplate" }, + { "label": "ColumnHelper", "to": "reference/index/type-aliases/ColumnHelper" }, + { "label": "ColumnMeta", "to": "reference/index/interfaces/ColumnMeta" }, + { "label": "createColumnHelper", "to": "reference/index/functions/createColumnHelper" }, + { "label": "constructColumn", "to": "reference/index/functions/constructColumn" }, + { "label": "AccessorFn", "to": "reference/index/type-aliases/AccessorFn" }, + { "label": "DeepKeys", "to": "reference/index/type-aliases/DeepKeys" }, + { "label": "DeepValue", "to": "reference/index/type-aliases/DeepValue" } + ], + "frameworks": [ { - "label": "Column Sizing", - "to": "api/features/column-sizing" + "label": "alpine", + "children": [ + { "label": "AppColumnHelper", "to": "framework/alpine/reference/type-aliases/AppColumnHelper" } + ] }, { - "label": "Column Visibility", - "to": "api/features/column-visibility" + "label": "react", + "children": [ + { "label": "AppColumnHelper", "to": "framework/react/reference/index/type-aliases/AppColumnHelper" } + ] }, { - "label": "Global Faceting", - "to": "api/features/global-faceting" + "label": "angular", + "children": [ + { "label": "AppColumnHelper", "to": "framework/angular/reference/type-aliases/AppColumnHelper" } + ] }, { - "label": "Global Filtering", - "to": "api/features/global-filtering" + "label": "ember", + "children": [ + { "label": "AppColumnHelper", "to": "framework/ember/reference/type-aliases/AppColumnHelper" } + ] }, { - "label": "Sorting", - "to": "api/features/sorting" + "label": "preact", + "children": [ + { "label": "AppColumnHelper", "to": "framework/preact/reference/type-aliases/AppColumnHelper" } + ] }, { - "label": "Grouping", - "to": "api/features/grouping" + "label": "octane", + "children": [ + { "label": "AppColumnHelper", "to": "framework/octane/reference/interfaces/AppColumnHelper" } + ] }, { - "label": "Expanding", - "to": "api/features/expanding" + "label": "solid", + "children": [ + { "label": "AppColumnHelper", "to": "framework/solid/reference/type-aliases/AppColumnHelper" } + ] }, { - "label": "Pagination", - "to": "api/features/pagination" + "label": "svelte", + "children": [ + { "label": "AppColumnHelper", "to": "framework/svelte/reference/type-aliases/AppColumnHelper" } + ] }, { - "label": "Row Pinning", - "to": "api/features/row-pinning" + "label": "vue", + "children": [ + { "label": "AppColumnHelper", "to": "framework/vue/reference/type-aliases/AppColumnHelper" } + ] }, { - "label": "Row Selection", - "to": "api/features/row-selection" + "label": "lit", + "children": [ + { "label": "AppColumnHelper", "to": "framework/lit/reference/type-aliases/AppColumnHelper" } + ] } ] }, { - "label": "Enterprise", + "collapsible": true, + "defaultCollapsed": true, + "label": "Row API Reference", "children": [ - { - "label": "AG Grid", - "to": "enterprise/ag-grid" - } + { "label": "Row", "to": "reference/index/type-aliases/Row" }, + { "label": "RowData", "to": "reference/index/type-aliases/RowData" }, + { "label": "RowModel", "to": "reference/index/interfaces/RowModel" }, + { "label": "constructRow", "to": "reference/index/functions/constructRow" }, + { "label": "createCoreRowModel", "to": "reference/index/functions/createCoreRowModel" }, + { "label": "createFilteredRowModel", "to": "reference/index/functions/createFilteredRowModel" }, + { "label": "createSortedRowModel", "to": "reference/index/functions/createSortedRowModel" }, + { "label": "createGroupedRowModel", "to": "reference/index/functions/createGroupedRowModel" }, + { "label": "createExpandedRowModel", "to": "reference/index/functions/createExpandedRowModel" }, + { "label": "createPaginatedRowModel", "to": "reference/index/functions/createPaginatedRowModel" }, + { "label": "createFacetedRowModel", "to": "reference/index/functions/createFacetedRowModel" }, + { "label": "createFacetedMinMaxValues", "to": "reference/index/functions/createFacetedMinMaxValues" }, + { "label": "createFacetedUniqueValues", "to": "reference/index/functions/createFacetedUniqueValues" }, + { "label": "expandRows", "to": "reference/index/functions/expandRows" }, + { "label": "RowModelFns", "to": "reference/index/type-aliases/RowModelFns" }, + { "label": "CreateRowModels", "to": "reference/index/type-aliases/CreateRowModels" }, + { "label": "CachedRowModels", "to": "reference/index/type-aliases/CachedRowModels" } ] }, { - "label": "Examples", - "children": [], + "collapsible": true, + "defaultCollapsed": true, + "label": "Cell API Reference", + "children": [ + { "label": "Cell", "to": "reference/index/type-aliases/Cell" }, + { "label": "CellContext", "to": "reference/index/interfaces/CellContext" }, + { "label": "CellData", "to": "reference/index/type-aliases/CellData" }, + { "label": "constructCell", "to": "reference/index/functions/constructCell" } + ], "frameworks": [ + { + "label": "react", + "children": [ + { "label": "AppCellContext", "to": "framework/react/reference/index/type-aliases/AppCellContext" } + ] + }, { "label": "angular", "children": [ - { - "to": "framework/angular/examples/basic", - "label": "Basic" - }, - { - "to": "framework/angular/examples/grouping", - "label": "Column Grouping" - }, - { - "to": "framework/angular/examples/column-ordering", - "label": "Column Ordering" - }, - { - "to": "framework/angular/examples/column-pinning", - "label": "Column Pinning" - }, - { - "to": "framework/angular/examples/column-pinning-sticky", - "label": "Sticky Column Pinning" - }, - { - "to": "framework/angular/examples/column-visibility", - "label": "Column Visibility" - }, - { - "to": "framework/angular/examples/filters", - "label": "Column Filters" - }, - { - "to": "framework/angular/examples/row-selection", - "label": "Row Selection" - }, - { - "to": "framework/angular/examples/expanding", - "label": "Expanding" - }, - { - "to": "framework/angular/examples/sub-components", - "label": "Sub Components" - }, - { - "to": "framework/angular/examples/signal-input", - "label": "Signal Input" - }, - { - "to": "framework/angular/examples/editable", - "label": "Editable data" - }, - { - "to": "framework/angular/examples/row-dnd", - "label": "Row DnD" - }, - { - "to": "framework/angular/examples/column-resizing-performant", - "label": "Performant Column Resizing" - } + { "label": "AppCellContext", "to": "framework/angular/reference/type-aliases/AppCellContext" } + ] + }, + { + "label": "preact", + "children": [ + { "label": "AppCellContext", "to": "framework/preact/reference/type-aliases/AppCellContext" } + ] + }, + { + "label": "octane", + "children": [ + { "label": "AppCellContext", "to": "framework/octane/reference/interfaces/AppCellContext" } + ] + }, + { + "label": "solid", + "children": [ + { "label": "AppCellContext", "to": "framework/solid/reference/type-aliases/AppCellContext" } + ] + }, + { + "label": "svelte", + "children": [ + { "label": "AppCellContext", "to": "framework/svelte/reference/type-aliases/AppCellContext" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "AppCellContext", "to": "framework/vue/reference/type-aliases/AppCellContext" } ] }, { "label": "lit", "children": [ - { - "to": "framework/lit/examples/basic", - "label": "Basic" - }, - { - "to": "framework/lit/examples/column-sizing", - "label": "Column Sizing" - }, - { - "to": "framework/lit/examples/filters", - "label": "Filters" - }, - { - "to": "framework/lit/examples/row-selection", - "label": "Row Selection" - }, - { - "to": "framework/lit/examples/sorting", - "label": "Sorting" - }, - { - "to": "framework/lit/examples/virtualized-rows", - "label": "Virtualized Rows" - } + { "label": "AppCellContext", "to": "framework/lit/reference/type-aliases/AppCellContext" } + ] + } + ] + }, + { + "collapsible": true, + "defaultCollapsed": true, + "label": "Header API Reference", + "children": [ + { "label": "Header", "to": "reference/index/type-aliases/Header" }, + { "label": "HeaderGroup", "to": "reference/index/type-aliases/HeaderGroup" }, + { "label": "HeaderContext", "to": "reference/index/interfaces/HeaderContext" }, + { "label": "constructHeader", "to": "reference/index/functions/constructHeader" }, + { "label": "buildHeaderGroups", "to": "reference/index/functions/buildHeaderGroups" } + ], + "frameworks": [ + { + "label": "react", + "children": [ + { "label": "AppHeaderContext", "to": "framework/react/reference/index/type-aliases/AppHeaderContext" } ] }, { - "label": "qwik", + "label": "angular", "children": [ - { - "to": "framework/qwik/examples/basic", - "label": "Basic" - }, - { - "to": "framework/qwik/examples/filters", - "label": "Filters" - }, - { - "to": "framework/qwik/examples/row-selection", - "label": "Row Selection" - }, - { - "to": "framework/qwik/examples/sorting", - "label": "Sorting" - } + { "label": "AppHeaderContext", "to": "framework/angular/reference/type-aliases/AppHeaderContext" } + ] + }, + { + "label": "preact", + "children": [ + { "label": "AppHeaderContext", "to": "framework/preact/reference/type-aliases/AppHeaderContext" } + ] + }, + { + "label": "octane", + "children": [ + { "label": "AppHeaderContext", "to": "framework/octane/reference/interfaces/AppHeaderContext" } + ] + }, + { + "label": "solid", + "children": [ + { "label": "AppHeaderContext", "to": "framework/solid/reference/type-aliases/AppHeaderContext" } + ] + }, + { + "label": "svelte", + "children": [ + { "label": "AppHeaderContext", "to": "framework/svelte/reference/type-aliases/AppHeaderContext" } ] }, + { + "label": "vue", + "children": [ + { "label": "AppHeaderContext", "to": "framework/vue/reference/type-aliases/AppHeaderContext" } + ] + }, + { + "label": "lit", + "children": [ + { "label": "AppHeaderContext", "to": "framework/lit/reference/type-aliases/AppHeaderContext" } + ] + } + ] + }, + { + "collapsible": true, + "defaultCollapsed": true, + "label": "Features API Reference", + "children": [ + { "label": "stockFeatures", "to": "reference/index/variables/stockFeatures" }, + { "label": "coreFeatures", "to": "reference/index/variables/coreFeatures" }, + { "label": "coreCellsFeature", "to": "reference/index/variables/coreCellsFeature" }, + { "label": "coreColumnsFeature", "to": "reference/index/variables/coreColumnsFeature" }, + { "label": "coreHeadersFeature", "to": "reference/index/variables/coreHeadersFeature" }, + { "label": "coreRowModelsFeature", "to": "reference/index/variables/coreRowModelsFeature" }, + { "label": "coreRowsFeature", "to": "reference/index/variables/coreRowsFeature" }, + { "label": "coreTablesFeature", "to": "reference/index/variables/coreTablesFeature" }, + { "label": "cellSpanningFeature", "to": "reference/index/variables/cellSpanningFeature" }, + { "label": "columnFilteringFeature", "to": "reference/index/variables/columnFilteringFeature" }, + { "label": "columnFacetingFeature", "to": "reference/index/variables/columnFacetingFeature" }, + { "label": "rowAggregationFeature", "to": "reference/index/variables/rowAggregationFeature" }, + { "label": "columnGroupingFeature", "to": "reference/index/variables/columnGroupingFeature" }, + { "label": "columnOrderingFeature", "to": "reference/index/variables/columnOrderingFeature" }, + { "label": "columnPinningFeature", "to": "reference/index/variables/columnPinningFeature" }, + { "label": "columnResizingFeature", "to": "reference/index/variables/columnResizingFeature" }, + { "label": "columnSizingFeature", "to": "reference/index/variables/columnSizingFeature" }, + { "label": "columnVisibilityFeature", "to": "reference/index/variables/columnVisibilityFeature" }, + { "label": "globalFilteringFeature", "to": "reference/index/variables/globalFilteringFeature" }, + { "label": "rowExpandingFeature", "to": "reference/index/variables/rowExpandingFeature" }, + { "label": "rowPaginationFeature", "to": "reference/index/variables/rowPaginationFeature" }, + { "label": "rowPinningFeature", "to": "reference/index/variables/rowPinningFeature" }, + { "label": "rowSelectionFeature", "to": "reference/index/variables/rowSelectionFeature" }, + { "label": "rowSortingFeature", "to": "reference/index/variables/rowSortingFeature" }, + { "label": "filterFns", "to": "reference/index/variables/filterFns" }, + { "label": "sortFns", "to": "reference/index/variables/sortFns" }, + { "label": "aggregationFns", "to": "reference/index/variables/aggregationFns" }, + { "label": "ColumnFilter", "to": "reference/index/interfaces/ColumnFilter" }, + { "label": "ColumnPinningState", "to": "reference/index/interfaces/ColumnPinningState" }, + { "label": "ColumnSort", "to": "reference/index/interfaces/ColumnSort" }, + { "label": "PaginationState", "to": "reference/index/interfaces/PaginationState" }, + { "label": "RowPinningState", "to": "reference/index/interfaces/RowPinningState" }, + { "label": "columnResizingState", "to": "reference/index/interfaces/columnResizingState" }, + { "label": "FilterFn", "to": "reference/index/interfaces/FilterFn" }, + { "label": "FilterFns", "to": "reference/index/interfaces/FilterFns" }, + { "label": "FilterMeta", "to": "reference/index/interfaces/FilterMeta" }, + { "label": "SortFn", "to": "reference/index/interfaces/SortFn" }, + { "label": "SortFns", "to": "reference/index/interfaces/SortFns" }, + { "label": "AggregationFns", "to": "reference/index/interfaces/AggregationFns" }, + { "label": "AggregationContext", "to": "reference/index/interfaces/AggregationContext" }, + { "label": "AggregationMergeContext", "to": "reference/index/interfaces/AggregationMergeContext" }, + { "label": "AggregationFnDef", "to": "reference/index/interfaces/AggregationFnDef" }, + { "label": "AggregationFnDescriptor", "to": "reference/index/interfaces/AggregationFnDescriptor" }, + { "label": "AggregationValueContext", "to": "reference/index/interfaces/AggregationValueContext" }, + { "label": "AggregationValueResult", "to": "reference/index/interfaces/AggregationValueResult" }, + { "label": "constructAggregationFn", "to": "reference/index/functions/constructAggregationFn" }, + { "label": "ColumnFiltersState", "to": "reference/index/type-aliases/ColumnFiltersState" }, + { "label": "ColumnOrderState", "to": "reference/index/type-aliases/ColumnOrderState" }, + { "label": "ColumnPinningPosition", "to": "reference/index/type-aliases/ColumnPinningPosition" }, + { "label": "ColumnResizeDirection", "to": "reference/index/type-aliases/ColumnResizeDirection" }, + { "label": "ColumnResizeMode", "to": "reference/index/type-aliases/ColumnResizeMode" }, + { "label": "ColumnSizingState", "to": "reference/index/type-aliases/ColumnSizingState" }, + { "label": "ColumnVisibilityState", "to": "reference/index/type-aliases/ColumnVisibilityState" }, + { "label": "ExpandedState", "to": "reference/index/type-aliases/ExpandedState" }, + { "label": "GroupingState", "to": "reference/index/type-aliases/GroupingState" }, + { "label": "RowPinningPosition", "to": "reference/index/type-aliases/RowPinningPosition" }, + { "label": "RowSelectionState", "to": "reference/index/type-aliases/RowSelectionState" }, + { "label": "SortDirection", "to": "reference/index/type-aliases/SortDirection" }, + { "label": "SortingState", "to": "reference/index/type-aliases/SortingState" }, + { "label": "AggregationFnOption", "to": "reference/index/type-aliases/AggregationFnOption" }, + { "label": "AggregationResult", "to": "reference/index/type-aliases/AggregationResult" }, + { "label": "FilterFnOption", "to": "reference/index/type-aliases/FilterFnOption" }, + { "label": "SortFnOption", "to": "reference/index/type-aliases/SortFnOption" }, + { "label": "BuiltInAggregationFn", "to": "reference/index/type-aliases/BuiltInAggregationFn" }, + { "label": "BuiltInFilterFn", "to": "reference/index/type-aliases/BuiltInFilterFn" }, + { "label": "BuiltInSortFn", "to": "reference/index/type-aliases/BuiltInSortFn" } + ] + }, + { + "collapsible": true, + "defaultCollapsed": true, + "label": "Static Functions API Reference", + "children": [ + { "label": "Static Functions Overview", "to": "reference/static-functions/index" } + ] + }, + { + "collapsible": true, + "defaultCollapsed": true, + "label": "Legacy API Reference", + "children": [], + "frameworks": [ { "label": "react", "children": [ - { - "to": "framework/react/examples/basic", - "label": "Basic" - }, - { - "to": "framework/react/examples/column-groups", - "label": "Header Groups" - }, - { - "to": "framework/react/examples/filters", - "label": "Column Filters" - }, - { - "to": "framework/react/examples/filters-faceted", - "label": "Column Filters (Faceted)" - }, - { - "to": "framework/react/examples/filters-fuzzy", - "label": "Fuzzy Search Filters" - }, - { - "to": "framework/react/examples/column-ordering", - "label": "Column Ordering" - }, - { - "to": "framework/react/examples/column-dnd", - "label": "Column Ordering (DnD)" - }, - { - "to": "framework/react/examples/column-pinning", - "label": "Column Pinning" - }, - { - "to": "framework/react/examples/column-pinning-sticky", - "label": "Sticky Column Pinning" - }, - { - "to": "framework/react/examples/column-sizing", - "label": "Column Sizing" - }, - { - "to": "framework/react/examples/column-resizing-performant", - "label": "Performant Column Resizing" - }, - { - "to": "framework/react/examples/column-visibility", - "label": "Column Visibility" - }, - { - "to": "framework/react/examples/editable-data", - "label": "Editable Data" - }, - { - "to": "framework/react/examples/expanding", - "label": "Expanding" - }, - { - "to": "framework/react/examples/sub-components", - "label": "Sub Components" - }, - { - "to": "framework/react/examples/fully-controlled", - "label": "Fully Controlled" - }, - { - "to": "framework/react/examples/grouping", - "label": "Grouping" - }, - { - "to": "framework/react/examples/pagination", - "label": "Pagination" - }, - { - "to": "framework/react/examples/pagination-controlled", - "label": "Pagination Controlled" - }, - { - "to": "framework/react/examples/row-dnd", - "label": "Row DnD" - }, - { - "to": "framework/react/examples/row-pinning", - "label": "Row Pinning" - }, - { - "to": "framework/react/examples/row-selection", - "label": "Row Selection" - }, - { - "to": "framework/react/examples/sorting", - "label": "Sorting" - }, - { - "to": "framework/react/examples/virtualized-columns", - "label": "Virtualized Columns" - }, - { - "to": "framework/react/examples/virtualized-columns-experimental", - "label": "Virtualized Columns (Experimental)" - }, - { - "to": "framework/react/examples/virtualized-rows", - "label": "Virtualized Rows" - }, - { - "to": "framework/react/examples/virtualized-rows-experimental", - "label": "Virtualized Rows (Experimental)" - }, - { - "to": "framework/react/examples/virtualized-infinite-scrolling", - "label": "Virtualized Infinite Scrolling" - }, - { - "to": "framework/react/examples/kitchen-sink", - "label": "Kitchen Sink" - }, - { - "to": "framework/react/examples/bootstrap", - "label": "React Bootstrap" - }, - { - "to": "framework/react/examples/material-ui-pagination", - "label": "Material UI Pagination" - }, - { - "to": "framework/react/examples/full-width-table", - "label": "React Full Width" - }, - { - "to": "framework/react/examples/full-width-resizable-table", - "label": "React Full Width Resizable" - }, - { - "to": "framework/react/examples/custom-features", - "label": "Custom Features" - }, - { - "to": "framework/react/examples/query-router-search-params", - "label": "Query Router Search Params" - } + { "label": "Legacy API Overview", "to": "framework/react/reference/legacy/index" }, + { "label": "useLegacyTable", "to": "framework/react/reference/legacy/functions/useLegacyTable" }, + { "label": "legacyCreateColumnHelper", "to": "framework/react/reference/legacy/functions/legacyCreateColumnHelper" }, + { "label": "getCoreRowModel", "to": "framework/react/reference/legacy/functions/getCoreRowModel" }, + { "label": "getFilteredRowModel", "to": "framework/react/reference/legacy/functions/getFilteredRowModel" }, + { "label": "getSortedRowModel", "to": "framework/react/reference/legacy/functions/getSortedRowModel" }, + { "label": "getGroupedRowModel", "to": "framework/react/reference/legacy/functions/getGroupedRowModel" }, + { "label": "getExpandedRowModel", "to": "framework/react/reference/legacy/functions/getExpandedRowModel" }, + { "label": "getPaginationRowModel", "to": "framework/react/reference/legacy/functions/getPaginationRowModel" }, + { "label": "getFacetedRowModel", "to": "framework/react/reference/legacy/functions/getFacetedRowModel" }, + { "label": "getFacetedMinMaxValues", "to": "framework/react/reference/legacy/functions/getFacetedMinMaxValues" }, + { "label": "getFacetedUniqueValues", "to": "framework/react/reference/legacy/functions/getFacetedUniqueValues" }, + { "label": "LegacyRowModelOptions", "to": "framework/react/reference/legacy/interfaces/LegacyRowModelOptions" }, + { "label": "LegacyTable", "to": "framework/react/reference/legacy/type-aliases/LegacyTable" }, + { "label": "LegacyTableOptions", "to": "framework/react/reference/legacy/type-aliases/LegacyTableOptions" }, + { "label": "LegacyReactTable", "to": "framework/react/reference/legacy/type-aliases/LegacyReactTable" }, + { "label": "LegacyColumnDef", "to": "framework/react/reference/legacy/type-aliases/LegacyColumnDef" } + ] + } + ] + }, + { + "label": "Basic Examples", + "children": [], + "frameworks": [ + { + "label": "alpine", + "children": [ + { "to": "framework/alpine/examples/basic-create-table", "label": "Basic (createTable)" }, + { "to": "framework/alpine/examples/basic-app-table", "label": "Basic (createAppTable)" }, + { "to": "framework/alpine/examples/basic-external-state", "label": "Basic (External State)" }, + { "to": "framework/alpine/examples/basic-external-atoms", "label": "Basic (External Atoms)" }, + { "to": "framework/alpine/examples/basic-dynamic-columns", "label": "Basic (Dynamic Columns)" }, + { "to": "framework/alpine/examples/header-groups", "label": "Header Groups" } + ] + }, + { + "label": "angular", + "children": [ + { "label": "Basic (injectTable)", "to": "framework/angular/examples/basic-inject-table" }, + { "label": "Basic (createAppTable)", "to": "framework/angular/examples/basic-app-table" }, + { "label": "Basic (External State)", "to": "framework/angular/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/angular/examples/basic-external-atoms" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/angular/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/angular/examples/header-groups" } + ] + }, + { + "label": "ember", + "children": [ + { "label": "Basic (useTable)", "to": "framework/ember/examples/basic-table" }, + { "label": "Basic (createAppTable)", "to": "framework/ember/examples/basic-app-table" }, + { "label": "Basic (External State)", "to": "framework/ember/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/ember/examples/basic-external-atoms" }, + { "label": "Header Groups", "to": "framework/ember/examples/header-groups" } + ] + }, + { + "label": "lit", + "children": [ + { "label": "Basic (TableController)", "to": "framework/lit/examples/basic-table-controller" }, + { "label": "Basic (useAppTable)", "to": "framework/lit/examples/basic-app-table" }, + { "label": "Basic (External State)", "to": "framework/lit/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/lit/examples/basic-external-atoms" }, + { "label": "Basic (Subscribe)", "to": "framework/lit/examples/basic-subscribe" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/lit/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/lit/examples/header-groups" } + ] + }, + { + "label": "react", + "children": [ + { "label": "Basic (useTable)", "to": "framework/react/examples/basic-use-table" }, + { "label": "Basic (useAppTable)", "to": "framework/react/examples/basic-use-app-table" }, + { "label": "Basic (useLegacyTable)", "to": "framework/react/examples/basic-use-legacy-table" }, + { "label": "Basic (External State)", "to": "framework/react/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/react/examples/basic-external-atoms" }, + { "label": "Basic (Subscribe)", "to": "framework/react/examples/basic-subscribe" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/react/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/react/examples/header-groups" } ] }, { "label": "solid", "children": [ - { - "to": "framework/solid/examples/basic", - "label": "Basic" - }, - { - "to": "framework/solid/examples/column-groups", - "label": "Column Groups" - }, - { - "to": "framework/solid/examples/column-ordering", - "label": "Column Ordering" - }, - { - "to": "framework/solid/examples/column-visibility", - "label": "Column Visibility" - }, - { - "to": "framework/solid/examples/filters", - "label": "Filters" - }, - { - "to": "framework/solid/examples/sorting", - "label": "Sorting" - }, - { - "to": "framework/solid/examples/bootstrap", - "label": "Solid Bootstrap" - } + { "label": "Basic (createTable)", "to": "framework/solid/examples/basic-use-table" }, + { "label": "Basic (createAppTable)", "to": "framework/solid/examples/basic-app-table" }, + { "label": "Basic (External State)", "to": "framework/solid/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/solid/examples/basic-external-atoms" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/solid/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/solid/examples/header-groups" } ] }, { "label": "svelte", "children": [ - { - "to": "framework/svelte/examples/basic", - "label": "Basic" - }, - { - "to": "framework/svelte/examples/column-groups", - "label": "Column Groups" - }, - { - "to": "framework/svelte/examples/column-ordering", - "label": "Column Ordering" - }, - { - "to": "framework/svelte/examples/column-pinning", - "label": "Column Pinning" - }, - { - "to": "framework/svelte/examples/column-visibility", - "label": "Column Visibility" - }, - { - "to": "framework/svelte/examples/filtering", - "label": "Filtering" - }, - { - "to": "framework/svelte/examples/sorting", - "label": "Sorting" - } + { "label": "Basic (createTable)", "to": "framework/svelte/examples/basic-create-table" }, + { "label": "Basic (createAppTable)", "to": "framework/svelte/examples/basic-app-table" }, + { "label": "Basic (Snippets)", "to": "framework/svelte/examples/basic-snippets" }, + { "label": "Basic (External State)", "to": "framework/svelte/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/svelte/examples/basic-external-atoms" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/svelte/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/svelte/examples/header-groups" } ] }, { "label": "vue", "children": [ - { - "to": "framework/vue/examples/basic", - "label": "Basic" - }, - { - "to": "framework/vue/examples/column-ordering", - "label": "Column Ordering" - }, - { - "to": "framework/vue/examples/column-pinning", - "label": "Column Pinning" - }, - { - "to": "framework/vue/examples/pagination", - "label": "Pagination" - }, - { - "to": "framework/vue/examples/row-selection", - "label": "Row Selection" - }, - { - "to": "framework/vue/examples/sorting", - "label": "Sorting" - }, - { - "to": "framework/vue/examples/sub-components", - "label": "Sub Components" - }, - { - "to": "framework/vue/examples/filters", - "label": "Column Filters" - }, - { - "to": "framework/vue/examples/virtualized-rows", - "label": "Virtualized Rows" - }, - { - "to": "framework/vue/examples/grouping", - "label": "Grouping" - } + { "label": "Basic (useTable)", "to": "framework/vue/examples/basic-use-table" }, + { "label": "Basic (useAppTable)", "to": "framework/vue/examples/basic-use-app-table" }, + { "label": "Basic (External State)", "to": "framework/vue/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/vue/examples/basic-external-atoms" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/vue/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/vue/examples/header-groups" } + ] + }, + { + "label": "preact", + "children": [ + { "label": "Basic (useTable)", "to": "framework/preact/examples/basic-use-table" }, + { "label": "Basic (useAppTable)", "to": "framework/preact/examples/basic-use-app-table" }, + { "label": "Basic (External State)", "to": "framework/preact/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/preact/examples/basic-external-atoms" }, + { "label": "Basic (Subscribe)", "to": "framework/preact/examples/basic-subscribe" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/preact/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/preact/examples/header-groups" } + ] + }, + { + "label": "octane", + "children": [ + { "label": "Basic (useTable)", "to": "framework/octane/examples/basic-use-table" }, + { "label": "Basic (useAppTable)", "to": "framework/octane/examples/basic-use-app-table" }, + { "label": "Basic (External State)", "to": "framework/octane/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/octane/examples/basic-external-atoms" }, + { "label": "Basic (Subscribe)", "to": "framework/octane/examples/basic-subscribe" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/octane/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/octane/examples/header-groups" } ] }, { "label": "vanilla", "children": [ - { - "to": "framework/vanilla/examples/basic", - "label": "Basic" - }, - { - "to": "framework/vanilla/examples/pagination", - "label": "Pagination" - }, - { - "to": "framework/vanilla/examples/sorting", - "label": "Sorting" - } + { "label": "Basic", "to": "framework/vanilla/examples/basic" } + ] + } + ] + }, + { + "label": "Feature Examples", + "children": [], + "frameworks": [ + { + "label": "alpine", + "children": [ + { "to": "framework/alpine/examples/cell-selection", "label": "Cell Selection" }, + { "to": "framework/alpine/examples/cell-spanning", "label": "Cell Spanning" }, + { "to": "framework/alpine/examples/filters", "label": "Column Filters" }, + { "to": "framework/alpine/examples/filters-faceted", "label": "Column Filters (Faceted)" }, + { "to": "framework/alpine/examples/filters-faceted-bucketed", "label": "Bucketed Faceted Filters" }, + { "to": "framework/alpine/examples/column-ordering", "label": "Column Ordering" }, + { "to": "framework/alpine/examples/column-pinning", "label": "Column Pinning" }, + { "to": "framework/alpine/examples/column-pinning-split", "label": "Column Pinning (Split)" }, + { "to": "framework/alpine/examples/column-pinning-sticky", "label": "Sticky Column Pinning" }, + { "to": "framework/alpine/examples/column-sizing", "label": "Column Sizing" }, + { "to": "framework/alpine/examples/column-resizing", "label": "Column Resizing" }, + { "to": "framework/alpine/examples/column-resizing-performant", "label": "Performant Column Resizing" }, + { "to": "framework/alpine/examples/column-visibility", "label": "Column Visibility" }, + { "to": "framework/alpine/examples/expanding", "label": "Expanding" }, + { "to": "framework/alpine/examples/sub-components", "label": "Expanding Sub Components" }, + { "to": "framework/alpine/examples/grouping", "label": "Grouping" }, + { "to": "framework/alpine/examples/aggregation", "label": "Aggregation" }, + { "to": "framework/alpine/examples/grouped-aggregation", "label": "Grouped Aggregation" }, + { "to": "framework/alpine/examples/pagination", "label": "Pagination" }, + { "to": "framework/alpine/examples/row-pinning", "label": "Row Pinning" }, + { "to": "framework/alpine/examples/row-selection", "label": "Row Selection" }, + { "to": "framework/alpine/examples/sorting", "label": "Sorting" }, + { "to": "framework/alpine/examples/sorting-dynamic-data", "label": "Sorting (Dynamic Data)" } + ] + }, + { + "label": "angular", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/angular/examples/kitchen-sink" }, + { "label": "Cell Selection", "to": "framework/angular/examples/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/angular/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/angular/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/angular/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/angular/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/angular/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/angular/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/angular/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/angular/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/angular/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/angular/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/angular/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/angular/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/angular/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/angular/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/angular/examples/sub-components" }, + { "label": "Grouping", "to": "framework/angular/examples/grouping" }, + { "label": "Aggregation", "to": "framework/angular/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/angular/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/angular/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/angular/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/angular/examples/row-selection" }, + { "label": "Sorting", "to": "framework/angular/examples/sorting" } + ] + }, + { + "label": "ember", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/ember/examples/kitchen-sink" }, + { "label": "Cell Selection", "to": "framework/ember/examples/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/ember/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/ember/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/ember/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/ember/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/ember/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/ember/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/ember/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/ember/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/ember/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/ember/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/ember/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/ember/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/ember/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/ember/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/ember/examples/sub-components" }, + { "label": "Grouping", "to": "framework/ember/examples/grouping" }, + { "label": "Aggregation", "to": "framework/ember/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/ember/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/ember/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/ember/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/ember/examples/row-selection" }, + { "label": "Sorting", "to": "framework/ember/examples/sorting" } + ] + }, + { + "label": "lit", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/lit/examples/kitchen-sink" }, + { "label": "Cell Selection", "to": "framework/lit/examples/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/lit/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/lit/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/lit/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/lit/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/lit/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/lit/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/lit/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/lit/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/lit/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/lit/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/lit/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/lit/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/lit/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/lit/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/lit/examples/sub-components" }, + { "label": "Grouping", "to": "framework/lit/examples/grouping" }, + { "label": "Aggregation", "to": "framework/lit/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/lit/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/lit/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/lit/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/lit/examples/row-selection" }, + { "label": "Sorting", "to": "framework/lit/examples/sorting" }, + { "label": "Sorting (Dynamic Data)", "to": "framework/lit/examples/sorting-dynamic-data" } + ] + }, + { + "label": "react", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/react/examples/kitchen-sink" }, + { "label": "Cell Selection", "to": "framework/react/examples/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/react/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/react/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/react/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/react/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/react/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/react/examples/column-ordering" }, + { "label": "Column Ordering (DnD)", "to": "framework/react/examples/column-dnd" }, + { "label": "Column Pinning", "to": "framework/react/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/react/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/react/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/react/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/react/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/react/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/react/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/react/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/react/examples/sub-components" }, + { "label": "Grouping", "to": "framework/react/examples/grouping" }, + { "label": "Aggregation", "to": "framework/react/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/react/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/react/examples/pagination" }, + { "label": "Row DnD", "to": "framework/react/examples/row-dnd" }, + { "label": "Row Pinning", "to": "framework/react/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/react/examples/row-selection" }, + { "label": "Sorting", "to": "framework/react/examples/sorting" } + ] + }, + { + "label": "solid", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/solid/examples/kitchen-sink" }, + { "label": "Cell Selection", "to": "framework/solid/examples/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/solid/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/solid/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/solid/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/solid/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/solid/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/solid/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/solid/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/solid/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/solid/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/solid/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/solid/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/solid/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/solid/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/solid/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/solid/examples/sub-components" }, + { "label": "Grouping", "to": "framework/solid/examples/grouping" }, + { "label": "Aggregation", "to": "framework/solid/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/solid/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/solid/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/solid/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/solid/examples/row-selection" }, + { "label": "Sorting", "to": "framework/solid/examples/sorting" } + ] + }, + { + "label": "svelte", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/svelte/examples/kitchen-sink" }, + { "label": "Cell Selection", "to": "framework/svelte/examples/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/svelte/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/svelte/examples/filtering" }, + { "label": "Column Filters (Faceted)", "to": "framework/svelte/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/svelte/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/svelte/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/svelte/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/svelte/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/svelte/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/svelte/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/svelte/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/svelte/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/svelte/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/svelte/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/svelte/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/svelte/examples/sub-components" }, + { "label": "Grouping", "to": "framework/svelte/examples/grouping" }, + { "label": "Aggregation", "to": "framework/svelte/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/svelte/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/svelte/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/svelte/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/svelte/examples/row-selection" }, + { "label": "Sorting", "to": "framework/svelte/examples/sorting" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/vue/examples/kitchen-sink" }, + { "label": "Cell Selection", "to": "framework/vue/examples/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/vue/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/vue/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/vue/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/vue/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/vue/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/vue/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/vue/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/vue/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/vue/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/vue/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/vue/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/vue/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/vue/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/vue/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/vue/examples/sub-components" }, + { "label": "Grouping", "to": "framework/vue/examples/grouping" }, + { "label": "Aggregation", "to": "framework/vue/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/vue/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/vue/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/vue/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/vue/examples/row-selection" }, + { "label": "Sorting", "to": "framework/vue/examples/sorting" } + ] + }, + { + "label": "preact", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/preact/examples/kitchen-sink" }, + { "label": "Cell Selection", "to": "framework/preact/examples/cell-selection" }, + { "label": "Cell Spanning", "to": "framework/preact/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/preact/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/preact/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/preact/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/preact/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/preact/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/preact/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/preact/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/preact/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/preact/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/preact/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/preact/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/preact/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/preact/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/preact/examples/sub-components" }, + { "label": "Grouping", "to": "framework/preact/examples/grouping" }, + { "label": "Aggregation", "to": "framework/preact/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/preact/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/preact/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/preact/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/preact/examples/row-selection" }, + { "label": "Sorting", "to": "framework/preact/examples/sorting" } + ] + }, + { + "label": "octane", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/octane/examples/kitchen-sink" }, + { "label": "Cell Spanning", "to": "framework/octane/examples/cell-spanning" }, + { "label": "Column Filters", "to": "framework/octane/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/octane/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/octane/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/octane/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/octane/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/octane/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/octane/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/octane/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/octane/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/octane/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/octane/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/octane/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/octane/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/octane/examples/sub-components" }, + { "label": "Grouping", "to": "framework/octane/examples/grouping" }, + { "label": "Aggregation", "to": "framework/octane/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/octane/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/octane/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/octane/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/octane/examples/row-selection" }, + { "label": "Sorting", "to": "framework/octane/examples/sorting" } + ] + }, + { + "label": "vanilla", + "children": [ + { "label": "Aggregation", "to": "framework/vanilla/examples/aggregation" }, + { "label": "Pagination", "to": "framework/vanilla/examples/pagination" }, + { "label": "Sorting", "to": "framework/vanilla/examples/sorting" } + ] + } + ] + }, + { + "label": "Specialized Examples", + "children": [], + "frameworks": [ + { + "label": "alpine", + "children": [ + { "label": "Custom Plugin", "to": "framework/alpine/examples/custom-plugin" } + ] + }, + { + "label": "angular", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/angular/examples/composable-tables" }, + { "label": "Custom Plugin", "to": "framework/angular/examples/custom-plugin" }, + { "label": "With TanStack Virtual - Columns", "to": "framework/angular/examples/virtualized-columns" }, + { "label": "With TanStack Virtual - Rows", "to": "framework/angular/examples/virtualized-rows" }, + { "label": "With TanStack Virtual - Infinite Scrolling", "to": "framework/angular/examples/virtualized-infinite-scrolling" }, + { "label": "With TanStack Form", "to": "framework/angular/examples/with-tanstack-form" }, + { "label": "With TanStack Query", "to": "framework/angular/examples/with-tanstack-query" }, + { "label": "Fetch API data (SPA / SSR)", "to": "framework/angular/examples/remote-data" }, + { "label": "Signal Input", "to": "framework/angular/examples/signal-input" }, + { "label": "Row Selection (Signal)", "to": "framework/angular/examples/row-selection-signal" }, + { "label": "Editable data", "to": "framework/angular/examples/editable" }, + { "label": "Row Drag & Drop", "to": "framework/angular/examples/row-dnd" } + ] + }, + { + "label": "ember", + "children": [ + { "label": "Custom Plugin", "to": "framework/ember/examples/custom-plugin" }, + { "label": "Editable data", "to": "framework/ember/examples/editable" }, + { "label": "Fetch API data (SPA / SSR)", "to": "framework/ember/examples/remote-data" }, + { "label": "Row Drag & Drop", "to": "framework/ember/examples/row-dnd" } + ] + }, + { + "label": "lit", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/lit/examples/composable-tables" }, + { "label": "With TanStack Virtual - Columns", "to": "framework/lit/examples/virtualized-columns" }, + { "label": "With TanStack Virtual - Rows", "to": "framework/lit/examples/virtualized-rows" }, + { "label": "With TanStack Virtual - Infinite Scrolling", "to": "framework/lit/examples/virtualized-infinite-scrolling" } + ] + }, + { + "label": "react", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/react/examples/composable-tables" }, + { "label": "Custom Plugin", "to": "framework/react/examples/custom-plugin" }, + { "label": "Experimental Web Workers Plugin", "to": "framework/react/examples/web-worker-row-models" }, + { "label": "Experimental Spreadsheet", "to": "framework/react/examples/spreadsheet" }, + { "label": "With TanStack Virtual - Columns", "to": "framework/react/examples/virtualized-columns" }, + { "label": "With TanStack Virtual - Columns (Exp)", "to": "framework/react/examples/virtualized-columns-experimental" }, + { "label": "With TanStack Virtual - Rows", "to": "framework/react/examples/virtualized-rows" }, + { "label": "With TanStack Virtual - Rows (Exp)", "to": "framework/react/examples/virtualized-rows-experimental" }, + { "label": "With TanStack Virtual - Infinite Scrolling", "to": "framework/react/examples/virtualized-infinite-scrolling" }, + { "label": "With TanStack Form", "to": "framework/react/examples/with-tanstack-form" }, + { "label": "With TanStack Query", "to": "framework/react/examples/with-tanstack-query" }, + { "label": "With TanStack Router", "to": "framework/react/examples/with-tanstack-router" } + ] + }, + { + "label": "solid", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/solid/examples/composable-tables" }, + { "label": "Experimental Spreadsheet", "to": "framework/solid/examples/spreadsheet" }, + { "label": "With TanStack Virtual - Columns", "to": "framework/solid/examples/virtualized-columns" }, + { "label": "With TanStack Virtual - Rows", "to": "framework/solid/examples/virtualized-rows" }, + { "label": "With TanStack Virtual - Infinite Scrolling", "to": "framework/solid/examples/virtualized-infinite-scrolling" }, + { "label": "With TanStack Form", "to": "framework/solid/examples/with-tanstack-form" }, + { "label": "With TanStack Query", "to": "framework/solid/examples/with-tanstack-query" }, + { "label": "With TanStack Router", "to": "framework/solid/examples/with-tanstack-router" } + ] + }, + { + "label": "svelte", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/svelte/examples/composable-tables" }, + { "label": "Experimental Spreadsheet", "to": "framework/svelte/examples/spreadsheet" }, + { "label": "With TanStack Virtual - Columns", "to": "framework/svelte/examples/virtualized-columns" }, + { "label": "With TanStack Virtual - Rows", "to": "framework/svelte/examples/virtualized-rows" }, + { "label": "With TanStack Virtual - Infinite Scrolling", "to": "framework/svelte/examples/virtualized-infinite-scrolling" }, + { "label": "With TanStack Form", "to": "framework/svelte/examples/with-tanstack-form" }, + { "label": "With TanStack Query", "to": "framework/svelte/examples/with-tanstack-query" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/vue/examples/composable-tables" }, + { "label": "With TanStack Virtual - Columns", "to": "framework/vue/examples/virtualized-columns" }, + { "label": "With TanStack Virtual - Rows", "to": "framework/vue/examples/virtualized-rows" }, + { "label": "With TanStack Virtual - Infinite Scrolling", "to": "framework/vue/examples/virtualized-infinite-scrolling" }, + { "label": "With TanStack Form", "to": "framework/vue/examples/with-tanstack-form" }, + { "label": "With TanStack Query", "to": "framework/vue/examples/with-tanstack-query" } + ] + }, + { + "label": "preact", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/preact/examples/composable-tables" }, + { "label": "Custom Plugin", "to": "framework/preact/examples/custom-plugin" }, + { "label": "With TanStack Query", "to": "framework/preact/examples/with-tanstack-query" } + ] + }, + { + "label": "octane", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/octane/examples/composable-tables" }, + { "label": "Custom Plugin", "to": "framework/octane/examples/custom-plugin" } + ] + } + ] + }, + { + "label": "Component Library Examples", + "children": [], + "frameworks": [ + { + "label": "react", + "children": [ + { "label": "Basic - Shadcn (Base UI)", "to": "framework/react/examples/lib-shadcn-base" }, + { "label": "Basic - Shadcn (Radix)", "to": "framework/react/examples/lib-shadcn-radix" }, + { "label": "Basic - React Aria", "to": "framework/react/examples/lib-react-aria" }, + { "label": "Basic - Hero UI", "to": "framework/react/examples/lib-hero-ui" }, + { "label": "Basic - Material UI", "to": "framework/react/examples/lib-material-ui" }, + { "label": "Basic - Mantine", "to": "framework/react/examples/lib-mantine" }, + { "label": "Basic - Chakra UI", "to": "framework/react/examples/lib-chakra-ui" }, + { "label": "Kitchen Sink - Shadcn (Base UI)", "to": "framework/react/examples/kitchen-sink-shadcn-base" }, + { "label": "Kitchen Sink - Shadcn (Radix)", "to": "framework/react/examples/kitchen-sink-shadcn-radix" }, + { "label": "Kitchen Sink - React Aria", "to": "framework/react/examples/kitchen-sink-react-aria" }, + { "label": "Kitchen Sink - Hero UI", "to": "framework/react/examples/kitchen-sink-hero-ui" }, + { "label": "Kitchen Sink - Material UI", "to": "framework/react/examples/kitchen-sink-material-ui" }, + { "label": "Kitchen Sink - Mantine", "to": "framework/react/examples/kitchen-sink-mantine" }, + { "label": "Kitchen Sink - Chakra UI", "to": "framework/react/examples/kitchen-sink-chakra-ui" }, + { "label": "Material React Table", "to": "framework/react/examples/material-react-table" }, + { "label": "Mantine React Table", "to": "framework/react/examples/mantine-react-table" } + ] + }, + { + "label": "svelte", + "children": [ + { "label": "Basic - Shadcn", "to": "framework/svelte/examples/lib-shadcn" }, + { "label": "Kitchen Sink - Shadcn", "to": "framework/svelte/examples/kitchen-sink-shadcn" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "Basic - Shadcn", "to": "framework/vue/examples/lib-shadcn" }, + { "label": "Kitchen Sink - Shadcn", "to": "framework/vue/examples/kitchen-sink-shadcn" } ] } ] diff --git a/docs/devtools.md b/docs/devtools.md new file mode 100644 index 0000000000..5614ce499f --- /dev/null +++ b/docs/devtools.md @@ -0,0 +1,384 @@ +--- +title: Devtools +id: devtools +--- + +TanStack Table provides framework-specific devtools adapters that plug into the [TanStack Devtools](https://tanstack.com/devtools) multi-panel UI. + +The table devtools let you inspect registered table instances, switch between multiple tables, and inspect features, state, options, rows, and columns in real time. + +> [!NOTE] +> By default, the framework adapters only include the live devtools in development mode. In production builds they export no-op implementations unless you opt into the `/production` entrypoints. + +## Installation + +Install the TanStack Devtools host package and the Table adapter for your framework. + + + +# React + +```sh +npm install @tanstack/react-devtools @tanstack/react-table-devtools +``` + +# Preact + +```sh +npm install @tanstack/preact-devtools @tanstack/preact-table-devtools +``` + +# Octane + +There is not currently a dedicated Octane Table Devtools adapter. + +# Vue + +```sh +npm install @tanstack/vue-devtools @tanstack/vue-table-devtools +``` + +# Solid + +```sh +npm install @tanstack/solid-devtools @tanstack/solid-table-devtools +``` + +# Angular + +```sh +npm install @tanstack/angular-devtools @tanstack/angular-table-devtools +``` + + + +Octane, Lit, Svelte, Alpine, and vanilla do not currently ship dedicated table devtools adapters. + +## The Required `key` Table Option + +The devtools identify each table by the `key` table option. Registration requires it. If you register a table without a `key`, the devtools log an error (`Missing table key. Add a 'key' option to your table to use devtools.`) and skip the table entirely. + +```ts +const table = useTable({ + key: 'users-table', // needed for devtools, omit if you don't want to use the devtools + features, + columns, + data, +}) +``` + +The `key` is also the label shown in the devtools panel selector, so give each table a unique, descriptive key. + +## Setup Pattern + +The recommended setup has three parts: + +1. Give each table a unique `key` option +2. Mount `TanStackDevtools` at the app root with `tableDevtoolsPlugin()` +3. Register each table with `useTanStackTableDevtools(table)` (or `injectTanStackTableDevtools` in Angular) immediately after creating it + +If you register multiple tables, the Table panel shows a selector so you can switch between them. + +## Setup + + + +# React + +```tsx +import React from 'react' +import ReactDOM from 'react-dom/client' +import { useTable } from '@tanstack/react-table' +import { TanStackDevtools } from '@tanstack/react-devtools' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools' + +function App() { + const table = useTable({ + key: 'users-table', // needed for devtools + // ... + }) + + useTanStackTableDevtools(table) + + return +} + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + , +) +``` + +See the [React row-selection example](./framework/react/examples/row-selection). + +# Preact + +```tsx +import { render } from 'preact' +import { useTable } from '@tanstack/preact-table' +import { TanStackDevtools } from '@tanstack/preact-devtools' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/preact-table-devtools' + +function App() { + const table = useTable({ + key: 'users-table', // needed for devtools + // ... + }) + + useTanStackTableDevtools(table) + + return +} + +render( + <> + + + , + document.getElementById('root')!, +) +``` + +# Octane + +Octane tables expose the same `table.state`, slice atoms, `table.store`, and `table.Subscribe` inspection surfaces, but there is not currently an Octane Table Devtools plugin to register. + +See the [Preact row-selection example](./framework/preact/examples/row-selection). + +# Vue + +```ts +// main.ts +import { createApp, defineComponent, h } from 'vue' +import { TanStackDevtools } from '@tanstack/vue-devtools' +import { tableDevtoolsPlugin } from '@tanstack/vue-table-devtools' +import App from './App.vue' + +const Root = defineComponent({ + setup() { + return () => [ + h(App), + h(TanStackDevtools, { + plugins: [tableDevtoolsPlugin({})], + }), + ] + }, +}) + +createApp(Root).mount('#app') +``` + +```vue + +``` + +See the [Vue row-selection example](./framework/vue/examples/row-selection). + +# Solid + +```tsx +import { render } from 'solid-js/web' +import { createTable } from '@tanstack/solid-table' +import { TanStackDevtools } from '@tanstack/solid-devtools' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/solid-table-devtools' + +function App() { + const table = createTable({ + key: 'users-table', // needed for devtools + // ... + }) + + useTanStackTableDevtools(table) + + return +} + +render( + () => ( + <> + + + + ), + document.getElementById('root')!, +) +``` + +See the [Solid row-selection example](./framework/solid/examples/row-selection). + +# Angular + +Provide the devtools host once in your application config, rendering the table panel from `@tanstack/angular-table-devtools`: + +```ts +// app.config.ts +import { isDevMode } from '@angular/core' +import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [ + isDevMode() + ? provideTanStackDevtools(() => ({ + plugins: [ + { + name: 'TanStack Table', + render: () => + import('@tanstack/angular-table-devtools').then((m) => + m.TableDevtoolsPanel(), + ), + }, + ], + })) + : [], + ], +} +``` + +Then register each table with `injectTanStackTableDevtools` in an injection context (such as a component constructor): + +```ts +import { Component } from '@angular/core' +import { injectTable } from '@tanstack/angular-table' +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' + +@Component({ + // ... +}) +export class App { + constructor() { + injectTanStackTableDevtools(() => ({ + table: this.table, + })) + } + + readonly table = injectTable(() => ({ + key: 'users-table', // needed for devtools + // ... + })) +} +``` + +See the [Angular basic example](./framework/angular/examples/basic-inject-table) or the [Angular row-selection example](./framework/angular/examples/row-selection). + + + +## Disabling Registration + +Each registration function accepts an `enabled` option if you want to conditionally register a table: + + + +# React + +```ts +useTanStackTableDevtools(table, { enabled: false }) +``` + +# Preact + +```ts +useTanStackTableDevtools(table, { enabled: false }) +``` + +# Octane + +No adapter toggle is required because Octane does not currently ship a Table Devtools adapter. + +# Vue + +```ts +useTanStackTableDevtools(table, { enabled: false }) +``` + +# Solid + +```ts +useTanStackTableDevtools(table, { enabled: false }) +``` + +# Angular + +```ts +injectTanStackTableDevtools(() => ({ + table: this.table, + enabled: () => false, +})) +``` + + + +## Production Builds + +If you need the live devtools in production, import from the `/production` entrypoint for your framework package: + + + +# React + +```tsx +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools/production' +``` + +# Preact + +```tsx +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/preact-table-devtools/production' +``` + +# Octane + +There is no production Devtools entry point for Octane Table at this time. + +# Vue + +```tsx +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/vue-table-devtools/production' +``` + +# Solid + +```tsx +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/solid-table-devtools/production' +``` + +# Angular + +```ts +import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools/production' +``` + + diff --git a/docs/enterprise/ag-grid.md b/docs/enterprise/ag-grid.md deleted file mode 100644 index 6836b9af11..0000000000 --- a/docs/enterprise/ag-grid.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: AG Grid - An alternative enterprise data-grid solution ---- - -

- - - -

- -While we clearly love TanStack Table, we acknowledge that it is not a "batteries" included product packed with customer support and enterprise polish. We realize that some of our users may need this though! To help out here, we want to introduce you to AG Grid, an enterprise-grade data grid solution that can supercharge your applications with its extensive feature set and robust performance. While TanStack Table is also a powerful option for implementing data grids, we believe in providing our users with a diverse range of choices that best fit their specific requirements. AG Grid is one such choice, and we're excited to highlight its capabilities for you. - -## Why Choose [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable)? - -Here are some good reasons to consider AG Grid for your next project: - -### Comprehensive Feature Set - -AG Grid offers an extensive set of features, making it a versatile and powerful data grid solution. With AG Grid, you get access to a wide range of functionalities that cater to the needs of complex enterprise applications. From advanced sorting, filtering, and grouping capabilities to column pinning, multi-level headers, and tree data structure support, AG Grid provides you with the tools to create dynamic and interactive data grids that meet your application's unique demands. - -### High Performance - -When it comes to handling large datasets and achieving exceptional performance, AG Grid delivers outstanding results. It employs highly optimized rendering techniques, efficient data updates, and virtualization to ensure smooth scrolling and fast response times, even when dealing with thousands or millions of rows of data. AG Grid's performance optimizations make it an excellent choice for applications that require high-speed data manipulation and visualization. - -### Customization and Extensibility - -AG Grid is designed to be highly customizable and extensible, allowing you to tailor the grid to your specific needs. It provides a rich set of APIs and events that enable you to integrate custom functionality seamlessly. You can define custom cell renderers, editors, filters, and aggregators to enhance the grid's behavior and appearance. AG Grid also supports a variety of themes, allowing you to match the grid's visual style to your application's design. - -### Support for Enterprise Needs - -As an enterprise-focused solution, AG Grid caters to the requirements of complex business applications. It offers enterprise-specific features such as row grouping, column pinning, server-side row model, master/detail grids, and rich editing capabilities. AG Grid also integrates well with other enterprise frameworks and libraries, making it a reliable choice for large-scale projects. - -### Active Development and Community Support - -AG Grid benefits from active development and a thriving community of developers. The team behind AG Grid consistently introduces new features and enhancements, ensuring that the product evolves to meet the changing needs of the industry. The community support is robust, with forums, documentation, and examples readily available to assist you in utilizing the full potential of AG Grid. - -## Conclusion - -While TanStack Table remains a powerful and flexible option for implementing data grids, we understand that different projects have different requirements. AG Grid offers a compelling enterprise-grade solution that may be particularly suited to your needs. Its comprehensive feature set, high performance, customization options, and focus on enterprise requirements make AG Grid an excellent choice for projects that demand a robust and scalable data grid solution. - -We encourage you to explore AG Grid further by visiting their website and trying out their demo. Remember that both TanStack Table and AG Grid have their unique strengths and considerations. We believe in providing options to our users, empowering you to make informed decisions and choose the best fit for your specific use case. - -Visit the [AG Grid website](https://www.ag-grid.com). diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index 32bda46315..0000000000 --- a/docs/faq.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: FAQ ---- - -## How do I stop infinite rendering loops? - -If you are using React, there is a very common pitfall that can cause infinite rendering. If you fail to give your `columns`, `data`, or `state` a stable reference, React will enter an infinite loop of re-rendering upon any change to the table state. - -Why does this happen? Is this a bug in TanStack Table? **No**, it is not. *This is fundamentally how React works*, and properly managing your columns, data, and state will prevent this from happening. - -TanStack Table is designed to trigger a re-render whenever either the `data` or `columns` that are passed into the table change, or whenever any of the table's state changes. - -> Failing to give `columns` or `data` stable references can cause an infinite loop of re-renders. - -### Pitfall 1: Creating new columns or data on every render - -```js -export default function MyComponent() { - //😵 BAD: This will cause an infinite loop of re-renders because `columns` is redefined as a new array on every render! - const columns = [ - // ... - ]; - - //😵 BAD: This will cause an infinite loop of re-renders because `data` is redefined as a new array on every render! - const data = [ - // ... - ]; - - //❌ Columns and data are defined in the same scope as `useReactTable` without a stable reference, will cause infinite loop! - const table = useReactTable({ - columns, - data, - }); - - return ...
; -} -``` - -### Solution 1: Stable references with useMemo or useState - -In React, you can give a "stable" reference to variables by defining them outside/above the component, or by using `useMemo` or `useState`, or by using a 3rd party state management library (like Redux or React Query 😉) - -```js -//✅ OK: Define columns outside of the component -const columns = [ - // ... -]; - -//✅ OK: Define data outside of the component -const data = [ - // ... -]; - -// Usually it's more practical to define columns and data inside the component, so use `useMemo` or `useState` to give them stable references -export default function MyComponent() { - //✅ GOOD: This will not cause an infinite loop of re-renders because `columns` is a stable reference - const columns = useMemo(() => [ - // ... - ], []); - - //✅ GOOD: This will not cause an infinite loop of re-renders because `data` is a stable reference - const [data, setData] = useState(() => [ - // ... - ]); - - // Columns and data are defined in a stable reference, will not cause infinite loop! - const table = useReactTable({ - columns, - data, - }); - - return ...
; -} -``` - -### Pitfall 2: Mutating columns or data in place - -Even if you give your initial `columns` and `data` stable references, you can still run into infinite loops if you mutate them in place. This is a common pitfall that you may not notice that you are doing at first. Something as simple as an inline `data.filter()` can cause an infinite loop if you are not careful. - -```js -export default function MyComponent() { - //✅ GOOD - const columns = useMemo(() => [ - // ... - ], []); - - //✅ GOOD (React Query provides stable references to data automatically) - const { data, isLoading } = useQuery({ - //... - }); - - const table = useReactTable({ - columns, - //❌ BAD: This will cause an infinite loop of re-renders because `data` is mutated in place (destroys stable reference) - data: data?.filter(d => d.isActive) ?? [], - }); - - return ...
; -} -``` - -### Solution 2: Memoize your data transformations - -To prevent infinite loops, you should always memoize your data transformations. This can be done with `useMemo` or similar. - -```js -export default function MyComponent() { - //✅ GOOD - const columns = useMemo(() => [ - // ... - ], []); - - //✅ GOOD - const { data, isLoading } = useQuery({ - //... - }); - - //✅ GOOD: This will not cause an infinite loop of re-renders because `filteredData` is memoized - const filteredData = useMemo(() => data?.filter(d => d.isActive) ?? [], [data]); - - const table = useReactTable({ - columns, - data: filteredData, // stable reference! - }); - - return ...
; -} -``` - -### React Forget - -When React Forget is released, these problems might be a thing of the past. Or just use Solid.js... 🤓 - -## How do I stop my table state from automatically resetting when my data changes? - -Most plugins use state that _should_ normally reset when the data sources changes, but sometimes you need to suppress that from happening if you are filtering your data externally, or immutably editing your data while looking at it, or simply doing anything external with your data that you don't want to trigger a piece of table state to reset automatically. - -For those situations, each plugin provides a way to disable the state from automatically resetting internally when data or other dependencies for a piece of state change. By setting any of them to `false`, you can stop the automatic resets from being triggered. - -Here is a React-based example of stopping basically every piece of state from changing as they normally do while we edit the `data` source for a table: - -```js -const [data, setData] = React.useState([]) -const skipPageResetRef = React.useRef() - -const updateData = newData => { - // When data gets updated with this function, set a flag - // to disable all of the auto resetting - skipPageResetRef.current = true - - setData(newData) -} - -React.useEffect(() => { - // After the table has updated, always remove the flag - skipPageResetRef.current = false -}) - -useReactTable({ - ... - autoResetPageIndex: !skipPageResetRef.current, - autoResetExpanded: !skipPageResetRef.current, -}) -``` - -Now, when we update our data, the above table states will not automatically reset! diff --git a/docs/framework/alpine/guide/aggregation.md b/docs/framework/alpine/guide/aggregation.md new file mode 100644 index 0000000000..0100de15cf --- /dev/null +++ b/docs/framework/alpine/guide/aggregation.md @@ -0,0 +1,269 @@ +--- +title: Aggregation (Alpine) Guide +--- + +## Examples + +- [Aggregation](../examples/aggregation) +- [Grouped Aggregation](../examples/grouped-aggregation) + +Aggregation is independent from column grouping. Register `rowAggregationFeature` +whenever columns calculate totals or aggregated values. Add +`columnGroupingFeature` separately only when the table also groups rows. + +## Aggregation Setup + +Register only the built-in functions referenced by name. Passing a definition +directly to a column does not require a registry entry. + +```ts +import { + rowAggregationFeature, + aggregationFn_count, + aggregationFn_extent, + aggregationFn_mean, + aggregationFn_sum, + tableFeatures, + createTable, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowAggregationFeature, + aggregationFns: { + count: aggregationFn_count, + extent: aggregationFn_extent, + mean: aggregationFn_mean, + sum: aggregationFn_sum, + }, +}) + +const table = createTable({ + features, + columns, + data, +}) +``` + +The aggregation feature does not require a grouped row model. This makes grand +totals and custom row-subset totals available in otherwise ordinary tables. + +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + +## Column Aggregations + +A column accepts one aggregation or an array. A single entry returns a scalar; +multiple entries return an object keyed by the aggregation name or descriptor +`id`. + +```ts +columnHelper.accessor('amount', { + aggregationFn: 'sum', +}) + +columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], +}) +``` + +String values remain backward-compatible. Use descriptors when a result needs +a stable custom key or options. + +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + +## Grand Totals and Row Subsets + +Call `column.getAggregationValue()` without arguments to aggregate the default +pre-grouped row model. Filtering is included; grouping, sorting, expansion, and +pagination do not change that default total. + +```ts +footer: ({ column }) => column.getAggregationValue().toLocaleString() +``` + +Pass one options object with rows from any row model to choose a different set: + +```ts +column.getAggregationValue({ rows: table.getCoreRowModel().rows }) +column.getAggregationValue({ rows: table.getRowModel().rows }) +column.getAggregationValue({ rows: table.getFilteredSelectedRowModel().rows }) +column.getAggregationValue({ rows: table.getCoreRowModel().rows.slice(0, 3) }) +column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) +``` + +Depth is relative to the supplied row array. `0` selects those roots, `1` +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` + +## Grouped Aggregation + +Grouped aggregation composes two independent features. Register both, add the +grouped row-model slot, and configure aggregation functions on the columns that +should produce grouped values. + +```ts +const features = tableFeatures({ + rowAggregationFeature, + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), + aggregationFns: { sum: aggregationFn_sum }, +}) + +columnHelper.accessor('visits', { + aggregationFn: 'sum', + aggregatedCell: ({ getValue }) => getValue().toLocaleString(), + footer: ({ column }) => column.getAggregationValue().toLocaleString(), +}) +``` + +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. + +## Custom Aggregation Definitions + +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers + +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/alpine/guide/cell-selection.md b/docs/framework/alpine/guide/cell-selection.md new file mode 100644 index 0000000000..2ada44ca5d --- /dev/null +++ b/docs/framework/alpine/guide/cell-selection.md @@ -0,0 +1,385 @@ +--- +title: Cell Selection (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Cell Selection](../examples/cell-selection) + +### Cell Selection Setup + +Here's how you set up your table to use cell selection features. Adding the cell selection feature enables the related APIs. + +```ts +import Alpine from 'alpinejs' +import { + createTable, + tableFeatures, + cellSelectionFeature, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ cellSelectionFeature }) + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: defaultData }) + + const table = createTable( + { + features, + columns, + get data() { + return local.data + }, + }, + // the selector decides which slices bump Alpine's version counter; + // cellSelection has to be here or the highlight never moves + (state) => ({ cellSelection: state.cellSelection }), + ) + + return { table } +}) +``` + +## Cell Selection (Alpine) Guide + +The cell selection feature keeps track of spreadsheet-style rectangular selections. A user can click a cell, drag across a block of cells, Shift-click to extend, and Ctrl/Cmd-drag to add or subtract a rectangle based on whether the starting cell is selected. Let's take a look at some common use cases. + +### Access Cell Selection State + +The table instance already manages the cell selection state for you. You can access the selection or values derived from it through a few APIs. + +- `table.atoms.cellSelection.get()` - returns the current cell selection. Alpine re-renders from a version counter, so include `cellSelection` in the `createTable` selector for the DOM to update +- `getSelectedCellCount()` - returns how many cells are selected +- `getSelectedCellIds()` - returns the ids of every selected cell +- `getCellSelectionRowIds()` / `getCellSelectionColumnIds()` - returns the rows and columns the selection touches +- `getSelectedCellRangesData()` - returns each final positive selection region's values as a row-major grid + +```ts +console.log(table.atoms.cellSelection.get()) //get the cell selection state +console.log(table.getSelectedCellCount()) //3 +console.log(table.getSelectedCellIds()) //['0_firstName', '0_lastName', '1_firstName'] +console.log(table.getSelectedCellRangesData()) //[[['Tanner', 'Linsley'], ['Kevin', 'Vandy']]] +``` + +Reads of `table.atoms.cellSelection.get()` are tracked inside Alpine reactive contexts, so they stay fresh automatically. Outside one, the same call is a plain snapshot. + +The expansion APIs (`getSelectedCellIds`, `getSelectedCellRangesData`) are memoized and pull-based. They cost nothing unless you actually call them, so a table that only highlights cells never pays to enumerate a large selection. + +### Cell Selection State Shape + +`CellSelectionState` is an ordered array of range operations, each stored as its two defining corners: + +```ts +type CellSelectionRange = { + anchorRowId: string + anchorColumnId: string + focusRowId: string + focusColumnId: string + operation?: 'include' | 'exclude' +} + +type CellSelectionState = Array +``` + +The `anchor` corner is where the selection started and stays put. The `focus` corner is the one that moves while dragging or Shift-extending. Storing both corners, rather than a normalized min/max rectangle, is what makes Shift-extend and "collapse back to the active cell" possible. + +Ranges are applied in order. An omitted `operation` is an inclusion for backward compatibility; an `exclude` range subtracts its rectangle from the selection produced so far. This compact operation log means a "select all except these cells" interaction does not build a map with one entry per selected cell. + +### Manage Cell Selection State + +If you need access to the selection elsewhere in your application, you can own the state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. + +```ts +import { createAtom } from '@tanstack/store' +import { + createTable, + tableFeatures, + cellSelectionFeature, + type CellSelectionState, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ cellSelectionFeature }) +const cellSelectionAtom = createAtom([]) + +const table = createTable( + { + features, + columns, + get data() { + return local.data + }, + atoms: { cellSelection: cellSelectionAtom }, + }, + (state) => ({ cellSelection: state.cellSelection }), +) +``` + +The classic controlled-state pattern also works: + +```ts +const local = Alpine.reactive({ data, cellSelection: [] as CellSelectionState }) + +const table = createTable( + { + features, + columns, + get data() { + return local.data + }, + get state() { + return { cellSelection: local.cellSelection } + }, + onCellSelectionChange: (updater) => { + local.cellSelection = + typeof updater === 'function' ? updater(local.cellSelection) : updater + }, + }, + (state) => ({ cellSelection: state.cellSelection }), +) +``` + +> [!NOTE] +> a drag emits one change per cell boundary the pointer crosses, so `onCellSelectionChange` fires repeatedly during a drag. If you are syncing selection to a server or a URL, debounce it or commit on `mouseup`. + +### Useful Row Ids + +Cell selection is keyed by row id and column id, so a meaningful row id matters here for the same reason it does with row selection. Use the `getRowId` table option to key selection by something stable from your data. + +```ts +const table = createTable({ + features, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +### Enable Cell Selection Conditionally + +Cell selection is enabled by default for every cell. Use the `enableCellSelection` table option to turn it off entirely, or pass a function for per-cell control. + +```ts +const table = createTable({ + features, + //... + enableCellSelection: (cell) => cell.row.original.age > 18, //only adults' cells are selectable +}) +``` + +A column def can also opt out, which is the common case for checkbox or action columns. A column-level `false` wins over the table option. + +```ts +columnHelper.accessor('actions', { + enableCellSelection: false, //this column can never be selected +}) +``` + +A cell that cannot be selected is skipped even when a rectangle is drawn straight through it, and `moveCellSelection` steps over its column rather than landing on it. Use `cell.getCanSelect()` to decide whether to attach selection handlers in your UI. + +### Mouse Interactions + +Two cell handlers drive every mouse interaction: + +- `cell.getSelectionStartHandler()` - bind to `onMouseDown` +- `cell.getSelectionExtendHandler()` - bind to `onMouseEnter` + +```html + + + +``` + +You do not need to handle `mouseup` yourself. The start handler attaches its own document-level `mouseup` listener and removes it when the drag ends, so releasing the pointer outside the table still finishes the drag correctly. If your table renders into another document, such as an iframe or a popout window, pass that document in: `cell.getSelectionStartHandler(myDocument)`. + +#### Drag Selection + +Pressing down on a cell starts a new single-cell range, and every cell the pointer then enters moves that range's focus corner. Set `enableCellSelectionDrag: false` to require explicit clicks instead. + +#### Shift Range Selection + +Shift-clicking moves the active range's focus corner to the clicked cell, keeping its anchor fixed. The active cell therefore stays where the selection started, matching spreadsheet behavior. + +The handler recognizes Shift when the event exposes either `event.shiftKey` or `event.nativeEvent.shiftKey`. You can disable range behavior or replace the detection: + +```ts +const table = createTable({ + features, + //... + enableCellRangeSelection: false, + + // For example, use the platform modifier instead of Shift: + // isCellRangeSelectionEvent: event => Boolean(event.metaKey), +}) +``` + +#### Multiple Ranges + +Ctrl-clicking or Cmd-clicking an unselected cell adds a new inclusive rectangle. Starting the same modified interaction on a selected cell adds an exclusion instead, so clicking removes that cell and dragging subtracts the whole rectangle. Whether the drag includes or excludes is fixed when it starts; shrinking an exclusion drag restores cells that leave its rectangle. Set `enableMultiCellRangeSelection: false` to disable both behaviors, or override `isMultiCellRangeSelectionEvent` to change the modifier. + +#### Programmatic Range Operations + +`table.selectCellRange(range)` replaces the current selection. Pass `{ mode: 'include' }` to append an inclusion or `{ mode: 'exclude' }` to append an exclusion. The older `{ additive: true }` option remains as a deprecated alias for include mode; `mode` wins if both options are supplied. `table.getCellSelectionBounds()` resolves the operation log into deterministic, disjoint positive rectangles. + +### Render Cell Selection UI + +TanStack Table does not dictate how you render selected cells. These cell APIs give you everything you need: + +- `cell.getIsSelected()` - whether this cell falls inside any range +- `cell.getIsFocused()` - whether this is the active cell (an excluded anchor can be focused without being selected) +- `cell.getSelectionEdges()` - which sides sit on the selection boundary +- `cell.getTabIndex()` - `0` for the focused cell and `-1` otherwise, for roving tabindex + +`getSelectionEdges()` returns `{ top, right, bottom, left }`, where a side is `true` when the neighboring cell in that direction is not itself selected. That is what lets you draw a single continuous outline around a selection, including around a union of separate rectangles, without every cell inspecting its neighbors. + +```tsx +function getCellClassName(cell) { + // most cells are unselected, so bail before asking for edges + if (!cell.getIsSelected()) { + return cell.getIsFocused() ? 'cell cell-focused' : 'cell' + } + + const edges = cell.getSelectionEdges() + + return [ + 'cell', + 'cell-selected', + cell.getIsFocused() && 'cell-focused', + edges.top && 'cell-edge-top', + edges.right && 'cell-edge-right', + edges.bottom && 'cell-edge-bottom', + edges.left && 'cell-edge-left', + ] + .filter(Boolean) + .join(' ') +} +``` + +> [!TIP] +> draw the outline with `box-shadow: inset ...` rather than `border`. On a `border-collapse` table a thicker border widens the shared grid line, which makes rows change height as cells become selected. A box-shadow never affects layout. + +### Keyboard Navigation + +Cell selection ships no keyboard handling of its own. Instead it exposes imperative APIs so a dedicated library, such as [TanStack Hotkeys](https://tanstack.com/hotkeys), can drive it: + +- `table.moveCellSelection(direction)` - collapse the selection to a single cell one step away +- `table.extendCellSelection(direction)` - move the active range's focus corner, keeping its anchor +- `table.setFocusedCell(rowId, columnId)` - collapse the selection to one specific cell +- `table.selectAllCells()` - select every selectable cell +- `table.resetCellSelection(true)` - clear the selection + +`direction` is `'up'`, `'down'`, `'left'`, or `'right'`. + +```ts +import { createMultiHotkeyHandler } from '@tanstack/hotkeys' + +// Alpine has no hotkeys adapter, so the framework-agnostic core handler is used +onGridKeyDown: createMultiHotkeyHandler({ + ArrowUp: () => table.moveCellSelection('up'), + ArrowDown: () => table.moveCellSelection('down'), + 'Shift+ArrowDown': () => table.extendCellSelection('down'), + 'Mod+A': () => table.selectAllCells(), + Escape: () => table.resetCellSelection(true), +}), + +// then, in the markup: +//
...
+``` + +Scope the hotkeys to the grid element rather than the document, or arrow keys and Escape will hijack inputs elsewhere on the page. + +### Copying a Selection + +`getSelectedCellRangesData()` returns raw values indexed as `[regionIndex][rowIndex][columnIndex]`. A region is one of the final disjoint positive rectangles after all include and exclude operations are applied, so it does not necessarily correspond one-to-one with stored state. Turning that into clipboard text is left to your application, because the delimiter, the representation of `null`, and any quoting rules are decisions only you can make. + +```ts +function escapeTsvValue(value: unknown) { + const text = value == null ? '' : String(value) + const safeText = + typeof value === 'string' && /^[\t\r ]*[=+@-]/.test(value) + ? `'${text}` + : text + // spreadsheets expect a quoted field once it contains a delimiter, a newline, + // or a quote, with inner quotes doubled + return /["\t\n\r]/.test(safeText) + ? `"${safeText.replace(/"/g, '""')}"` + : safeText +} + +function toTsv(ranges: Array>>) { + return ranges + .map((grid) => + grid.map((row) => row.map(escapeTsvValue).join('\t')).join('\n'), + ) + .join('\n\n') +} + +navigator.clipboard.writeText(toTsv(table.getSelectedCellRangesData())) +``` + +### How Ranges Survive Table Changes + +Ranges store row and column ids, not positions, so they follow their corner cells rather than screen coordinates. + +- **Sorting, filtering, and column reordering** keep the corners pinned and recompute what sits between them. A range from "row A to row B" still runs from A to B after a sort, even though different rows now fall in between. +- **Column pinning** is accounted for in render order, so a rectangle stays visually contiguous when a column is pinned. +- **Hiding a column** that a corner sits on makes the range inert. Nothing renders as selected, but the range stays in state and comes back when the column is shown again. +- **Pagination** resolves against the pre-pagination order, so a range can span pages and lights up correctly on whichever page you are viewing. + +Because a reorder can widen a selection onto columns the user never picked, some applications prefer to clear the selection whenever the column layout changes. That is a userland decision; an Alpine reactive effect can implement it: + +```ts +let lastLayoutKey: string | undefined + +Alpine.effect(() => { + const layoutKey = JSON.stringify([ + table.atoms.columnOrder.get(), + table.atoms.columnPinning.get(), + table.atoms.columnVisibility.get(), + ]) + + if (lastLayoutKey === undefined) { + lastLayoutKey = layoutKey + } else if (layoutKey !== lastLayoutKey) { + lastLayoutKey = layoutKey + queueMicrotask(() => table.resetCellSelection(true)) + } +}) +``` + +### Resetting Cell Selection + +`table.resetCellSelection()` restores `initialState.cellSelection`. Pass `true` to ignore initial state and clear the selection entirely. + +The selection also resets automatically whenever `data` changes, because new data can invalidate the row ids a range points at, or silently re-select cells if the new data happens to reuse ids. Turn that off with `autoResetCellSelection: false`, and note that `autoResetAll` overrides it. + +```ts +const table = createTable({ + features, + //... + autoResetCellSelection: false, //keep ranges across data changes +}) +``` + +### Performance + +Alpine re-renders from a version counter rather than tracking individual +reads, so every selection change re-evaluates the `x-for` that renders the rows. +That makes cell selection the feature most likely to make a large Alpine table +feel slow. + +Measured on a table with a thousand rows and twelve columns, a drag updates in +roughly 160ms per move. The same table is around 12-19ms per move on the +signal-based adapters. Almost all of that difference is Alpine re-rendering rows, +not the selection reads themselves: `cell.getIsSelected()` resolves the cell's +row and column index and compares them against a memoized cache of the selection +bounds, which is a handful of integer comparisons. + +There is no fine-grained subscription primitive to reach for here, so the +practical fix is to render fewer rows. Paginate the table, or keep the rendered +row count small, and cell selection stays responsive. Ranges are stored as row +and column ids resolved against the pre-pagination order, so a selection can +still span pages. diff --git a/docs/framework/alpine/guide/cell-spanning.md b/docs/framework/alpine/guide/cell-spanning.md new file mode 100644 index 0000000000..143b94d03a --- /dev/null +++ b/docs/framework/alpine/guide/cell-spanning.md @@ -0,0 +1,150 @@ +--- +title: Cell Spanning (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Cell Spanning](../examples/cell-spanning) + +### Cell Spanning Setup + +Here's how you set up your table to use cell spanning features. Adding the cell spanning feature enables the related APIs. + +```ts +import Alpine from 'alpinejs' +import { + FlexRender, + createTable, + tableFeatures, + cellSpanningFeature, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ cellSpanningFeature }) + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: defaultData }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + }) + + return { table, FlexRender } +}) +``` + +Cell spanning itself is stateless, so it needs nothing in the `createTable` selector. Spans recompute whenever the row model changes, so keep whichever state slices drive your row model (`sorting`, `columnFilters`, `pagination`, and so on) in the selector as usual. + +## Cell Spanning (Alpine) Guide + +The cell spanning feature merges adjacent body cells into one rendered cell, the way `rowspan` and `colspan` merge cells in a plain HTML table or a spreadsheet. Row spans are derived from the data: adjacent rows that share a value in an opted-in column merge into one vertically spanning cell. Column spans are declared per row for things like full-width summary rows. + +The feature is stateless. Spans are always recomputed from the rows that are actually rendered, so sorting, filtering, pagination, and row pinning simply change which rows are adjacent and the spans follow. There is nothing to persist and nothing to reset. + +### Enable Row Spanning per Column + +Opt a column into value-based row spanning with `spanRows` on its column def: + +```ts +const columns = [ + columnHelper.accessor('region', { + spanRows: true, // adjacent rows with equal region values merge + }), +] +``` + +`spanRows: true` merges adjacent rows whose values are the same value, compared with `Object.is`. Nullish values never merge under the default comparison, since a merged block of blanks reads as a rendering bug and joins semantically unrelated rows. + +Pass a predicate to control run boundaries yourself. The run is anchored: every candidate row is tested against the run's first row, which keeps runs transitive by construction. + +```ts +columnHelper.accessor('createdAt', { + spanRows: ({ anchorValue, value }) => + sameMonth(anchorValue as Date, value as Date), +}) +``` + +### Rendering Spanned Cells + +A covered cell reports a span of `0`, and the renderer skips it. This is the same convention as [`header.rowSpan`](../../../guide/headers#header-row-spanning). + +```html + + + +``` + +`cell.getIsCovered()` is a convenience for the same check, so `x-if="!cell.getIsCovered()"` also works when you do not need the span numbers separately. + +### Column Spanning and Summary Rows + +Declare horizontal spans with `spanColumns` on the column that should carry the merged content. The count is resolved per row and measured in the order columns actually render, so hidden columns are not counted and column reordering is handled for you. + +```ts +columnHelper.accessor('label', { + spanColumns: ({ row }) => (row.original.isSummary ? Infinity : 1), +}) +``` + +Values larger than the available room are clamped to the end of the cell's pinned region, so `Infinity` means "the rest of my region". A column span can never cross the boundary between start-pinned, center, and end-pinned columns. + +When a cell spans rows and columns at once, the merged block is a rectangle: the anchor cell reports both spans and every other cell in the rectangle reports `0` on at least one axis. Cells only join a vertical run when their column spans match, so a full-width summary row never merges into the data run above it. + +### Spanning and Sorting, Filtering, and Pagination + +Spans are derived from the final row model, never stored, so every row model change recomputes them: + +- Sorting changes adjacency. Sorting by the spanned column clusters equal values and produces the largest runs; sorting by an unrelated column usually shatters them. +- Filtering removes rows. When a filter removes the middle of a run, the remaining neighbors become adjacent and merge. +- Pagination clips runs. A run never crosses a page boundary; the next page opens a fresh cell even when the value continues. +- Pinned rows render in separate sections, so a run never crosses a pinned section boundary either. + +### Disable Cell Spanning + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableCellSpanning: false, // document-wide kill switch +}) + +columnHelper.accessor('status', { + enableCellSpanning: false, // per-column opt out +}) +``` + +### Selecting Merged Cells + +`cellSelectionFeature` composes with cell spanning. When both features are registered, a selection rectangle expands to fully enclose every merged cell it touches, so a merge is always entirely selected or entirely unselected. This applies to subtractions too: excluding any part of a merge deselects the whole merge. Arrow-key navigation treats a merge as a single stop, `getSelectedCellCount()` counts a merge once, and `getSelectedCellIds()` returns only the cells that render. `getSelectedCellRangesData()` still returns the full row-major lattice grid, since covered cells carry real underlying values. + +The expansion happens when the selection bounds are derived, not when the selection is stored. Stored corners stay stable while sorting, paging, or toggling `enableCellSpanning` changes which cells merge; the derived selection follows the current spans. + +### Known Limitations + +- Row virtualization needs extra care: if a run's anchor row is scrolled out of the rendered window, the covered rows render nothing. Read `table.getCellSpanIndex()` to find the anchor and render a clamped span at the top of the window. +- Grouped columns ignore `spanRows`, since grouping already collapses repeated values into group rows, and grouped rows never join a run in any column. +- Footer groups and `` rendering are unaffected by cell spanning. diff --git a/docs/framework/alpine/guide/column-faceting.md b/docs/framework/alpine/guide/column-faceting.md new file mode 100644 index 0000000000..8946b1f883 --- /dev/null +++ b/docs/framework/alpine/guide/column-faceting.md @@ -0,0 +1,351 @@ +--- +title: Faceting (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Faceted Filters](../examples/filters-faceted) +- [Bucketed Faceted Filters](../examples/filters-faceted-bucketed) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Faceting Setup + +Here's how you set up your table to use faceting features. Adding the faceting feature enables the related APIs. If you use client-side faceting, also set up `filteredRowModel` and `facetedRowModel` after their features, since row model slots are type-checked. + +```ts +import { + columnFacetingFeature, + columnFilteringFeature, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createTable, + filterFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + facetedRowModel: createFacetedRowModel(), // if using client-side faceting + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + filterFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Faceting (Alpine) Guide + +### What is Faceting? + +Faceting derives information that can be used to build filtering interfaces. For a given column, faceting can answer questions such as: + +- Which values are available? +- How often does each value occur? +- What is the minimum and maximum value among the available rows? +- Which rows should be used for a custom facet calculation? + +For example, an application could use faceting to render a plan filter like this: + +```text +Plan +☐ Free 128 +☐ Pro 47 +☐ Enterprise 9 +``` + +The plan names and counts are derived from the table's faceted row model. If a filter on another column changes, such as `Region = Europe`, the plan counts update to describe only the rows in that region. + +Faceting does not apply filters to the table. It provides values, counts, ranges, or rows that you can use to build a filter UI. The column filtering feature owns the filter state and determines which rows match the selected filter values. + +#### Faceting vs Row Aggregation + +Faceting and row aggregation both summarize data, but they serve different purposes. Faceting produces metadata for filter controls, such as available values, occurrence counts, or a numeric range. Row aggregation computes result values over a set of rows, such as a sum, average, or total, for display in footers or grouped rows. + +Faceted counts do not create aggregate rows or use a column's `aggregationFn`. A useful way to distinguish the features is: + +- Filtering answers: Which rows remain? +- Faceting answers: Which filtering choices remain? +- Row aggregation answers: What summary value can be calculated from these rows? + +### How Column Faceting Responds to Filters + +A column's faceted row model includes rows that pass every applicable filter except that column's own filter. This lets a facet continue to show alternative choices while the user edits it. + +Consider a table with `Region` and `Plan` filters: + +1. The user selects `Region = Europe`. +2. The `Plan` facet applies the region filter and recalculates its plan counts. +3. The user selects `Plan = Pro`. +4. The table displays only European Pro rows. +5. The `Plan` facet still calculates its choices from all European rows because it excludes its own `Plan` filter. + +Other facets do apply the selected plan filter. For example, a `Status` facet would now describe only European Pro rows. This is what allows multiple facets to narrow each other. + +Client-side faceting needs both `filteredRowModel` and `facetedRowModel` to provide this behavior. Without a filtered row model, the faceted row model falls back to the pre-filtered rows, so its values will not react to other column filters. + +### Faceting APIs + +Use the faceting API that matches the filter interface you are building: + +| API | Result | Common uses | +| --------------------------------- | --------------------------------------- | ------------------------------------------------------ | +| `column.getFacetedRowModel()` | Rows that pass the other active filters | Custom facet calculations | +| `column.getFacetedUniqueValues()` | A `Map` of values to occurrence counts | Checkboxes, select menus, and autocomplete suggestions | +| `column.getFacetedMinMaxValues()` | A `[min, max]` tuple or `undefined` | Number inputs and range sliders | + +The row model factories registered in `tableFeatures` enable these APIs: + +- `createFacetedRowModel()` is required for client-side faceting. +- `createFacetedUniqueValues()` is required for unique values and counts. +- `createFacetedMinMaxValues()` is required for numeric minimum and maximum values. + +Register only the factories your table uses. The complete setup near the top of this guide registers all three. + +### Unique Values and Counts + +`column.getFacetedUniqueValues()` returns a `Map` whose keys are facet values and whose values are occurrence counts. You can turn that map into a sorted list for an autocomplete or select control: + +```ts +const suggestions = Array.from(column.getFacetedUniqueValues().entries()) + .sort(([valueA], [valueB]) => String(valueA).localeCompare(String(valueB))) + .slice(0, 5_000) +``` + +Each entry contains both the value and its count: + +```html + +``` + +For a scalar column, each row normally contributes one value, so the occurrence count is also a row count. A row can contribute more than one facet value by defining the column's `getUniqueValues` option. In that case, the counts describe occurrences and their total can be greater than the number of rows. + +```ts +columnHelper.accessor('tags', { + header: 'Tags', + getUniqueValues: (row) => row.tags, +}) +``` + +If you want each count to represent rows, make sure `getUniqueValues` returns each value no more than once per row. + +### Reactive Facet Controls in Alpine + +Expose facet reads as methods on the object returned by `Alpine.data`, then call those methods from Alpine bindings. The bindings reevaluate when the adapter's reactive table state changes. + +```ts +Alpine.data('table', () => { + const table = createTable({ + // ... + }) + + return { + table, + facetValues(column) { + return Array.from(column.getFacetedUniqueValues().entries()) + }, + facetSelected(column, value) { + return (column.getFilterValue() ?? []).includes(value) + }, + toggleFacet(column, value) { + const selected = column.getFilterValue() ?? [] + column.setFilterValue( + selected.includes(value) + ? selected.filter((item) => item !== value) + : [...selected, value], + ) + }, + } +}) +``` + +```html + +``` + +The filter function for the column still determines how the selected values match rows. See the [Column Filtering Guide](./column-filtering) for filter functions and filter state, or the [Faceted Filters example](../examples/filters-faceted) for a complete implementation. + +### Minimum and Maximum Values + +`column.getFacetedMinMaxValues()` returns the numeric range available after applying the other active filters. It returns `undefined` when there are no numeric values. + +```ts +facetRange(column) { + return column.getFacetedMinMaxValues() ?? [0, 1] +} +``` + +```html + +``` + +The minimum and maximum describe the values that are available to the filter UI. Your column's filter function determines how a selected value or range filters rows. + +### Bucketed Faceting for Continuous Values + +Raw unique values are not always useful. Dates, file sizes, durations, prices, and measurements can produce hundreds or thousands of distinct values. These columns are often easier to filter when their values are placed into meaningful buckets: + +```text +Last login +☐ Today +☐ Yesterday +☐ This week +☐ This month +☐ Older +``` + +You can use the column's `getUniqueValues` option to return a bucket key for faceting while keeping the original accessor value for rendering and other table features. + +```ts +type StorageBucket = + 'under-1-gb' | '1-to-10-gb' | '10-to-100-gb' | '100-gb-plus' + +const GB = 1024 ** 3 + +function getStorageBucket(value: number): StorageBucket { + if (value < GB) return 'under-1-gb' + if (value < 10 * GB) return '1-to-10-gb' + if (value < 100 * GB) return '10-to-100-gb' + return '100-gb-plus' +} + +const storageBucketFilter = constructFilterFn({ + resolveDataValue: (value) => getStorageBucket(value as number), + filter: (bucket, selected: Array) => selected.includes(bucket), + autoRemove: (selected: Array) => selected.length === 0, +}) + +const storageColumn: ColumnDef = { + accessorKey: 'storageBytes', + header: 'Storage', + getUniqueValues: (row) => [getStorageBucket(row.storageBytes)], + filterFn: storageBucketFilter, +} +``` + +Faceting and filtering should use the same bucket definitions so the displayed counts match the rows selected by each bucket. The column keeps its raw numeric value, so there is no need to create a hidden derived column only for faceting. See the [Bucketed Faceted Filters example](../examples/filters-faceted-bucketed) for complete date and storage bucket filters. + +### Client-Side Faceting and Performance + +The built-in client-side faceting row models are memoized. They recalculate when their input rows or relevant filter state changes. The cost still depends on the number of rows, columns, and unique values in the table. + +For columns with many unique values, consider these options: + +- Render only the first or most relevant values instead of every map entry. +- Let users search the available values before rendering a long list. +- Bucket continuous or high-cardinality values into useful ranges. +- Move faceting to the server when the complete dataset is not available in the browser. + +Avoid sorting or converting a large facet map repeatedly in unrelated components. Derive and render facet options close to the component that subscribes to the relevant filter state. + +### Custom Server-Side Faceting + +When filtering is performed on the server, the rows loaded into the browser may not contain enough information to calculate complete facet values or counts. In that case, calculate the facets on the server and provide custom `facetedUniqueValues` and `facetedMinMaxValues` factories. + +Each factory receives the table and a column ID, then returns a function that resolves the faceted result. The regular column APIs will return the server-provided values. + +Factories are resolved once per table and column, but the function each factory returns runs on every read; the table does not cache its result. Read live values inside that returned function (from a signal, store, or `table.options.meta`) so updated server facets show up immediately, and memoize inside the factory if the calculation is expensive. + +```ts +// `local` is your Alpine.reactive state, refreshed when facets arrive +async function loadFacets() { + local.serverFacets = await fetch('/api/faceting').then((res) => res.json()) +} + +const features = tableFeatures({ + columnFacetingFeature, + // The returned functions run on every read and table.options stays in + // sync with the latest render, so read live data through options.meta + facetedUniqueValues: (table, columnId) => () => { + const serverFacets = table.options.meta?.serverFacets + return new Map(serverFacets?.uniqueValues[columnId] ?? []) + }, + facetedMinMaxValues: (table, columnId) => () => { + return table.options.meta?.serverFacets?.minMaxValues[columnId] + }, +}) + +const table = createTable({ + features, + columns, + get meta() { + return { serverFacets: local.serverFacets } + }, + get data() { + return local.data + }, +}) +``` + +To match the built-in column faceting behavior, a server query for one column should apply the other active filters but exclude that column's own filter. This keeps alternative choices available in the current facet while allowing facets to narrow each other. + +You can also fetch facet values and pass them directly to your filter components without using the TanStack Table faceting APIs. + +### Global Faceting + +Global faceting derives values across every leaf column that can participate in global filtering. It is useful for autocomplete suggestions or other metadata associated with a global filter. The global faceted row model applies active column filters and excludes the global filter itself. + +If the table uses global filtering, register `globalFilteringFeature` so the row filtering pipeline evaluates the global filter. The same faceting factories used by column faceting also power these table APIs: + +```ts +const globalFacetedRows = table.getGlobalFacetedRowModel().flatRows + +const suggestions = Array.from(table.getGlobalFacetedUniqueValues().entries()) + +const [min, max] = table.getGlobalFacetedMinMaxValues() ?? [0, 1] +``` + +Custom faceting factories receive the internal `__global__` column ID for global requests. You can branch on that ID when the server returns separate column and global facet results: + +```ts +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + if (columnId === '__global__') { + return new Map(globalFacets.uniqueValues) + } + + return new Map(columnFacets[columnId]?.uniqueValues) + }, +}) +``` diff --git a/docs/framework/alpine/guide/column-filtering.md b/docs/framework/alpine/guide/column-filtering.md new file mode 100644 index 0000000000..c98ef242ec --- /dev/null +++ b/docs/framework/alpine/guide/column-filtering.md @@ -0,0 +1,553 @@ +--- +title: Column Filtering (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Bucketed Faceted Filters](../examples/filters-faceted-bucketed) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Filtering Setup + +Here's how you set up your table to use column filtering features. Adding the column filtering feature enables the related APIs. If you use client-side filtering, also set up `filteredRowModel` after its feature, since row model slots are type-checked. + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createTable, + filterFn_includesString, + filterFn_inNumberRange, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +> [!NOTE] +> The `filterFns` registry above lists only the built-in filter functions this table uses. Spreading the entire built-in `filterFns` registry (`filterFns: { ...filterFns }`) still works, but it puts every built-in filter function in your bundle. Register just the functions you use, or pass a function directly to the `filterFn` column option with no registration at all. + +## Column Filtering (Alpine) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on column filtering, which is a filter that is applied to a single column's accessor value. + +TanStack table supports both client-side and manual server-side filtering. This guide will go over how to implement and customize both, and help you decide which one is best for your use-case. + +### Client-Side vs Server-Side Filtering + +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. + +The client-side filtered row model also invokes the page-index auto-reset hook when column filtering inputs change. Whether the page index resets depends on the `autoResetPageIndex`, `autoResetAll`, and `manualPagination` options. If filtering is manual and this row model is omitted or bypassed, a column filter state change does not invoke that hook, so reset server-side pagination in the filter change handler when needed. + +### Manual Server-Side Filtering + +If you have decided that you need to implement server-side filtering instead of using the built-in client-side filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `tableFeatures`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +const features = tableFeatures({ columnFilteringFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + manualFiltering: true, +}) +``` + +> [!NOTE] +> When using manual filtering, many of the options that are discussed in the rest of this guide will have no effect. When `manualFiltering` is set to `true`, the table instance will not apply any filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the `data` that you pass to it as-is. + +### Client-Side Filtering + +If you are using the built-in client-side filtering features, add the `columnFilteringFeature` and the `filteredRowModel` factory to your features. Import `createFilteredRowModel` and the filter functions you need from TanStack Table: + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createTable, + filterFn_includesString, + filterFn_inNumberRange, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Column Filter State + +Whether or not you use client-side or server-side filtering, you can take advantage of the built-in column filter state management that TanStack Table provides. There are many table and column APIs to mutate and interact with the filter state and retrieve the column filter state. + +The column filtering state is defined as an array of objects with the following shape: + +```ts +interface ColumnFilter { + id: string + value: unknown +} +type ColumnFiltersState = ColumnFilter[] +``` + +Since the column filter state is an array of objects, you can have multiple column filters applied at once. + +#### Accessing Column Filter State + +The table's state atoms are reactive in Alpine. `table.atoms.columnFilters.get()` is a reactive read when used inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. `table.store.get()` returns a current full-state snapshot, useful for debugging. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... +}) + +table.atoms.columnFilters.get() // reactive read inside Alpine bindings, plain read elsewhere +``` + +However, if you need access to the column filter state outside of the table, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state in other parts of your application, you can own the column filter state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The filter values can be read, written, or subscribed to elsewhere (such as in a query key for server-side filtering) without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' + +const columnFiltersAtom = createAtom([]) // can set initial column filter state here + +// subscribe to the atom wherever you need the value (e.g. for a query key) +columnFiltersAtom.subscribe(() => { + // react to filter changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... + atoms: { + columnFilters: columnFiltersAtom, // table filter APIs now update columnFiltersAtom + }, +}) +``` + +Alternatively, the v8-style `state.columnFilters` plus `onColumnFiltersChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ columnFilters: [] as ColumnFiltersState }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... + state: { + get columnFilters() { + return local.columnFilters // connect the reactive slice back down to the table + }, + }, + onColumnFiltersChange: (updater) => { + local.columnFilters = + typeof updater === 'function' ? updater(local.columnFilters) : updater + }, +}) +``` + +#### Initial Column Filter State + +If you do not need to control the column filter state in your own state management or scope, but you still want to set an initial column filter state, you can use the `initialState` table option instead of `state`. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +}) +``` + +> [!NOTE] +> Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 18 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `startsWith` - Case-insensitive string prefix match +- `endsWith` - Case-insensitive string suffix match +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `empty` - The row's value is nullish or whitespace-only (the filter value is an on/off flag) +- `notEmpty` - The row's value is not nullish or whitespace-only (the filter value is an on/off flag) +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `inDateRange` - Inclusive `[min, max]` date range accepting `Date` objects, timestamps, or date strings (blank endpoints are open-ended) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom filter functions, either inline as the `filterFn` column option, or by name in the `filterFns` registry slot on `tableFeatures`. + +#### Custom Filter Functions + +> [!NOTE] +> These filter functions only run during client-side filtering. + +Whether you register a custom filter function in the `filterFns` slot on `tableFeatures` or pass it directly as a `filterFn` column option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = ( + row, // Row + columnId: string, + filterValue: any, + addMeta?: (meta: FilterMeta) => void, +): boolean => ... +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // reference a custom filter function registered in features + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + }, +] +//... +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +> **TypeScript Note:** For `filterFn: 'myCustomFilterFn'` string references to typecheck, register the function in the `filterFns` slot on `tableFeatures` (as shown above). The slot is the registry; no `declare module` augmentation is needed. Alternatively, skip the registry entirely by passing the function directly to the `filterFn` column option. + +##### Customize Filter Function Behavior + +You can attach a few other properties to filter functions to customize their behavior: + +- `filterFn.resolveFilterValue` - This optional "hanging" method on any given `filterFn` allows the filter function to transform/sanitize/format the filter value before it is passed to the filter function. The table applies it once per filter (not once per row), so it is also the right place for expensive preparation work. + +- `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. + +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. + +The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: + +```ts +const startsWithFilterFn = constructFilterFn({ + // compare the (resolved) row value against the (resolved) filter value + filter: (dataValue, filterValue) => + Boolean(dataValue?.startsWith(filterValue)), + // normalize the filter value once, before any rows are tested + resolveFilterValue: (value) => String(value).toLowerCase().trim(), + // normalize each row's value before it reaches the comparator + resolveDataValue: (value) => String(value ?? '').toLowerCase(), + // remove the filter value from filter state if it is falsy (empty string in this case) + autoRemove: (value) => !value, +}) +``` + +Keeping the comparison in `filter` and the normalization in the resolvers pays off when you need a variant of an existing filter function. The definition is attached to the returned function, so you can spread any filter function built with `constructFilterFn` and override only what differs. For example, a version of `includesString` that also ignores diacritics (so a search for "eric" matches "Éric"): + +```ts +const normalize = (value: unknown) => + String(value ?? '') + .toLowerCase() + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + +const includesStringIgnoreDiacritics = constructFilterFn({ + ...filterFn_includesString, // reuse the comparator and autoRemove behavior + resolveFilterValue: normalize, + resolveDataValue: normalize, +}) +``` + +Register the variant by name in the `filterFns` registry or pass it directly to the `filterFn` column option, just like any other custom filter function. + +> [!NOTE] +> The table applies `resolveFilterValue` once per filter before any rows are tested. If you ever call a filter function directly (outside of a table), resolve the filter value yourself: `myFilterFn(row, columnId, myFilterFn.resolveFilterValue?.(rawValue) ?? rawValue)`. + +### Wiring up the filter UI + +TanStack Table will not add filter inputs to your table. Add them yourself on real elements (Alpine does not initialize directives inside content set with `x-html`). Read the current value with `column.getFilterValue()` and write it with `column.setFilterValue()`. Use `column.getCanFilter()` to decide whether to render an input. + +```html + + + +``` + +For a numeric range filter, store a `[min, max]` tuple and update each bound separately. Reading and writing each bound is small enough to keep inline, or you can expose helpers on your `Alpine.data` object. + +```ts +Alpine.data('table', () => { + // ...createTable as above + return { + table, + FlexRender, + rangeValue(column, index) { + return column.getFilterValue()?.[index] ?? '' + }, + setRangeMin(column, value) { + column.setFilterValue((old) => [ + value === '' ? undefined : Number(value), + old?.[1], + ]) + }, + setRangeMax(column, value) { + column.setFilterValue((old) => [ + old?.[0], + value === '' ? undefined : Number(value), + ]) + }, + } +}) +``` + +```html +
+ + +
+``` + +### Customize Column Filtering + +There are a lot of table and column options that you can use to further customize the column filtering behavior. + +#### Disable Column Filtering + +By default, column filtering is enabled for all columns. You can disable the column filtering for all columns or for specific columns by using the `enableColumnFilters` table option or the `enableColumnFilter` column option. You can also turn off both column and global filtering by setting the `enableFilters` table option to `false`. + +Disabling column filtering for a column will cause the `column.getCanFilter` API to return `false` for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableColumnFilters: false, // disable column filtering for all columns +}) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + filterFromLeafRows: true, // filter and search through sub-rows +}) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +}) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `input` or `change` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/alpine/guide/column-ordering.md b/docs/framework/alpine/guide/column-ordering.md new file mode 100644 index 0000000000..85f0501ab1 --- /dev/null +++ b/docs/framework/alpine/guide/column-ordering.md @@ -0,0 +1,196 @@ +--- +title: Column Ordering (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Ordering](../examples/column-ordering) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Ordering Setup + +Here's how you set up your table to use column ordering features. Adding the column ordering feature enables the related APIs. + +```ts +import { + createTable, + tableFeatures, + columnOrderingFeature, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Ordering (Alpine) Guide + +By default, columns are ordered in the order they are defined in the `columns` array. However, you can manually specify the column order using the `columnOrder` state. Other features like column pinning and grouping can also affect the column order. + +### What Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual **Column Ordering** - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +> [!NOTE] +> `columnOrder` state will only affect unpinned columns if used in conjunction with column pinning. + +### Column Order State + +If you don't provide a `columnOrder` state, TanStack Table will just use the order of the columns in the `columns` array. However, you can provide an array of string column ids to the `columnOrder` state to specify the order of the columns. + +#### Default Column Order + +If all you need to do is specify the initial column order, you can just specify the `columnOrder` state in the `initialState` table option. + +```ts +const features = tableFeatures({ columnOrderingFeature }) + +const table = createTable({ + features, + //... + initialState: { + columnOrder: ['columnId1', 'columnId2', 'columnId3'], + }, + //... +}) +``` + +> [!NOTE] +> If you are using the `state` table option to also specify the `columnOrder` state, the `initialState` will have no effect. Only specify particular states in either `initialState` or `state`, not both. + +#### Managing Column Order State + +If you need to dynamically change the column order, or set the column order after the table has been initialized, you can manage the `columnOrder` state just like any other table state. + +In v9, the recommended way to own a state slice is with an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the column order without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import { + createTable, + tableFeatures, + columnOrderingFeature, +} from '@tanstack/alpine-table' +import type { ColumnOrderState } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const columnOrderAtom = createAtom([ + 'columnId1', + 'columnId2', + 'columnId3', +]) + +// subscribe wherever it is needed +columnOrderAtom.subscribe(() => { + // react to column order changes +}) + +const table = createTable({ + features, + //... + atoms: { + columnOrder: columnOrderAtom, + }, + //... +}) +``` + +Alternatively, the v8-style `state.columnOrder` plus `onColumnOrderChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnOrderingFeature }) + +const local = Alpine.reactive({ + columnOrder: ['columnId1', 'columnId2', 'columnId3'] as ColumnOrderState, +}) +//... +const table = createTable({ + features, + //... + state: { + get columnOrder() { + return local.columnOrder // connect the reactive slice back down to the table + }, + //... + }, + onColumnOrderChange: (updater) => { + local.columnOrder = + typeof updater === 'function' ? updater(local.columnOrder) : updater + }, + //... +}) +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, hook the drop event of your drag-and-drop solution up to `table.setColumnOrder`. For example, with native browser drag events on the header cells. Keep the drag state in `Alpine.reactive` so the markup can react to it: + +```ts +const local = Alpine.reactive({ movingColumnId: null as string | null }) + +// move the dragged column in front of the column it was dropped on +function handleDrop(targetColumnId: string) { + const fromId = local.movingColumnId + if (!fromId || fromId === targetColumnId) return + table.setColumnOrder((prevColumnOrder) => { + const newColumnOrder = [...prevColumnOrder] + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + newColumnOrder.splice(newColumnOrder.indexOf(fromId), 1)[0]!, + ) + return newColumnOrder + }) + local.movingColumnId = null +} +``` + +`table.setColumnOrder` works the same whether the table manages the `columnOrder` state internally, you control it with `state` + `onColumnOrderChange`, or you own it with an external atom. The official [Column Ordering example](../examples/column-ordering) calls it with a full array of leaf column ids. + +### Column Ordering APIs + +Use `table.setColumnOrder` to update the column order state directly. Use `table.resetColumnOrder` to reset the order to `initialState.columnOrder`, or pass `true` to clear the order state. + +```ts +table.setColumnOrder(['lastName', 'firstName', 'age']) +table.resetColumnOrder() +table.resetColumnOrder(true) +``` + +Columns expose helpers for reading their current position after column pinning, manual ordering, and grouping have been applied. + +```ts +column.getIndex() +column.getIndex('start') +column.getIndex('center') +column.getIndex('end') + +column.getIsFirstColumn() +column.getIsLastColumn() +``` + +These helpers are useful for styling column boundaries or building drag-and-drop targets that need to know the current rendered order. + +#### Drag and Drop Column Reordering Suggestions (Alpine) + +TanStack Table is not opinionated about which drag-and-drop solution you use. Here are a few suggestions: + +1. Consider native browser drag events (`@dragstart`, `@dragenter`, `@dragend`) with your own `Alpine.reactive` state if you want zero dependencies. This can be very lightweight, but you will need to do extra work for proper touch support on mobile. [Material React Table](https://www.material-react-table.com/docs/examples/column-ordering) implements TanStack Table column ordering this way with no DnD dependencies; the approach translates directly to Alpine since it is just DOM events feeding `table.setColumnOrder`. + +2. If you want a library, look at framework-agnostic options such as Atlassian's [Pragmatic drag and drop](https://atlassian.design/components/pragmatic-drag-and-drop/about). Check maintenance status, bundle size, and how well they handle semantic `` markup before committing. + +3. Do NOT reach for React-only DnD libraries (including DnD Kit's `@dnd-kit/*` packages). They depend on React's component model and do not work with Alpine. diff --git a/docs/framework/alpine/guide/column-pinning.md b/docs/framework/alpine/guide/column-pinning.md new file mode 100644 index 0000000000..e82ae63317 --- /dev/null +++ b/docs/framework/alpine/guide/column-pinning.md @@ -0,0 +1,243 @@ +--- +title: Column Pinning (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Pinning Setup + +Here's how you set up your table to use column pinning features. Adding the column pinning feature enables the related APIs. + +```ts +import { + createTable, + tableFeatures, + columnPinningFeature, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ columnPinningFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Pinning (Alpine) Guide + +TanStack Table offers state and APIs helpful for implementing column pinning features in your table UI. You can implement column pinning in multiple ways. You can either split pinned columns into their own separate tables, or you can keep all columns in the same table, but use the pinning state to order the columns correctly and use sticky CSS to pin the columns to the start or end. + +`start` and `end` are logical pinning regions. In LTR languages/layouts, `start` usually corresponds to left and `end` to right. In RTL languages/layouts, `start` usually corresponds to right and `end` to left. + +### How Column Pinning Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. **Column Pinning** - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +The only way to change the order of the pinned columns is in the `columnPinning.start` and `columnPinning.end` state itself. `columnOrder` state will only affect the order of the unpinned ("center") columns. + +### Column Pinning State + +Managing the `columnPinning` state is optional, and usually not necessary unless you are adding persistent state features. TanStack Table will already keep track of the column pinning state for you. Manage the `columnPinning` state just like any other table state if you need to. + +In v9, the recommended way to own a state slice is with an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import { + createTable, + tableFeatures, + columnPinningFeature, +} from '@tanstack/alpine-table' +import type { ColumnPinningState } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnPinningFeature }) + +const columnPinningAtom = createAtom({ + start: [], + end: [], +}) + +// subscribe wherever it is needed +columnPinningAtom.subscribe(() => { + // react to pinning changes +}) + +const table = createTable({ + features, + //... + atoms: { + columnPinning: columnPinningAtom, + }, + //... +}) +``` + +Alternatively, the v8-style `state.columnPinning` plus `onColumnPinningChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + columnPinning: { start: [], end: [] } as ColumnPinningState, +}) + +const table = createTable({ + features, + //... + state: { + get columnPinning() { + return local.columnPinning // connect the reactive slice back down to the table + }, + //... + }, + onColumnPinningChange: (updater) => { + local.columnPinning = + typeof updater === 'function' ? updater(local.columnPinning) : updater + }, + //... +}) +``` + +### Pin Columns by Default + +A very common use case is to pin some columns by default. You can do this by either initializing the `columnPinning` state with the pinned columnIds, or by using the `initialState` table option: + +```ts +const table = createTable({ + features, + //... + initialState: { + columnPinning: { + start: ['expand-column'], + end: ['actions-column'], + }, + //... + }, + //... +}) +``` + +### Useful Column Pinning APIs + +> [!NOTE] +> These APIs are available when using `columnPinningFeature`. + +There are a handful of useful Column API methods to help you implement column pinning features: + +- `column.getCanPin`: Use to determine if a column can be pinned. +- `column.pin`: Use to pin a column to the start or end. Or use to unpin a column. +- `column.getIsPinned`: Use to determine where a column is pinned. +- `column.getPinnedIndex`: Use to read the column's index within its pinned column group. +- `column.getStart`: Use to provide the correct `start` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `end` CSS value for a pinned column. +- `column.getIsLastColumn`: Use to determine if a column is the last column in its pinned group. Useful for adding a box-shadow. +- `column.getIsFirstColumn`: Use to determine if a column is the first column in its pinned group. Useful for adding a box-shadow. + +Use `table.setColumnPinning` to update the pinning state directly. Use `table.resetColumnPinning` to reset to `initialState.columnPinning`, or pass `true` to clear both pinned column arrays. + +```ts +table.setColumnPinning({ + start: ['firstName'], + end: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```ts +table.getStartLeafColumns() +table.getCenterLeafColumns() +table.getEndLeafColumns() + +table.getStartVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getEndVisibleLeafColumns() + +table.getStartHeaderGroups() +table.getCenterHeaderGroups() +table.getEndHeaderGroups() + +table.getStartFooterGroups() +table.getCenterFooterGroups() +table.getEndFooterGroups() + +table.getStartFlatHeaders() +table.getCenterFlatHeaders() +table.getEndFlatHeaders() + +table.getStartLeafHeaders() +table.getCenterLeafHeaders() +table.getEndLeafHeaders() +``` + +You can also request pinned leaf columns by region with `table.getPinnedLeafColumns(position)` and visible pinned leaf columns with `table.getPinnedVisibleLeafColumns(position)`. + +```ts +table.getPinnedLeafColumns('start') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('end') + +table.getPinnedVisibleLeafColumns('start') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('end') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'start'` or `'end'` to check one pinned side. + +### Wiring up the pinning UI + +Because Alpine does not initialize directives inside content set with `x-html`, render the header content with `x-html="FlexRender({ header })"` and attach the pin handlers to real ` +``` + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getStartHeaderGroups`, `table.getCenterHeaderGroups`, `table.getEndHeaderGroups`, `row.getStartVisibleCells`, `row.getCenterVisibleCells`, and `row.getEndVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/alpine/guide/column-resizing.md b/docs/framework/alpine/guide/column-resizing.md new file mode 100644 index 0000000000..822b09b1b7 --- /dev/null +++ b/docs/framework/alpine/guide/column-resizing.md @@ -0,0 +1,408 @@ +--- +title: Column Resizing (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Resizing Setup + +Here's how you set up your table to use column resizing features. Column resizing depends on column sizing, so add `columnSizingFeature` before `columnResizingFeature`. Adding the column resizing feature enables the related APIs. + +```ts +import { + createTable, + tableFeatures, + columnSizingFeature, + columnResizingFeature, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Resizing (Alpine) Guide + +TanStack Table provides built-in column resizing state and APIs for implementing column resizing in your table UI with a variety of options for UX and performance. + +Column resizing builds on column sizing. If you only need to define starting, minimum, or maximum widths, see the [Column Sizing Guide](./column-sizing). + +### Enable Column Resizing + +To use column resizing, add `columnSizingFeature` and then `columnResizingFeature` to your features. The `column.getCanResize()` API will return `true` by default for all columns, but you can either disable column resizing for all columns with the `enableColumnResizing` table option, or disable column resizing on a per-column basis with the `enableResizing` column option. + +```ts +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + createTable, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns = [ + { + accessorKey: 'id', + enableResizing: false, // disable resizing for just this column + size: 200, // starting column size + }, + //... +] + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Column Resize Mode + +By default, the column resize mode is set to `"onEnd"`. This means that the `column.getSize()` API will not return the new column size until the user has finished resizing (dragging) the column. Usually a small UI indicator will be displayed while the user is resizing the column. + +The `"onEnd"` default exists because immediate resize updates can be expensive in large or complex tables: every drag movement updates the `columnSizing` state, and anything that reads column widths recomputes. Alpine's per-binding reactivity helps here, since only the bindings that actually read the sizing state re-run, but if every header and cell reads `column.getSize()` directly, a complex table can still stutter during an `"onChange"` drag. The `"onEnd"` mode sidesteps this by deferring the size update until the drag finishes. + +> Advanced column resizing performance tips will be discussed [down below](#advanced-column-resizing-performance). + +If you want to change the column resize mode to `"onChange"` for immediate column resizing renders, you can do so with the `columnResizeMode` table option. + +```ts +const table = createTable({ + //... + columnResizeMode: 'onChange', // change column resize mode to "onChange" +}) +``` + +### Column Resize Direction + +By default, TanStack Table assumes that the table markup is laid out in a left-to-right direction. For right-to-left layouts, you may need to change the column resize direction to `"rtl"`. + +```ts +const table = createTable({ + //... + columnResizeDirection: 'rtl', // change column resize direction to "rtl" for certain locales +}) +``` + +### Connect Column Resizing APIs to UI + +There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI. + +#### Column Size APIs + +To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs: + +```ts +header.getSize() +column.getSize() +cell.column.getSize() +``` + +How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes. Because table reads are reactive inside Alpine bindings, a `:style` that reads `header.getSize()` updates automatically as the size changes: + +```html + +``` + +Though, as discussed in the [advanced column resizing performance section](#advanced-column-resizing-performance), you may want to consider using CSS variables to apply column sizes to your markup. + +#### Column Resize APIs + +TanStack Table provides a pre-built event handler to make your drag interactions easy to implement. These event handlers are just convenience functions that call other internal APIs to update the column sizing state and re-render the table. Use `header.getResizeHandler()` to connect to your column resize drag interactions, for both mouse and touch events. The handler is returned by `getResizeHandler()` and called with the event, so the pattern in markup is `header.getResizeHandler()($event)`. + +```html +
+``` + +#### Column Resize Indicator with Column Resizing State + +TanStack Table keeps track of a `columnResizing` state object that you can use to render a column resize indicator UI. Read it with `table.atoms.columnResizing.get()`. The `:style` binding that uses `header.column.getIsResizing()` and the resizing state stays reactive, so the indicator follows the drag. + +When using the `"onEnd"` resize mode, the size only updates when the drag finishes, so you translate the resize handle by the live `deltaOffset` while dragging. A method on your `Alpine.data` object is a convenient place to compute that transform: + +```ts +Alpine.data('table', () => { + const local = Alpine.reactive({ + data: makeData(10), + columnResizeMode: 'onEnd' as ColumnResizeMode, + columnResizeDirection: 'ltr' as ColumnResizeDirection, + }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + get columnResizeMode() { + return local.columnResizeMode + }, + get columnResizeDirection() { + return local.columnResizeDirection + }, + }) + + return { + table, + FlexRender, + local, + // Translate the resizer while dragging when using the "onEnd" resize mode. + resizerTransform(header: any) { + if (local.columnResizeMode === 'onEnd' && header.column.getIsResizing()) { + const delta = table.atoms.columnResizing.get().deltaOffset ?? 0 + const dir = local.columnResizeDirection === 'rtl' ? -1 : 1 + return `transform: translateX(${dir * delta}px)` + } + return '' + }, + } +}) +``` + +```html +
+``` + +This is the same pattern the [Column Resizing example](../examples/column-resizing) uses. + +The `columnResizing` state stores transient drag information: + +```ts +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +You rarely need to manage this transient drag state yourself, but if you do, the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can observe the resize state without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import type { columnResizingState } from '@tanstack/alpine-table' + +const columnResizingAtom = createAtom({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +// subscribe wherever it is needed +columnResizingAtom.subscribe(() => { + // react to resize state changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + columnResizing: columnResizingAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnResizing` plus `onColumnResizingChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + columnResizing: { + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, + } as columnResizingState, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get columnResizing() { + return local.columnResizing // connect the reactive slice back down to the table + }, + }, + onColumnResizingChange: (updater) => { + local.columnResizing = + typeof updater === 'function' ? updater(local.columnResizing) : updater + }, +}) +``` + +### Column Resizing APIs + +Use `header.getResizeHandler()` to connect mouse or touch events to the resizing logic. Use `column.getCanResize()` to decide whether to render a resize handle, and `column.getIsResizing()` to render active resizing UI. + +```ts +header.getResizeHandler() +column.getCanResize() +column.getIsResizing() +``` + +The table instance exposes APIs for the transient resize state through `table.setColumnResizing`. + +```ts +table.setColumnResizing((old) => ({ + ...old, + deltaOffset: 12, +})) + +table.resetHeaderSizeInfo() +table.resetHeaderSizeInfo(true) +``` + +### Advanced Column Resizing Performance + +Alpine bridges table reactivity through a single version counter, so by default any table state change re-evaluates every binding that reads the table. During an `"onChange"` drag on a large table that is a lot of work per frame. The [performant column resizing example](../examples/column-resizing-performant) shows how to keep a drag off Alpine's reactivity entirely. + +1. **Opt the table out of state-driven re-evaluation.** Pass a selector to `createTable` that returns a constant (`() => ({})`). Alpine then re-evaluates table bindings only when your data or options change, not on every resize tick. (Data changes such as Regenerate still re-render normally.) +2. **Write column widths as CSS variables imperatively.** In the component's `init()`, subscribe to `table.atoms.columnSizing` and set `--header--size` and `--col--size` variables directly on the `
+
+ +
+ +
` element. Cells reference them with `width: calc(var(--col-firstName-size) * 1px)`, so the browser applies new widths with no Alpine work per frame. Unsubscribe in `destroy()`. +3. **Drive the resizer highlight and any live state readout from subscriptions too**, toggling classes or text imperatively rather than through `x-` bindings. + +The example sets up the subscriptions in `init()` and references the table element with `x-ref`: + +```ts +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(200) }) + + const table = createTable( + { + features, + columns, + get data() { + return local.data + }, + defaultColumn: { minSize: 60, maxSize: 800 }, + columnResizeMode: 'onChange', + }, + () => ({}), // opt out of state-driven re-evaluation; the drag is handled by the subscriptions below + ) + + let subscriptions: Array<{ unsubscribe: () => void }> = [] + + return { + table, + FlexRender, + local, + init(this: { $refs: Record }) { + const tableEl = this.$refs.tableEl + + const writeColumnSizeVars = () => { + for (const header of table.getFlatHeaders()) { + tableEl.style.setProperty( + `--header-${header.id}-size`, + String(header.getSize()), + ) + tableEl.style.setProperty( + `--col-${header.column.id}-size`, + String(header.column.getSize()), + ) + } + tableEl.style.width = `${table.getTotalSize()}px` + } + + writeColumnSizeVars() // initial paint + subscriptions = [table.atoms.columnSizing.subscribe(writeColumnSizeVars)] + }, + destroy() { + subscriptions.forEach((subscription) => subscription.unsubscribe()) + subscriptions = [] + }, + } +}) +``` + +```html +
+ + + + + + +
+``` + +> [!NOTE] +> with the `() => ({})` selector, the `:class` binding on the resizer above will not update during a drag (the table is opted out of state-driven re-evaluation). The example instead toggles the `isResizing` class imperatively from a `table.atoms.columnResizing` subscription. Keeping the `:class` binding is fine if you accept the highlight only reflecting resize state on the next data-driven re-render. + +If you follow these steps, you should see significant performance improvements while resizing columns. diff --git a/docs/framework/alpine/guide/column-sizing.md b/docs/framework/alpine/guide/column-sizing.md new file mode 100644 index 0000000000..9545ecf647 --- /dev/null +++ b/docs/framework/alpine/guide/column-sizing.md @@ -0,0 +1,224 @@ +--- +title: Column Sizing (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Sizing](../examples/column-sizing) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Sizing Setup + +Here's how you set up your table to use column sizing features. Adding the column sizing feature enables the related APIs. + +```ts +import { + createTable, + tableFeatures, + columnSizingFeature, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ columnSizingFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Sizing (Alpine) Guide + +The column sizing feature lets you optionally set the width of each column including min and max widths. + +If you want users to dynamically change column widths by dragging column headers, see the [Column Resizing Guide](./column-resizing). + +### Column Widths + +Columns by default are given the following measurement options: + +```ts +export const defaultColumnSizing = { + size: 150, + minSize: 20, + maxSize: Number.MAX_SAFE_INTEGER, +} +``` + +These defaults can be overridden by both `tableOptions.defaultColumn` and individual column defs, in that order. + +```ts +const features = tableFeatures({ columnSizingFeature }) + +const columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +const table = createTable({ + features, + defaultColumn: { + size: 200, // starting column size + minSize: 50, // enforced during column resizing + maxSize: 500, // enforced during column resizing + }, + //... +}) +``` + +The column "sizes" are stored in the table state as numbers, and are usually interpreted as pixel unit values, but you can hook up these column sizing values to your css styles however you see fit. + +As a headless utility, table logic for column sizing is really only a collection of states that you can apply to your own layouts how you see fit (our example above implements 2 styles of this logic). You can apply these width measurements in a variety of ways: + +- semantic `table` elements or any elements being displayed in a table css mode +- `div/span` elements or any elements being displayed in a non-table css mode + - Block level elements with strict widths + - Absolutely positioned elements with strict widths + - Flexbox positioned elements with loose widths + - Grid positioned elements with loose widths +- Really any layout mechanism that can interpolate cell widths into a table structure. + +Each of these approaches has its own tradeoffs and limitations which are usually opinions held by a UI/component library or design system, luckily not you 😉. + +### Applying Column Sizes + +To apply the calculated size to your markup, read `header.getSize()` or `column.getSize()` inside an Alpine binding. Because table reads are reactive in Alpine bindings, the widths update automatically when the sizing state changes. A common approach is an inline `:style` that interpolates the size into a pixel width. + +```html + + + + + + + +
+``` + +### Column Sizing APIs + +Use the column and header APIs to read the calculated size and offsets for rendering. These values come from the `columnSizing` state and the column definition defaults. + +```ts +column.getSize() +header.getSize() + +column.getStart() // start offset in the current column flow +column.getStart('start') +column.getStart('center') +column.getStart('end') + +column.getAfter() // end offset in the current column flow +column.getAfter('start') +column.getAfter('center') +column.getAfter('end') + +column.resetSize() +``` + +The table instance also exposes total size helpers. These are useful when building scroll containers, split pinned-column tables, or CSS variables for column widths. + +```ts +table.getTotalSize() +table.getStartTotalSize() +table.getCenterTotalSize() +table.getEndTotalSize() +``` + +If you need to update sizing state directly, use `table.setColumnSizing`. Use `table.resetColumnSizing` to reset to `initialState.columnSizing`, or pass `true` to reset to the feature default. + +```ts +table.setColumnSizing({ + firstName: 180, + age: 80, +}) + +table.resetColumnSizing() +table.resetColumnSizing(true) +``` + +### Managing Column Sizing State + +If you need to own the `columnSizing` state yourself (for example, to persist user-set column widths), the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the sizing state without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import type { ColumnSizingState } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnSizingFeature }) + +const columnSizingAtom = createAtom({}) + +// subscribe wherever it is needed +columnSizingAtom.subscribe(() => { + // react to sizing changes (e.g. persist widths) +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + columnSizing: columnSizingAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnSizing` plus `onColumnSizingChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnSizingFeature }) + +const local = Alpine.reactive({ columnSizing: {} as ColumnSizingState }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get columnSizing() { + return local.columnSizing // connect the reactive slice back down to the table + }, + }, + onColumnSizingChange: (updater) => { + local.columnSizing = + typeof updater === 'function' ? updater(local.columnSizing) : updater + }, +}) +``` diff --git a/docs/framework/alpine/guide/column-visibility.md b/docs/framework/alpine/guide/column-visibility.md new file mode 100644 index 0000000000..0d3b2d1bae --- /dev/null +++ b/docs/framework/alpine/guide/column-visibility.md @@ -0,0 +1,219 @@ +--- +title: Column Visibility (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Visibility](../examples/column-visibility) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Visibility Setup + +Here's how you set up your table to use column visibility features. Adding the column visibility feature enables the related APIs. + +```ts +import { + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Visibility (Alpine) Guide + +The column visibility feature allows table columns to be hidden or shown dynamically. In v9, add `columnVisibilityFeature` to your `features` to enable this. There is a dedicated `columnVisibility` state and APIs for managing column visibility dynamically. + +### Column Visibility State + +The `columnVisibility` state is a map of column IDs to boolean values. A column will be hidden if its ID is present in the map and the value is `false`. If the column ID is not present in the map, or the value is `true`, the column will be shown. + +If you need to own the `columnVisibility` state yourself (for example, to persist user preferences), the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the visibility state without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import { + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import type { ColumnVisibilityState } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const columnVisibilityAtom = createAtom({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +// subscribe to the atom wherever you need the value +columnVisibilityAtom.subscribe(() => { + // react to visibility changes +}) + +const table = createTable({ + features, + //... + atoms: { + columnVisibility: columnVisibilityAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnVisibility` plus `onColumnVisibilityChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + } as ColumnVisibilityState, +}) + +const table = createTable({ + features, + //... + state: { + get columnVisibility() { + return local.columnVisibility // connect the reactive slice back down to the table + }, + //... + }, + onColumnVisibilityChange: (updater) => { + local.columnVisibility = + typeof updater === 'function' ? updater(local.columnVisibility) : updater + }, +}) +``` + +Alternatively, if you don't need to manage the column visibility state outside of the table, you can still set the initial default column visibility state using the `initialState` option. + +> [!NOTE] +> If `columnVisibility` is provided to both `initialState` and a controlled option (`atoms` or `state`), the controlled value will take precedence and `initialState` will be ignored. Only provide `columnVisibility` in one place. + +```ts +const features = tableFeatures({ columnVisibilityFeature }) + +const table = createTable({ + features, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +}) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```ts +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessorKey: 'name', // can be hidden + }, +] +``` + +### Column Visibility Toggle APIs + +There are several column API methods that are useful for rendering column visibility toggles in the UI. + +- `column.getCanHide` - Useful for disabling the visibility toggle for a column that has `enableHiding` set to `false`. +- `column.getIsVisible` - Useful for setting the initial state of the visibility toggle. +- `column.toggleVisibility` - Useful for toggling the visibility of a column. +- `column.getToggleVisibilityHandler` - Shortcut for hooking up the `column.toggleVisibility` method to a UI event handler. + +Render a checkbox per column on real elements (not inside `x-html`). Bind `:checked` to `column.getIsVisible()`, `:disabled` to `!column.getCanHide()`, and call the handler returned by `getToggleVisibilityHandler` from `@change`. + +```html + +``` + +A "Toggle All" checkbox can use the table-level helpers `table.getIsAllColumnsVisible()` and `table.getToggleAllColumnsVisibilityHandler()`. + +```html + +``` + +### Column Visibility Aware Table APIs + +When you render your header, body, and footer cells, there are a lot of API options available. You may see APIs like `table.getAllLeafColumns` and `row.getAllCells`, but if you use these APIs, they will not take column visibility into account. Instead, you need to use the "visible" variants of these APIs, such as `table.getVisibleLeafColumns` and `row.getVisibleCells`. + +Render cell and header content with `x-html="FlexRender(...)"`, and use the visible-aware row models when iterating with `x-for`: + +```html + + + + + + + +
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/alpine/guide/composable-tables.md b/docs/framework/alpine/guide/composable-tables.md new file mode 100644 index 0000000000..0cb7db7b36 --- /dev/null +++ b/docs/framework/alpine/guide/composable-tables.md @@ -0,0 +1,148 @@ +--- +title: Composable Tables (createTableHook) Guide +--- + +`createTableHook` creates an app-specific table factory. Use it to define shared features, row models, and default table options once, then create each Alpine table with the columns and data that are unique to that table. + +> [!NOTE] +> Unlike the React, Solid, Lit, and Svelte adapters, the Alpine `createTableHook` does not register reusable cell/header/table components. Alpine renders cell and header content as HTML strings through `x-html`, and Alpine has no component primitive to bind, so the hook is focused on sharing features and default options. Reusable interactive markup is expressed with [`Alpine.bind`](https://alpinejs.dev/globals/alpine-bind) bundles in your templates instead. + +## Examples + +- [Basic App Table](../examples/basic-app-table) - Minimal `createTableHook` setup. + +## Start With Shared Features and Options + +Create one app table hook and put the feature set, row models, and shared defaults there. This example makes sorting available to every table created by `createAppTable`. + +```ts +import { + createSortedRowModel, + createTableHook, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { createAppTable, createAppColumnHelper } = createTableHook({ + features, + debugTable: true, + enableSortingRemoval: false, +}) +``` + +Options passed to `createTableHook` become defaults for every table created by `createAppTable`. The `features` option is also bound to the returned column helper, so column definitions know that sorting APIs are available. + +## Create App Columns + +Create one column helper per row type. The helper is already bound to your app's feature set, so each table does not need to thread `typeof features` through its column definitions. Renderers return HTML strings, which are rendered with `x-html` via `table.FlexRender`. + +```ts +type Person = { + firstName: string + lastName: string + age: number + visits: number +} + +const columnHelper = createAppColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => 'Last Name', + cell: (info) => `${info.getValue()}`, + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), +]) +``` + +## Create A Table + +Create each table with `createAppTable` inside an `Alpine.data` component. The call site provides table-specific inputs such as `columns` and reactive `data`; shared features and defaults come from the hook. + +```ts +import Alpine from 'alpinejs' + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: [] as Array }) + + const table = createAppTable({ + columns, + get data() { + return local.data + }, + }) + + return { table } +}) + +window.Alpine = Alpine +Alpine.start() +``` + +## Render With The Normal Table APIs + +You render the table with the same table instance APIs used by a standalone `createTable` table. `table.FlexRender` is attached to the instance, so you do not need to import the top-level helper. Attach the sort click handler to a real element (Alpine does not initialize directives inside `x-html`). + +```html +
+ + + + + + + +
+
+``` + +## Override Shared Defaults Per Table + +Options passed to `createAppTable` override defaults from `createTableHook`. Use this for the few tables that need different behavior without creating a separate app hook. + +```ts +const table = createAppTable({ + columns, + get data() { + return local.data + }, + enableSortingRemoval: true, // override the hook default for this table only +}) +``` + +## When To Use This Pattern + +Use `createTableHook` when multiple tables should share features, row models, default options, or conventions. Use the standalone `createTable` API for a one-off table. diff --git a/docs/framework/alpine/guide/custom-features.md b/docs/framework/alpine/guide/custom-features.md new file mode 100644 index 0000000000..8c38b58900 --- /dev/null +++ b/docs/framework/alpine/guide/custom-features.md @@ -0,0 +1,408 @@ +--- +title: Custom Features (Alpine) Guide +--- + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +## TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers when TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable` for Alpine, `useTable` for React, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrappers around their adapter's table function to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking: you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +## How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + initTableInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initCellInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + cell: Cell, + ) => void + initColumnInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + column: Column, + ) => void + initHeaderGroupInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + headerGroup: HeaderGroup, + ) => void + initHeaderInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + header: Header, + ) => void + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void + resetTableInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods does: + +### Default Options and Initial State + +
+ +#### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Resizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-resizing/columnResizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +#### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +#### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-pagination/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +### API Creators + +
+ +#### initTableInstanceData and resetTableInstanceData + +Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier. + +Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`. + +Keep API assignment in `constructTableAPIs`; initialization and reset hooks are for data owned by the feature. + +
+ +#### constructTableAPIs + +The `constructTableAPIs` method in a table feature is exclusively responsible for adding methods to the `table` instance. It runs after all feature-owned table instance data has been initialized. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +#### assignHeaderPrototype and initHeaderInstanceData + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. The `initHeaderInstanceData` method is available for per-header instance data or caches that cannot live on the shared prototype. It runs during header construction, before sub-headers are populated and before the header is linked to its header group. Headers are reconstructed whenever header groups recompute, so it reruns on every rebuild. + +
+ +#### initHeaderGroupInstanceData + +The `initHeaderGroupInstanceData` method is available for per-header-group instance data. Header groups have no shared prototype, so this is their only per-instance extension point. It runs after a header group's `depth`, `id`, and fully populated `headers` array have been assigned, and reruns whenever header groups are rebuilt. + +
+ +#### assignColumnPrototype and initColumnInstanceData + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. The `initColumnInstanceData` method is available for per-column instance data or caches that cannot live on the shared prototype. For example, the [Aggregation](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.ts) feature uses it to set up a per-column aggregation cache. + +
+ +#### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +#### assignCellPrototype and initCellInstanceData + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, Column Grouping adds `getIsGrouped` and `getIsPlaceholder`, while Aggregation adds `getIsAggregated`. The `initCellInstanceData` method is available for per-cell instance data or caches that cannot live on the shared prototype. Cells are constructed lazily on first access per row/column pair and cached, so it runs once per cell instance. + +## Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +The feature object itself is framework-agnostic, so these steps apply to any adapter. You can follow along with the Alpine [Custom Plugin](../examples/custom-plugin) example. Here's an in-depth look at the steps to create a custom feature. + +### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/alpine-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +import { + assignTableAPIs, + functionalUpdate, + makeStateUpdater, +} from '@tanstack/alpine-table' +import type { TableFeature, Updater } from '@tanstack/alpine-table' + +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // initCellInstanceData: (cell) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // initColumnInstanceData: (column) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, + // initHeaderInstanceData: (header) => {}, + // if you need to add header group instance data... + // initHeaderGroupInstanceData: (headerGroup) => {}, +} +``` + +### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //.. +}) +``` + +### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs, options, and state in your application. Here the `density` state is owned externally in `Alpine.reactive` and connected with the new `onDensityChange` option: + +```ts +const features = tableFeatures({ densityPlugin }) + +const local = Alpine.reactive({ + data: makeData(1_000), + density: 'md', +}) as { data: Array; density: DensityState } + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... + state: { + // passing the density state to the table, TS is still happy :) + get density() { + return local.density + }, + }, + onDensityChange: (updater) => { + // raise density state changes to our own state management + local.density = + typeof updater === 'function' ? updater(local.density) : updater + }, +}) +``` + +Expose a `densityPadding()` helper on your `Alpine.data` object and use it from the template to drive the cell padding. The new `table.toggleDensity()` API can be wired to a real button. + +```ts +Alpine.data('table', () => { + // ...table setup from above... + + return { + table, + FlexRender, + densityPadding() { + return local.density === 'sm' + ? '4px' + : local.density === 'md' + ? '8px' + : '16px' + }, + } +}) +``` + +```html + + + +``` + +### Do We Have to Do It This Way? + +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in `Alpine.reactive`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/alpine/guide/expanding.md b/docs/framework/alpine/guide/expanding.md new file mode 100644 index 0000000000..72b7f2a1cc --- /dev/null +++ b/docs/framework/alpine/guide/expanding.md @@ -0,0 +1,353 @@ +--- +title: Expanding (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Expanding](../examples/expanding) +- [Sub Components](../examples/sub-components) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Expanding Setup + +Here's how you set up your table to use expanding features. Adding the expanding feature enables the related APIs. If you use client-side expanding, also set up `expandedRowModel` after its feature, since row model slots are type-checked. + +```ts +import { + createExpandedRowModel, + createTable, + rowExpandingFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), // if using client-side expanding + // manualExpanding: true, // if using manual server-side expanding +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Expanding Feature (Alpine) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` and the `expandedRowModel` factory to your features: + +```ts +import { + createExpandedRowModel, + createTable, + rowExpandingFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), +}) + +const table = createTable({ + features, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are child rows that inherit the same column structure as their parent rows. If your data object already includes expanded row data, use the `getSubRows` function to specify these child rows. If your data object does not contain expanded row data, it can be treated as custom expanded data, which is discussed in the next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { + id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 }, + ], + }, + { + id: 3, + name: 'Doe', + age: 40, + children: [{ id: 4, name: 'Alice', age: 10 }], + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +const table = createTable({ + features, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +}) +``` + +> [!NOTE] +> You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +Because Alpine does not initialize directives inside content set with `x-html`, render the detail panel content with `x-html`, while the expanded sub-row markup itself stays in your template. Use `x-if="row.getIsExpanded()"` to conditionally render the detail row. + +```ts +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(10, 5) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + getRowCanExpand: () => true, + }) + + return { + table, + FlexRender, + renderSubComponent(row) { + return `
${JSON.stringify(
+        row.original,
+        null,
+        2,
+      )}
` + }, + } +}) +``` + +```html + +``` + +### Expanded rows state + +If you need access to the expanded state of the rows in other parts of your application, you can own the `expanded` state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The atom can be read, written, or subscribed to anywhere in your app without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' +import type { ExpandedState } from '@tanstack/alpine-table' + +const expandedAtom = createAtom({}) + +// subscribe to the atom wherever you need the value +expandedAtom.subscribe(() => { + // react to expanded changes +}) + +const table = createTable({ + features, + // other options... + atoms: { + expanded: expandedAtom, // expanding APIs now update expandedAtom + }, +}) +``` + +Alternatively, the v8-style `state.expanded` plus `onExpandedChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ expanded: {} as ExpandedState }) + +const table = createTable({ + features, + // other options... + state: { + get expanded() { + return local.expanded // connect the reactive slice back down to the table + }, + }, + onExpandedChange: (updater) => { + local.expanded = + typeof updater === 'function' ? updater(local.expanded) : updater + }, +}) +``` + +You can read the current expanded value with `table.atoms.expanded.get()`. Inside an Alpine binding this is a reactive read; in event handlers it simply returns the current value. + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows whose IDs are present as keys in the record and have a value of true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. Because Alpine does not initialize directives inside content set with `x-html`, the expander button cannot live inside an `x-html` cell. Instead, special-case the expander column by its column id directly in your markup and attach the handler returned by `getToggleExpandedHandler` to a real button. + +```html + + + + + + +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```ts +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```ts +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, filtering starts from the parent rows and moves downwards. If a parent row is excluded by the filter, all of its child rows are excluded too. You can change this with the `filterFromLeafRows` option. When it is enabled, filtering starts from the leaf (child) rows and moves upwards, so a parent row is included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. The `maxLeafRowFilterDepth` option sets the maximum depth of child rows that the filter considers. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns, +}) + +//... +const table = createTable({ + features, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parent's page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +const table = createTable({ + features, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Row Pinning Guide](./row-pinning) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Auto Reset Expanded State + +If you are also using the grouping feature, the `expanded` state is automatically reset whenever the grouped row model recomputes, such as when the `data` or the grouping state changes. This default is automatically disabled when `manualExpanding` is `true`, but it can be overridden by explicitly assigning a boolean value to the `autoResetExpanded` table option. There is also a global `autoResetAll` table option that disables (or enables) every auto-reset behavior at once. + +A common reason to set `autoResetExpanded: false` is editing data while viewing the table (for example, inline cell editing). Every edit updates `data`, which recomputes the row models and would otherwise collapse the user's expanded rows. If you also use the pagination feature, pair it with `autoResetPageIndex: false` so the current page is kept as well. + +```ts +const table = createTable({ + features, + // other options... + autoResetExpanded: false, // keep expanded state when data changes + // autoResetAll: false, // or turn off all auto resets at once +}) +``` + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const features = tableFeatures({ rowExpandingFeature }) + +const table = createTable({ + features, + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/alpine/guide/flex-render.md b/docs/framework/alpine/guide/flex-render.md new file mode 100644 index 0000000000..fc9727a5fc --- /dev/null +++ b/docs/framework/alpine/guide/flex-render.md @@ -0,0 +1,39 @@ +--- +title: FlexRender (Alpine) Guide +--- + +Alpine column definitions commonly contain strings or functions that return HTML strings for `header`, `cell`, `footer`, and `aggregatedCell`. The rendering utilities resolve those definitions with the correct table context. + +## `FlexRender` vs `flexRender` + +`FlexRender` is the recommended table-aware wrapper. Pass exactly one `cell`, `header`, or `footer` object: + +```html + + + +``` + +Import `FlexRender` from `@tanstack/alpine-table` and expose it to the Alpine data scope, or use `table.FlexRender` on a table created by the adapter. For footer groups, call `FlexRender({ footer: header })`. + +For cells, `FlexRender` selects `aggregatedCell` for aggregated rows, falls back to `cell`, and returns `null` for grouping placeholders. + +`flexRender` is the lower-level function for a definition and context: + +```ts +import { flexRender } from '@tanstack/alpine-table' + +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +It invokes function renderers and passes non-functions through unchanged. It does not select grouped-cell renderers or suppress grouping placeholders. + +Because `x-html` inserts HTML, only render markup produced by code you trust. Escape or sanitize untrusted data before including it in a renderer result. Use `x-text` or normal DOM bindings instead when a renderer only needs to display text. + +Placeholder headers remain the template's layout decision. Check `header.isPlaceholder` unless a spanning-header layout intentionally renders that placeholder. diff --git a/docs/framework/alpine/guide/fuzzy-filtering.md b/docs/framework/alpine/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..3f080abcd2 --- /dev/null +++ b/docs/framework/alpine/guide/fuzzy-filtering.md @@ -0,0 +1,234 @@ +--- +title: Fuzzy Filtering (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Filters](../examples/filters) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Fuzzy Filtering Setup + +Here's how you set up your table to use fuzzy filtering features. Adding the fuzzy filtering feature enables the related APIs. If you use client-side fuzzy filtering and sorting, also set up `filteredRowModel` and `sortedRowModel` after their features, since row model slots are type-checked. + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createSortedRowModel, + createTable, + globalFilteringFeature, + rowSortingFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + sortedRowModel: createSortedRowModel(), // if using client-side sorting + // manualSorting: true, // if using manual server-side sorting + filterFns: { fuzzy: fuzzyFilter }, + sortFns: { fuzzy: fuzzySort }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +> [!NOTE] +> The `filterFns` and `sortFns` registries above list only the custom `fuzzy` functions this guide uses. Spreading the entire built-in registries (`filterFns: { ...filterFns, fuzzy: fuzzyFilter }`) still works, but it puts every built-in function in your bundle. Register just the functions you use, or pass functions directly to the `filterFn` and `sortFn` column options with no registration. + +## Fuzzy Filtering (Alpine) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement client-side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> [!NOTE] +> You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked to work better with TanStack Table's row by row filtering approach. + +```bash +npm install @tanstack/match-sorter-utils +``` + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +Here's an example of a custom fuzzy filter function: + +```ts +import { rankItem } from '@tanstack/match-sorter-utils' +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import type { FilterFn, RowData, TableFeatures } from '@tanstack/alpine-table' + +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} +type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } + +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta?.({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the `rankItem` function from the `@tanstack/match-sorter-utils` library to rank the item. We then store the ranking information in the filter meta of the row (the `addMeta` callback is optional, so call it with optional chaining), and return whether the item passed the ranking criteria. + +To reference this filter function by the string name `'fuzzy'` (and to type the stored filter meta), register it in the `filterFns` slot on `tableFeatures` and declare a `filterMeta` slot for the meta type: + +```ts +import { metaHelper } from '@tanstack/alpine-table' +import type { FilterFn, RowData, TableFeatures } from '@tanstack/alpine-table' + +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} +type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } + +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + const itemRank = rankItem(row.getValue(columnId), value) + addMeta?.({ itemRank }) + return itemRank.passed +} + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { fuzzy: fuzzyFilter }, + sortFns: { fuzzy: fuzzySort }, + filterMeta: metaHelper(), +}) +``` + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, register the fuzzy filter function in the `filterFns` slot on `tableFeatures` and reference it in the `globalFilterFn` option of the table: + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createSortedRowModel, + createTable, + globalFilteringFeature, + metaHelper, + rowSortingFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), // needed if you want sorting with fuzzy rank + filterFns: { fuzzy: fuzzyFilter }, + sortFns: { fuzzy: fuzzySort }, + filterMeta: metaHelper(), +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, register your fuzzy filter function in the `filterFns` slot on `tableFeatures` (as shown above). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```ts +const column = [ + { + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +] +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```ts +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFn_alphanumeric } from '@tanstack/alpine-table' +import type { SortFn } from '@tanstack/alpine-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId].itemRank!, + rowB.columnFiltersMeta[columnId].itemRank!, + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFn_alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then pass this sorting function directly to the `sortFn` option of the column definition: + +```ts +{ + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + filterFn: 'fuzzy', // using our custom fuzzy filter function (registered above) + sortFn: fuzzySort, // pass our custom fuzzy sort function directly +} +``` + +> [!NOTE] +> Unlike `filterFn: 'fuzzy'` above, `fuzzySort` is passed as a function rather than a string. A string reference like `sortFn: 'fuzzySort'` would only work if you also registered the function in the `sortFns` slot on `tableFeatures` (e.g. `sortFns: { fuzzySort }`). Passing the function directly skips that step. diff --git a/docs/framework/alpine/guide/global-filtering.md b/docs/framework/alpine/guide/global-filtering.md new file mode 100644 index 0000000000..500b790359 --- /dev/null +++ b/docs/framework/alpine/guide/global-filtering.md @@ -0,0 +1,278 @@ +--- +title: Global Filtering (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Faceted Filters](../examples/filters-faceted) +- [Column Filters](../examples/filters) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Global Filtering Setup + +Here's how you set up your table to use global filtering features. Global filtering depends on column filtering, so add `columnFilteringFeature` before `globalFilteringFeature`. Adding the global filtering feature enables the related APIs. If you use client-side filtering, also set up `filteredRowModel` after its feature, since row model slots are type-checked. + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createTable, + filterFn_includesString, + globalFilteringFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + filterFns: { includesString: filterFn_includesString }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +> [!NOTE] +> The `filterFns` registry above lists only the built-in filter function this table uses. Spreading the entire built-in `filterFns` registry (`filterFns: { ...filterFns }`) still works, but it puts every built-in filter function in your bundle. Register just the functions you use, or pass a function directly to the `globalFilterFn` option with no registration at all. + +## Global Filtering (Alpine) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. + +The client-side filtered row model also invokes the page-index auto-reset hook when global filtering inputs change. Whether the page index resets depends on the `autoResetPageIndex`, `autoResetAll`, and `manualPagination` options. If filtering is manual and this row model is omitted or bypassed, a global filter state change does not invoke that hook, so reset server-side pagination in the filter change handler when needed. + +### Manual Server-Side Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `tableFeatures`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +import { + columnFilteringFeature, + createTable, + globalFilteringFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` (along with its required `columnFilteringFeature` prerequisite) and the `filteredRowModel` factory to your features: + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createTable, + filterFn_includesString, + globalFilteringFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { includesString: filterFn_includesString }, +}) + +const table = createTable({ + features, + // other options... +}) +``` + +### Global Filter Function + +The `globalFilterFn` option sets the filter function used for global filtering. The filter function can be a string that references a filter function (built-in or custom) registered in the `filterFns` slot on `tableFeatures`, or a filter function passed directly. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + globalFilterFn: 'includesString', // built-in filter function +}) +``` + +By default there are 12 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom global filter function and pass it directly to the `globalFilterFn` table option, as shown [below](#custom-global-filter-function). + +### Global Filter State + +The `globalFilter` state slice holds the current global filter value, usually a search string (the slice is typed as `any` so custom global filter functions can accept other value shapes). The table's state atoms are reactive in Alpine. `table.atoms.globalFilter.get()` is a reactive read when used inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. + +If you need access to the global filter state outside of the table, you can own the slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The filter value can be read, written, or subscribed to elsewhere (such as in a query key for server-side filtering) without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' + +const globalFilterAtom = createAtom('') + +// subscribe to the atom wherever you need the value (e.g. for a query key) +globalFilterAtom.subscribe(() => { + // react to global filter changes +}) + +const table = createTable({ + features, + // other options... + atoms: { + globalFilter: globalFilterAtom, // table.setGlobalFilter now updates globalFilterAtom + }, +}) +``` + +Alternatively, the v8-style `state.globalFilter` plus `onGlobalFilterChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ globalFilter: '' }) + +const table = createTable({ + features, + // other options... + state: { + get globalFilter() { + return local.globalFilter // connect the reactive slice back down to the table + }, + }, + onGlobalFilterChange: (updater) => { + local.globalFilter = + typeof updater === 'function' ? updater(local.globalFilter) : updater + }, +}) +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. Bind the input's `:value` to `table.atoms.globalFilter.get()` (a reactive read inside the binding) and update it from `@input` with `table.setGlobalFilter`. Put interactivity on real elements, not inside `x-html`. + +```html + +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the `globalFilterFn` option. + +> [!NOTE] +> It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering). + +```ts +const customFilterFn = (row, columnId, filterValue) => { + return // true if the row should be included in the filtered rows +} + +const table = createTable({ + features, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table `initialState` option. However, if you are controlling the slice yourself, set the starting value on your external atom or reactive state instead. + +```ts +const table = createTable({ + features, + // other options... + initialState: { + globalFilter: 'search term', // if not controlling globalFilter state, set initial state here + }, +}) +``` + +> [!NOTE] +> Do not use both `initialState.globalFilter` and a controlled `globalFilter` (via `atoms` or `state`) at the same time, as the controlled value will override `initialState.globalFilter`. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +const table = createTable({ + features, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` + +### Global Filter APIs + +There are several APIs that are useful for hooking up your global filter UI: + +- `table.setGlobalFilter` - Set the global filter value. Useful for connecting a search input's `input` handler. +- `table.resetGlobalFilter` - Reset the global filter value to its initial state, or clear it with `table.resetGlobalFilter(true)`. +- `table.getGlobalFilterFn` - Returns the filter function currently used for global filtering. +- `table.getGlobalAutoFilterFn` - Returns the default global filter function (currently `includesString`). +- `column.getCanGlobalFilter` - Returns whether a column participates in global filtering. Useful for debugging which columns are searched. diff --git a/docs/framework/alpine/guide/grouping.md b/docs/framework/alpine/guide/grouping.md new file mode 100644 index 0000000000..e7d700afe7 --- /dev/null +++ b/docs/framework/alpine/guide/grouping.md @@ -0,0 +1,300 @@ +--- +title: Grouping (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Grouping](../examples/grouping) + +> [!NOTE] +> `columnGroupingFeature` and `rowAggregationFeature` are now separate features. Register either one independently, or register both when grouped rows should also calculate aggregate values. See the [Aggregation Guide](./aggregation) for aggregation setup. + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Grouping Setup + +Here's how you set up your table to use grouping features. Adding the grouping feature enables the related APIs. If you use client-side grouping, also set up `groupedRowModel` after its feature, since row model slots are type-checked. + +```ts +import { + columnGroupingFeature, + createGroupedRowModel, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), // if using client-side grouping + // manualGrouping: true, // if using manual server-side grouping +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Grouping (Alpine) Guide + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +Grouping can also affect column order. There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +### Client-Side vs Server-Side Grouping + +Grouping should operate over the complete dataset when its groups are meant to describe all rows. Use client-side grouping when the browser has the complete dataset. Use manual server-side grouping when the server returns only a page or another subset, or when the server needs to perform grouping and aggregation. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. + +The client-side grouped row model invokes the page-index and expanded-state auto-reset hooks when its inputs change. Whether those states reset depends on the `autoResetPageIndex`, `autoResetExpanded`, `autoResetAll`, `manualPagination`, and `manualExpanding` options. If grouping is manual and this row model is omitted or bypassed, a grouping state change does not invoke those hooks, so reset dependent server-side state in the grouping change handler when needed. + +### Client-Side Grouping + +To use the grouping feature, add the `columnGroupingFeature` and the `groupedRowModel` factory to your features. The grouped row model is responsible for grouping the rows based on the grouping state. + +```ts +import { + columnGroupingFeature, + createGroupedRowModel, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), +}) + +const table = createTable({ + features, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```ts +const features = tableFeatures({ + columnGroupingFeature, + rowExpandingFeature, + groupedRowModel: createGroupedRowModel(), + expandedRowModel: createExpandedRowModel(), +}) + +const table = createTable({ + features, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```ts +table.setGrouping(['column1', 'column2']) +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```ts +table.resetGrouping() +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```ts +const table = createTable({ + features, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Manual Grouping + +If you are doing server-side grouping, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to group the rows before passing them to the table. + +```ts +const features = tableFeatures({ columnGroupingFeature }) + +const table = createTable({ + features, + // other options... + manualGrouping: true, +}) +``` + +> [!NOTE] +> There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Controlled Grouping State + +If you need access to the grouping state in other parts of your application, you can own the `grouping` state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The atom can be read, written, or subscribed to anywhere in your app (such as in a query key for server-side grouping) without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' +import type { GroupingState } from '@tanstack/alpine-table' + +const groupingAtom = createAtom([]) + +// subscribe to the atom wherever you need the value +groupingAtom.subscribe(() => { + // react to grouping changes +}) + +const table = createTable({ + features, + // other options... + atoms: { + grouping: groupingAtom, // grouping APIs now update groupingAtom + }, +}) +``` + +Alternatively, the v8-style `state.grouping` plus `onGroupingChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ grouping: [] as GroupingState }) + +const table = createTable({ + features, + // other options... + state: { + get grouping() { + return local.grouping // connect the reactive slice back down to the table + }, + }, + onGroupingChange: (updater) => { + local.grouping = + typeof updater === 'function' ? updater(local.grouping) : updater + }, +}) +``` + +You can read the current grouping value with `table.atoms.grouping.get()`. Inside an Alpine binding this is a reactive read; in event handlers it simply returns the current value. + +### Wiring up the grouping UI + +Because Alpine does not initialize directives inside content set with `x-html`, the in-cell grouping controls (the header group toggle button and the grouped-cell expander) cannot live inside an `x-html` span. Render the header and cell content with `x-html="FlexRender(...)"`, but special-case the interactive parts directly in the markup using the cell helpers (`cell.getIsGrouped()` and `cell.getIsPlaceholder()`). + +The group toggle button lives in the header. Call the handler returned by `getToggleGroupingHandler` with the event. + +```html + + + +``` + +In the cell, choose between grouped, placeholder, and normal rendering. The grouped cell also carries the expander button (which calls the row's `getToggleExpandedHandler`). It helps to expose a small helper on your `Alpine.data` object for the cell background. + +```ts +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(10_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + }) + + return { + table, + FlexRender, + cellBackground(cell) { + if (cell.getIsGrouped()) return '#0aff0082' + if (cell.getIsPlaceholder()) return '#ff000042' + return 'white' + }, + } +}) +``` + +```html + + + + + + + + +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```ts +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +``` + +Rows expose grouping helpers for grouped row rendering: + +```ts +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose grouping and placeholder helpers: + +```ts +cell.getIsGrouped() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```ts +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/alpine/guide/pagination.md b/docs/framework/alpine/guide/pagination.md new file mode 100644 index 0000000000..8c0d12b7d6 --- /dev/null +++ b/docs/framework/alpine/guide/pagination.md @@ -0,0 +1,295 @@ +--- +title: Pagination (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Pagination](../examples/pagination) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Pagination Setup + +Here's how you set up your table to use pagination features. Adding the pagination feature enables the related APIs. If you use client-side pagination, also set up `paginatedRowModel` after its feature, since row model slots are type-checked. + +```ts +import { + createPaginatedRowModel, + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), // if using client-side pagination + // manualPagination: true, // if using manual server-side pagination +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Pagination (Alpine) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain **_ALL_** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. + +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` and the `paginatedRowModel` factory to your features: + +```ts +import { + createPaginatedRowModel, + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, pass `-1` for `pageCount`. In that case, `getCanNextPage()` returns `true` because the table cannot detect the end, `getCanPreviousPage()` depends on the current `pageIndex`, and `getCanLastPage()` returns `false` because no finite last page is known. + +```ts +import { + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> [!NOTE] +> Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +In Alpine, the table's state atoms are reactive. `table.atoms.pagination.get()` is a reactive read when used inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. + +If you need access to the `pagination` state outside of the table (a server-side query key is the most common case), you can own the slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The pagination value can be used in a query key without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' +import { + createTable, + rowPaginationFeature, + tableFeatures, + type PaginationState, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const paginationAtom = createAtom({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +// subscribe to the atom wherever you need the value (e.g. for a query key) +paginationAtom.subscribe(() => { + // react to pagination changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + pagination: paginationAtom, // table pagination APIs now update paginationAtom + }, +}) +``` + +Alternatively, the v8-style `state.pagination` plus `onPaginationChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + pagination: { + pageIndex: 0, // initial page index + pageSize: 10, // default page size + } as PaginationState, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get pagination() { + return local.pagination // connect the reactive slice back down to the table + }, + }, + onPaginationChange: (updater) => { + local.pagination = + typeof updater === 'function' ? updater(local.pagination) : updater + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> [!NOTE] +> Do NOT provide the `pagination` slice in more than one of the `atoms`, `state`, and `initialState` options. Controlled values (`atoms` or `state`) will overwrite `initialState`. Only use one of them. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` whenever the client-side row models recompute, such as when the `data` is updated, filters change, sorting changes, or grouping changes. This behavior is automatically disabled when `manualPagination` is `true`, but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. There is also a global `autoResetAll` table option that disables (or enables) every auto-reset behavior at once. + +> [!NOTE] +> Automatic resets run only when an included client-side row model that triggers them recomputes. If a manual server-side table omits the filtered, sorted, grouped, or other relevant row model, changing that controlled state does not trigger a page-index reset, even when `autoResetPageIndex` or `autoResetAll` is `true`. Reset `pageIndex` yourself in the corresponding change handler. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + autoResetPageIndex: false, // turn off auto reset of pageIndex + // autoResetAll: false, // or turn off all auto resets at once +}) +``` + +A common reason to set `autoResetPageIndex: false` is editing data while viewing the table (for example, inline cell editing). Every edit updates `data`, which recomputes the row models and would otherwise snap the user back to the first page. Setting the option to a static `false` keeps the current page when the row model recomputes. If you also use the expanding feature, pair it with `autoResetExpanded: false` so expanded rows do not collapse on edits. + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `getCanLastPage`: Useful for disabling the "last page" button when no finite last page is known. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> [!NOTE] +> These pagination APIs are available when using `rowPaginationFeature`. + +Pagination controls live on real elements so the click handlers and `:disabled` bindings stay interactive. Read the page index and page size with `table.atoms.pagination.get()`. + +```html + + + + + + Page + + + of + + + + +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/alpine/guide/row-pinning.md b/docs/framework/alpine/guide/row-pinning.md new file mode 100644 index 0000000000..b9236d9560 --- /dev/null +++ b/docs/framework/alpine/guide/row-pinning.md @@ -0,0 +1,294 @@ +--- +title: Row Pinning (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Row Pinning](../examples/row-pinning) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Row Pinning Setup + +Here's how you set up your table to use row pinning features. Adding the row pinning feature enables the related APIs. + +```ts +import { + createTable, + tableFeatures, + rowPinningFeature, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Row Pinning (Alpine) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory. + +```ts +import { + rowPinningFeature, + tableFeatures, + createTable, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```ts +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, the recommended v9 approach is an external atom passed to the table's `atoms` option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without going through the component that owns the table. + +```ts +import { createAtom } from '@tanstack/store' +import type { RowPinningState } from '@tanstack/alpine-table' + +const rowPinningAtom = createAtom({ + top: [], + bottom: [], +}) + +// subscribe to the atom wherever you need the value +rowPinningAtom.subscribe(() => { + // react to pinning changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + rowPinning: rowPinningAtom, + }, +}) +``` + +Alternatively, the v8-style `state.rowPinning` plus `onRowPinningChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + rowPinning: { top: [], bottom: [] } as RowPinningState, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get rowPinning() { + return local.rowPinning // connect the reactive slice back down to the table + }, + }, + onRowPinningChange: (updater) => { + local.rowPinning = + typeof updater === 'function' ? updater(local.rowPinning) : updater + }, +}) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```ts +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +}) + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +You can read the current pinning state with `table.atoms.rowPinning.get()`, which is a reactive read when used inside an Alpine binding and a plain read elsewhere. + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```ts +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls. Because Alpine does not initialize directives inside content set with `x-html`, render any pin buttons on real elements rather than inside a cell renderer. Define a `pin` column that exposes a plain value, then special-case it in your template by column id: + +```ts +const columns = [ + { + id: 'pin', + header: () => 'Pin', + cell: () => '', // buttons are rendered on real elements in the template + }, + //... +] +``` + +```html + + + + +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```ts +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly with `x-for`: + +```html + + + + + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```ts +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableRowPinning: (row) => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/alpine/guide/row-selection.md b/docs/framework/alpine/guide/row-selection.md new file mode 100644 index 0000000000..f1495ae465 --- /dev/null +++ b/docs/framework/alpine/guide/row-selection.md @@ -0,0 +1,306 @@ +--- +title: Row Selection (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Row Selection](../examples/row-selection) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Row Selection Setup + +Here's how you set up your table to use row selection features. Adding the row selection feature enables the related APIs. + +```ts +import { + createTable, + tableFeatures, + rowSelectionFeature, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Row Selection (Alpine) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you. You can access the row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the row selection state (reactive when read inside an Alpine binding) +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +In Alpine, the table's state atoms are reactive. `table.atoms.rowSelection.get()` is a reactive read when called inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. + +> [!NOTE] +> If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +If you need easy access to the selected row ids in other parts of your application (for example, to make API calls with them), you can own the row selection state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. Atoms preserve fine-grained subscriptions, and the selection value can be read anywhere in your app without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' +import { + createTable, + tableFeatures, + rowSelectionFeature, + type RowSelectionState, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const rowSelectionAtom = createAtom({}) + +// subscribe to the atom wherever you need the value +rowSelectionAtom.subscribe(() => { + // react to selection changes +}) + +const table = createTable({ + features, + //... + atoms: { + rowSelection: rowSelectionAtom, // selection APIs now update rowSelectionAtom + }, +}) +``` + +Alternatively, the v8-style `state.rowSelection` plus `onRowSelectionChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ rowSelection: {} as RowSelectionState }) + +const table = createTable({ + features, + //... + state: { + get rowSelection() { + return local.rowSelection // connect the reactive slice back down to the table + }, + }, + onRowSelectionChange: (updater) => { + local.rowSelection = + typeof updater === 'function' ? updater(local.rowSelection) : updater + }, +}) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +const table = createTable({ + features, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +const table = createTable({ + //... + enableRowSelection: (row) => row.original.age > 18, //only enable row selection for adults +}) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +const table = createTable({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +}) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +const table = createTable({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +}) +``` + +Sub-row selection also applies to the select-all APIs. When a parent row blocks sub-row selection, `table.toggleAllRowsSelected()` and `table.toggleAllPageRowsSelected()` skip that parent's descendants, and `table.getIsAllRowsSelected()` and `table.getIsAllPageRowsSelected()` ignore those descendants when deciding whether everything is selected. + +Selecting a parent row writes the parent id and its selectable descendant ids into the row selection state. Deselecting a child afterwards does not remove the parent id by default, since some tables treat the state ids as literal selections. Pass the `deselectParents` option to the toggle APIs to remove ancestor ids whenever a row is deselected: + +```ts +row.getToggleSelectedHandler({ deselectParents: true }) +// or +row.toggleSelected(false, { deselectParents: true }) +``` + +### Shift Range Selection + +`row.getToggleSelectedHandler()` supports Shift range selection by default. After an ordinary selectable-row interaction establishes an anchor, Shift-selecting another row selects or deselects the inclusive interval between them. The clicked checkbox's resulting checked value controls the whole range, and the clicked endpoint becomes the anchor for the next Shift interaction. + +The handler recognizes Shift when the event exposes either `event.shiftKey` or `event.nativeEvent.shiftKey`. You can disable range behavior or replace event detection: + +Bind an Alpine checkbox handler with `@click`, not `@change`, so the handler receives the click event and its `shiftKey` modifier. + +```ts +const table = createTable({ + // ... + enableRowRangeSelection: false, + + // For example, use the platform modifier instead of Shift: + // isRowRangeSelectionEvent: event => + // Boolean((event as { metaKey?: boolean }).metaKey), +}) +``` + +Range selection follows the table's current logical display order, including filtering, grouping, sorting, and expansion. With client-side pagination, ranges can cross pages because the complete pre-pagination order is used. With manual/server pagination, only rows loaded in the current `data` can participate. + +By default, a parent encountered in a range recursively toggles its selectable descendants when sub-row selection is enabled. Pass `selectChildren: false` when only rows explicitly present in the display-order interval should change: + +```ts +const handler = row.getToggleSelectedHandler({ + selectChildren: false, +}) +``` + +The interaction anchor is preserved across sorting, filtering, grouping, expansion, pagination, and data updates while its row id remains in the display order. If filtering or data replacement removes the anchor, the next Shift interaction falls back to an ordinary row toggle and establishes a new anchor. `resetRowSelection`, either select-all API, and `table.reset()` clear the anchor. Direct calls to `row.toggleSelected()` or `table.setRowSelection()`, and external controlled-state changes, do not establish or move it. + +### Render Row Selection UI + +TanStack Table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These functions automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +Because checkboxes are interactive and Alpine cannot process directives inside content set with `x-html`, they cannot live in a cell or header renderer. Define a `select` column that exposes a plain value, then render the real `` elements in your template, special-cased by column id. Bind the indeterminate state with `x-effect`, since it is a DOM property that cannot be set with a normal attribute binding. + +```ts +const columns = [ + { + id: 'select', + header: () => '', // checkboxes are rendered on real elements in the template + cell: () => '', + }, + //... more column definitions... +] +``` + +```html + + + + +``` + +```html + + + + +``` + +> [!NOTE] +> The `getCanSelectSubRows()` and `getIsAllSubRowsSelected()` clauses on the row checkbox only matter for tables with sub-rows. With flat data, `row.getIsSelected()` alone is enough. See the expanding example for the full pattern, including the `deselectParents` option for pruning stale parent ids when children are deselected. + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. Attach it to a real element such as the ``. + +```html + + + +``` diff --git a/docs/framework/alpine/guide/sorting.md b/docs/framework/alpine/guide/sorting.md new file mode 100644 index 0000000000..b5c5456de0 --- /dev/null +++ b/docs/framework/alpine/guide/sorting.md @@ -0,0 +1,623 @@ +--- +title: Sorting (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Sorting](../examples/sorting) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Sorting Setup + +Here's how you set up your table to use sorting features. Adding the sorting feature enables the related APIs. If you use client-side sorting, also set up `sortedRowModel` after its feature, since row model slots are type-checked. + +```ts +import { + createSortedRowModel, + createTable, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_datetime, + sortFn_text, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), // if using client-side sorting + // manualSorting: true, // if using manual server-side sorting + sortFns: { + alphanumeric: sortFn_alphanumeric, + datetime: sortFn_datetime, + text: sortFn_text, + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +> [!NOTE] +> Spreading the entire built-in registry (`sortFns: { ...sortFns }`) still works, but it puts every built-in sorting function in your bundle. Registering just the functions you use, or passing a function directly to the `sortFn` column option, is recommended. The default `sortFn: 'auto'` resolves to `alphanumeric`, `text`, or `datetime` from the registry based on the column's data type, so register the ones your columns rely on. + +## Sorting (Alpine) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```ts +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +The table's state atoms are reactive in Alpine. `table.atoms.sorting.get()` is a reactive read when used inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. `table.store.get()` returns a current full-state snapshot, useful for debugging. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) + +table.atoms.sorting.get() // reactive read inside Alpine bindings, plain read elsewhere +``` + +However, if you need access to the sorting state outside of the table, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state in other parts of your application, you can own the sorting state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The atom can be read, written, or subscribed to elsewhere (such as in a query key for server-side sorting) without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' + +const sortingAtom = createAtom([]) // can set initial sorting state here + +// subscribe to the atom wherever you need the value (e.g. for a query key) +sortingAtom.subscribe(() => { + // react to sorting changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + sorting: sortingAtom, // table sorting APIs now update sortingAtom + }, +}) +``` + +Alternatively, the v8-style `state.sorting` plus `onSortingChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ sorting: [] as SortingState }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get sorting() { + return local.sorting // connect the reactive slice back down to the table + }, + }, + onSortingChange: (updater) => { + local.sorting = + typeof updater === 'function' ? updater(local.sorting) : updater + }, +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> [!NOTE] +> Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. + +The client-side sorted row model also invokes the page-index auto-reset hook when sorting inputs change. Whether the page index resets depends on the `autoResetPageIndex`, `autoResetAll`, and `manualPagination` options. If sorting is manual and this row model is omitted or bypassed, a sorting state change does not invoke that hook, so reset server-side pagination in the sorting change handler when needed. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorted row model, but you want to disable it, you can use the `manualSorting` table option. + +```ts +import { createAtom } from '@tanstack/store' + +const features = tableFeatures({ rowSortingFeature }) // feature needed for sorting state/APIs + +const sortingAtom = createAtom([]) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + manualSorting: true, // use pre-sorted row model instead of sorted row model + atoms: { + sorting: sortingAtom, + }, +}) +``` + +Hoisting the sorting state into your own scope (with an external atom or the `state.sorting` plus `onSortingChange` pattern) is covered in the [Controlled Sorting State](#controlled-sorting-state) section above. + +> [!NOTE] +> When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` and the `sortedRowModel` factory to your features. Import `createSortedRowModel` and the individual sorting functions you use from TanStack Table: + +```ts +import { + createSortedRowModel, + createTable, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_datetime, + sortFn_text, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + datetime: sortFn_datetime, + text: sortFn_text, + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions, either inline as the `sortFn` column option, or by name in the sorting function registry that you pass to `createSortedRowModel`. + +#### Custom Sorting Functions + +Whether you register a custom sorting function in the registry passed to `createSortedRowModel` or pass it directly as a `sortFn` column option, it should have the following signature: + +```ts +// optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = ( + rowA: Row, + rowB: Row, + columnId: string, +) => { + return // -1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> [!NOTE] +> The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take care of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and is expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // reference a custom sorting function registered with createSortedRowModel + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + }, +] +//... +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + datetime: sortFn_datetime, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +> **TypeScript Note:** For `sortFn: 'myCustomSortFn'` string references to typecheck, register the function in the `sortFns` slot on `tableFeatures` (as shown above). The slot is the registry; no `declare module` augmentation is needed. Alternatively, skip the registry entirely by passing the function directly to the `sortFn` column option. + +#### Customize Sorting Function Behavior + +Sorting functions support an optional "hanging" property: + +- `sortFn.resolveDataValue` - normalizes each row's value before the two sides are compared. It is honored by every sorting function built with the `constructSortFn` helper, which includes all built-in sorting functions. + +The `constructSortFn` helper builds a sorting function from a value-level comparator (`sort`) plus that optional resolver. Keeping the comparison in `sort` and the normalization in `resolveDataValue` means a variant of an existing sorting function only has to swap the resolver. The definition is attached to the returned function, so you can spread any sorting function built with `constructSortFn` and override only what differs. + +For example, a version of `alphanumeric` that ignores diacritics, so that "Éric Bernard" sorts next to "Eric Brandon" instead of after "Zak O'Sullivan": + +```ts +const stripDiacritics = (value: string) => + value.normalize('NFD').replace(/\p{Diacritic}/gu, '') + +const alphanumericIgnoreDiacritics = constructSortFn({ + ...sortFn_alphanumeric, // reuse the comparator + resolveDataValue: (value) => + stripDiacritics(sortFn_alphanumeric.resolveDataValue!(value)), +}) + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { alphanumeric: sortFn_alphanumeric, alphanumericIgnoreDiacritics }, +}) +``` + +The same pattern works when defining a new sorting function from scratch: + +```ts +const byLastName = constructSortFn({ + sort: (dataValueA, dataValueB) => + dataValueA === dataValueB ? 0 : dataValueA > dataValueB ? 1 : -1, + resolveDataValue: (value) => + String(value ?? '') + .split(' ') + .at(-1) ?? '', +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```ts +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, // sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, // sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + sortDescFirst: true, // sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> [!NOTE] +> You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +If not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be passed to the sorting function like any other value with no special handling; the sorting function is responsible for handling them +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> [!NOTE] +> `'first'` and `'last'` options are available in v9. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this (the first direction depends on the column's data type and the `sortDescFirst` option, as discussed [above](#sorting-direction); a string column is shown here): + +`'none' -> 'asc' -> 'desc' -> 'none' -> 'asc' -> 'desc' -> ...` + +If you disable sorting removal, the `'none'` state is skipped after the first sort: + +`'none' -> 'asc' -> 'desc' -> 'asc' -> 'desc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (sorting can never return to 'none' once applied) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```ts +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Wiring up the sort UI + +Because Alpine does not initialize directives inside content set with `x-html`, render the header content with `x-html="FlexRender({ header })"` but attach the click handler to a real element around it. Call the handler returned by `getToggleSortingHandler` with the event. + +```html + + + +``` + +### Reset Sorting When Data Changes + +Sorting state is preserved when the `data` option changes by default. Set `autoResetSorting: true` to reset sorting whenever a new data reference is processed. The reset restores `initialState.sorting`, or an empty sorting state when no initial value was provided. + +This option responds only to data changes. Changing sorting, filters, or grouping does not trigger it. The global `autoResetAll` option overrides `autoResetSorting` when explicitly set. + +Be careful when combining this option with manual/server-side sorting: a server response normally replaces `data`, so enabling the reset can immediately clear the sorting state that requested that response. + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/alpine/guide/table-state.md b/docs/framework/alpine/guide/table-state.md new file mode 100644 index 0000000000..21ec679be5 --- /dev/null +++ b/docs/framework/alpine/guide/table-state.md @@ -0,0 +1,331 @@ +--- +title: Table State (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these examples: + +- [Basic createTable](../examples/basic-create-table) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) + +## Table State (Alpine) Guide + +> **If you boil TanStack Table down to one sentence: TanStack Table is a large state-management coordinator for table states.** + +Understanding this guide is fundamental to understanding how TanStack Table works and how to interact with it for the best results. + +### Do you need to Manage External State? + +You usually do NOT need to manage table state yourself. If you pass nothing to `initialState`, `atoms`, `state`, or any of the `on[State]Change` table options, TanStack Table will manage its own state internally. + +There will be situations where you need to customize how you interact with the internal table state, or even hoist it up to your own scopes. TanStack Table lets you read, subscribe to, or own the state slices that matter to your app. This guide explains how table state works in Alpine, how to read it, and when to use external atoms or external state. + +### State in v9 + +TanStack Table v9 overhauled state management around TanStack Store. TanStack Store uses the `alien-signals` implementation and supports performant derived state. + +A table instance has a few state surfaces: + +- `table.baseAtoms` are the internal writable atoms created from the resolved initial state. +- `table.atoms` are readonly derived atoms exposed per registered state slice. +- `table.store` is a readonly flat TanStack Store derived by putting all of the registered `table.atoms` together. + +The Alpine adapter provides `alpineReactivity()` to the table's `coreReactivityFeature`, so the atoms are backed directly by TanStack Store. `createTable` then makes the instance reactive to Alpine: it returns the table wrapped in a proxy and subscribes to `table.store`. Because of this, any reactive Alpine binding that reads a table API re-runs when state changes, whether that read is in `x-text`, `x-html`, `x-for`, `x-if`, a bound attribute (`:value`), `x-effect`, or a getter/method on your `Alpine.data` object. (Event handlers like `@click` are not reactive; they simply read the current value whenever they fire.) You do not pass a state selector, and there is no `table.Subscribe`: reactivity is automatic per binding. When any registered slice changes, Alpine re-evaluates the bindings that read table APIs and patches only the DOM that actually changed. + +### Feature-based State + +State slices are only created for the features that are registered in `features`. This keeps TanStack Table tree-shakeable and gives TypeScript more accurate state inference. + +```ts +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) + +table.atoms.pagination.get() +table.atoms.sorting.get() + +// table.atoms.rowSelection // TypeScript error unless rowSelectionFeature is registered +``` + +If `features` does not include a feature, its state should not be available in `table.atoms`, `table.store.get()`, `initialState`, `state`, or `atoms`. + +### Accessing Table State + +There are two different questions when reading table state: + +- Do you only need the current value? +- Or should the markup update when that value changes? + +Use direct atom reads for slice values. Use `table.store.get()` for the current flat state snapshot. Because the adapter makes table reads reactive, both update your markup automatically when read inside an Alpine binding. + +#### Reading State + +The simplest and most performant way to read a current state value is to read the matching atom: + +```ts +const pagination = table.atoms.pagination.get() +const sorting = table.atoms.sorting.get() +``` + +You can also read the current flat store snapshot: + +```ts +const tableState = table.store.get() +const pagination = table.store.get().pagination +``` + +Prefer `table.atoms..get()` for narrow reads. Use `table.store.get()` for full-state debug output or when a binding intentionally depends on the whole table state. + +#### Reading State Reactively in Markup + +Because the table instance is reactive, you read state directly in your Alpine expressions. There is nothing extra to subscribe to: each binding tracks the table reads inside it and re-runs when they change. + +```html + + Page + + of + + + + +

+```
+
+For derived values, expose a getter or method on your `Alpine.data` object. These run inside Alpine's reactivity, so reading a table API from them stays reactive:
+
+```ts
+Alpine.data('table', () => {
+  const local = Alpine.reactive({ data: makeData(1_000) })
+
+  const table = createTable({
+    features,
+    columns,
+    get data() {
+      return local.data
+    },
+  })
+
+  return {
+    table,
+    FlexRender,
+    // derived value used from the template
+    get pageCount() {
+      return table.getPageCount()
+    },
+    sortIndicator(isSorted: false | 'asc' | 'desc') {
+      return { asc: ' 🔼', desc: ' 🔽' }[isSorted as string] ?? ''
+    },
+  }
+})
+```
+
+### Setting Table State
+
+You should almost never need to set table state directly. TanStack Table features expose dedicated APIs for interacting with their state, and those APIs are the safest way to make changes.
+
+```ts
+table.nextPage()
+table.previousPage()
+table.setPageIndex(0)
+table.setPageSize(25)
+```
+
+Use APIs like `table.setSorting(...)`, `table.setColumnFilters(...)`, `column.toggleVisibility()`, or `row.toggleSelected()` instead of manually editing the underlying state object.
+
+If you only care about setting starting values, use `initialState`. If you want to reset a state slice back to its initial value, use that feature's reset API.
+
+If you really do need to write a state slice directly, the low-level write surface for internally owned state is the matching base atom:
+
+```ts
+table.baseAtoms.pagination.set((old) => ({
+  ...old,
+  pageIndex: 0,
+}))
+```
+
+Direct base atom writes should be rare. If a slice is owned by an external atom passed through `atoms`, write to that external atom instead; `table.atoms.pagination` will read from the external atom, not the internal base atom.
+
+### Custom Initial State
+
+If you only need to customize the starting value for some table state, use `initialState`. You still do not need to manage that state yourself.
+
+`initialState` only applies to registered state slices. It is used to create the table's initial state and is also used by reset APIs such as `table.resetSorting()` or `table.resetPagination()`. Changing the `initialState` object later does not reset table state.
+
+```ts
+const table = createTable({
+  features,
+  columns,
+  get data() {
+    return local.data
+  },
+  initialState: {
+    sorting: [
+      {
+        id: 'age',
+        desc: true,
+      },
+    ],
+    pagination: {
+      pageIndex: 0,
+      pageSize: 25,
+    },
+  },
+})
+```
+
+> [!NOTE]
+> Do not provide the same state slice in multiple ownership places unless you intentionally want one to win. For a slice like `pagination`, prefer exactly one of `initialState.pagination`, `atoms.pagination`, or `state.pagination` as the source of truth. The precedence is `atoms[key]` > `state[key]` > internal `baseAtoms[key]`: external atoms take precedence over external `state`, and external `state` syncs into the table's internal base atom.
+
+#### Resetting to Initial State
+
+Feature reset APIs reset to `table.initialState` by default. Many reset APIs also accept `true` to reset to that feature's blank/default state instead:
+
+```ts
+table.resetSorting()
+table.resetPagination()
+table.resetPagination(true)
+```
+
+Slice reset APIs like `resetPagination()` update through that feature's state updater and can update an externally owned atom. The core `table.reset()` API resets the internal base atoms, so do not use it as the primary way to reset state that is owned by external atoms.
+
+### Controlled State
+
+If you need easy access to table state in other parts of your application, you can control individual state slices. In Alpine, you have two options: own the slice in an external TanStack Store atom (good for sharing across modules or subscribing outside the table), or own it in `Alpine.reactive` state and connect it with `state` plus `on[State]Change`.
+
+#### External Atoms
+
+Use external atoms when the app should own one or more table state slices as TanStack Store atoms. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. Create stable writable atoms, pass them to the `atoms` option, and read, write, or subscribe to them from anywhere.
+
+```ts
+import { createAtom } from '@tanstack/store'
+import {
+  createTable,
+  rowPaginationFeature,
+  tableFeatures,
+  type PaginationState,
+} from '@tanstack/alpine-table'
+
+const features = tableFeatures({
+  rowPaginationFeature,
+})
+
+// Create stable external atoms at module scope (or in a shared store module)
+const paginationAtom = createAtom({
+  pageIndex: 0,
+  pageSize: 10,
+})
+
+Alpine.data('table', () => {
+  const local = Alpine.reactive({ data: makeData(1_000) })
+
+  const table = createTable({
+    features,
+    columns,
+    get data() {
+      return local.data
+    },
+    atoms: {
+      pagination: paginationAtom,
+    },
+  })
+
+  return { table, FlexRender }
+})
+```
+
+Reads and writes for `pagination` are now routed through `paginationAtom` instead of the internal base atom. Atom changes flow through the derived `table.store`, which the adapter subscribes to, so the template re-renders. You can also subscribe to the atom directly from anywhere with `paginationAtom.subscribe(...)`. When using the `atoms` option for a slice, you do not need to add the matching `on[State]Change` option.
+
+#### External State
+
+Use `state` plus `on[State]Change` when an `Alpine.reactive` object should own a table state slice. Read the controlled slices through getters inside `state`: that is what lets the adapter re-apply options when they change.
+
+```ts
+const local = Alpine.reactive({
+  data: makeData(1_000),
+  sorting: [] as SortingState,
+  pagination: { pageIndex: 0, pageSize: 10 },
+})
+
+const table = createTable({
+  features,
+  columns,
+  get data() {
+    return local.data
+  },
+  // connect our external state back down to the table via getters
+  state: {
+    get sorting() {
+      return local.sorting
+    },
+    get pagination() {
+      return local.pagination
+    },
+  },
+  onSortingChange: (updater) => {
+    // raise sorting state changes to our own state management
+    local.sorting =
+      typeof updater === 'function' ? updater(local.sorting) : updater
+  },
+  onPaginationChange: (updater) => {
+    // raise pagination state changes to our own state management
+    local.pagination =
+      typeof updater === 'function' ? updater(local.pagination) : updater
+  },
+})
+```
+
+Use the per-slice `on[State]Change` callbacks to keep controlled table state slices atomic and separated.
+
+##### On State Change Callbacks
+
+The `on[State]Change` callbacks are useful when you are controlling a matching slice through the `state` option. They work like setters: an updater can be a raw value or a function that receives the previous value and returns the next value.
+
+If you provide an `on[State]Change` callback, also provide the corresponding value in `state`. For example, `onSortingChange` should be paired with `state.sorting`.
+
+```ts
+onPaginationChange: (updater) => {
+  local.pagination =
+    updater instanceof Function ? updater(local.pagination) : updater
+
+  // side effects or validation can happen here
+}
+```
+
+### State Types
+
+Most complex states in TanStack Table have their own TypeScript types that you can import and use.
+
+```ts
+import {
+  createTable,
+  type PaginationState,
+  type RowSelectionState,
+  type SortingState,
+  type TableState,
+} from '@tanstack/alpine-table'
+
+const local = Alpine.reactive({
+  sorting: [{ id: 'age', desc: true }] as SortingState,
+})
+```
+
+`TableState` is inferred from the features registered on that table:
+
+```ts
+type MyTableState = TableState
+```
diff --git a/docs/framework/alpine/quick-start.md b/docs/framework/alpine/quick-start.md
new file mode 100644
index 0000000000..11ce1f8a18
--- /dev/null
+++ b/docs/framework/alpine/quick-start.md
@@ -0,0 +1,209 @@
+---
+title: Quick Start
+---
+
+TanStack Table is a headless table library. It manages your table's state and logic (sorting, filtering, pagination, selection, and more) while you keep 100% control over the markup and styles. This page gets you from install to a rendering Alpine table, then shows how to layer on your first feature.
+
+## Installation
+
+```bash
+npm install @tanstack/alpine-table alpinejs
+```
+
+The `@tanstack/alpine-table` package works with Alpine 3.
+
+## How the Alpine adapter works
+
+The adapter is built around two ideas:
+
+- **`createTable` returns a reactive table instance.** State lives in [TanStack Store](https://tanstack.com/store/latest) atoms that the adapter bridges into Alpine's reactivity. Any Alpine binding that reads a table API (`table.getRowModel()`, `table.atoms.sorting.get()`, and so on) re-runs automatically when the underlying state changes. There is no state selector to pass.
+- **You render with `x-html` and `table.FlexRender`.** A column's `cell`/`header`/`footer` renderer returns a string of HTML. `table.FlexRender({ cell })` produces that string and you place it with `x-html`.
+
+> [!IMPORTANT]
+> Alpine does not initialize directives (`@click`, `x-model`, etc.) inside content set with `x-html`. So render cell/header **content** with `x-html="table.FlexRender(...)"`, but put any **interactivity** (click-to-sort, filter inputs, checkboxes) on real elements in your markup, next to the `x-html` span. You will see this pattern in the sorting example below.
+
+## Your First Table
+
+You define the table in a JavaScript module with [`Alpine.data`](https://alpinejs.dev/globals/alpine-data), then render it from your HTML.
+
+```ts
+// main.ts
+import Alpine from 'alpinejs'
+import { FlexRender, createTable, tableFeatures } from '@tanstack/alpine-table'
+import type { ColumnDef } from '@tanstack/alpine-table'
+
+// 1. Define the shape of your data
+type Person = {
+  firstName: string
+  lastName: string
+  age: number
+}
+
+const defaultData: Array = [
+  { firstName: 'tanner', lastName: 'linsley', age: 24 },
+  { firstName: 'tandy', lastName: 'miller', age: 40 },
+  { firstName: 'joe', lastName: 'dirte', age: 45 },
+]
+
+// 2. New in v9: declare which features this table uses (none yet)
+const features = tableFeatures({})
+
+// 3. Define your columns. Renderers return HTML strings (rendered via x-html).
+const columns: Array> = [
+  {
+    accessorKey: 'firstName', // accessorKey shorthand
+    header: 'First Name',
+    cell: (info) => info.getValue(),
+  },
+  {
+    accessorFn: (row) => row.lastName, // accessorFn alternative with a custom id
+    id: 'lastName',
+    header: () => 'Last Name',
+    cell: (info) => `${info.getValue()}`,
+  },
+  {
+    accessorKey: 'age',
+    header: () => 'Age',
+  },
+]
+
+// 4. Register an Alpine component
+Alpine.data('table', () => {
+  // Store data in Alpine-reactive state so updates flow into the table
+  const local = Alpine.reactive({ data: defaultData })
+
+  // 5. Create the table instance, reading data through a getter so it stays reactive
+  const table = createTable({
+    features,
+    columns,
+    get data() {
+      return local.data
+    },
+  })
+
+  // Expose the instance (and FlexRender) to the template
+  return { table, FlexRender }
+})
+
+window.Alpine = Alpine
+Alpine.start()
+```
+
+```html
+
+
+ + + + + + + +
+
+ +``` + +A few things to note: + +- `tableFeatures({})` declares which optional features the table uses. Registering only what you need keeps bundles small and gives TypeScript accurate types for the table instance. +- The core row model is always included automatically. Feature row models (sorting, filtering, pagination) are registered as slots directly on the `tableFeatures({...})` call when you need them. +- The `get data()` getter keeps the table reactive. When `local.data` is reassigned, the table sees the new data. Passing `data: local.data` would capture a one-time snapshot. +- `FlexRender` is also attached to the instance as `table.FlexRender`, so you can write `x-html="table.FlexRender({ cell })"` instead of exposing the top-level helper. + +See the full [Basic createTable example](./examples/basic-create-table) for a runnable version with more columns and a footer. + +## Add a Feature: Sorting + +Features are opt-in in v9. To make columns sortable, register `rowSortingFeature` and the `sortedRowModel` factory in `tableFeatures`, then wire a header click handler. Because the click handler cannot live inside `x-html`, wrap the rendered header in a real element and attach `@click` there. + +```ts +// main.ts (additions) +import { + FlexRender, + createSortedRowModel, + createTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, // enables sorting APIs and state + sortedRowModel: createSortedRowModel(), // client-side sorting + sortFns, +}) + +// columns and the Alpine.data registration are otherwise unchanged +``` + +```html + + + + +``` + +Clicking a header now toggles between ascending, descending, and unsorted. Every other feature follows this same pattern: register the feature (and its row model factory as a slot on `tableFeatures` if it has one), then use the APIs it adds to the table, columns, and rows, attaching any interactive controls to real elements in your markup. See the [Sorting example](./examples/sorting) for custom sort functions, multi-sorting, and per-column options. + +## Where to Go Next + +**Table state.** In v9, table state is backed by TanStack Store atoms, which the adapter makes reactive in Alpine. You usually do not need to manage it yourself. Set `initialState` for starting values and call feature APIs like `table.setSorting(...)` or `table.nextPage()`. When you need to read a state slice in your markup, use `table.atoms..get()` (for example `table.atoms.pagination.get().pageIndex`) or `table.store.get()` for the whole state. There is no state selector, because the table instance is already reactive. The [Table State Guide](./guide/table-state.md) is the foundational guide for everything else. + +**Feature examples.** Each feature has a runnable example, such as [Column Filters](./examples/filters), [Pagination](./examples/pagination), [Row Selection](./examples/row-selection), and [Column Visibility](./examples/column-visibility). + +**Composable tables.** When multiple tables in your app share features and row models, define them once with `createTableHook`: + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { createAppTable, createAppColumnHelper } = createTableHook({ features }) +``` + +Then call `createAppTable({ columns, data })` from your component instead of `createTable`, and define columns with `createAppColumnHelper`. See the [Basic createAppTable example](./examples/basic-app-table) for the full pattern. + +**Examples.** Browse the runnable [Alpine examples](./examples/basic-create-table), from basic tables to feature demos, to see intended usage end to end. diff --git a/docs/framework/alpine/reference/functions/FlexRender-1.md b/docs/framework/alpine/reference/functions/FlexRender-1.md new file mode 100644 index 0000000000..1124ef2e62 --- /dev/null +++ b/docs/framework/alpine/reference/functions/FlexRender-1.md @@ -0,0 +1,47 @@ +--- +id: FlexRender +title: FlexRender +--- + +# Function: FlexRender() + +```ts +function FlexRender(props): any; +``` + +Defined in: [flexRender.ts:76](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/flexRender.ts#L76) + +Simplified wrapper of `flexRender`. Use this utility function to render headers, cells, or footers with custom markup. +Only one prop (`cell`, `header`, or `footer`) may be passed. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `unknown` = `unknown` + +## Parameters + +### props + +[`FlexRenderProps`](../type-aliases/FlexRenderProps.md)\<`TFeatures`, `TData`, `TValue`\> + +## Returns + +`any` + +## Example + +```html + + + +``` diff --git a/docs/framework/alpine/reference/functions/createTable.md b/docs/framework/alpine/reference/functions/createTable.md new file mode 100644 index 0000000000..03e6b51bb3 --- /dev/null +++ b/docs/framework/alpine/reference/functions/createTable.md @@ -0,0 +1,53 @@ +--- +id: createTable +title: createTable +--- + +# Function: createTable() + +```ts +function createTable(tableOptions, selector?): AlpineTable; +``` + +Defined in: [createTable.ts:46](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTable.ts#L46) + +Creates an Alpine-reactive table instance. + +Reactivity is bridged through a single version counter that every proxied +table read registers as a dependency, so by default ANY state change +re-evaluates every Alpine binding that touches the table. Pass a `selector` +to gate that: the counter then only bumps when the selected slice of state +changes (shallow compare). Use `() => ({})` to opt out of state-driven +re-evaluation entirely and handle high-frequency state (e.g. column +resizing) with explicit `table.atoms..subscribe()` side effects. +Options changes (e.g. new `data`) always re-evaluate. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +## Parameters + +### tableOptions + +`TableOptions`\<`TFeatures`, `TData`\> + +### selector? + +(`state`) => `unknown` + +## Returns + +[`AlpineTable`](../type-aliases/AlpineTable.md)\<`TFeatures`, `TData`\> + +## Example + +```ts +const table = createTable(options, (state) => ({ sorting: state.sorting })) +``` diff --git a/docs/framework/alpine/reference/functions/createTableHook.md b/docs/framework/alpine/reference/functions/createTableHook.md new file mode 100644 index 0000000000..409617cc11 --- /dev/null +++ b/docs/framework/alpine/reference/functions/createTableHook.md @@ -0,0 +1,76 @@ +--- +id: createTableHook +title: createTableHook +--- + +# Function: createTableHook() + +```ts +function createTableHook(__namedParameters): object; +``` + +Defined in: [createTableHook.ts:26](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTableHook.ts#L26) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +## Parameters + +### \_\_namedParameters + +[`CreateTableHookOptions`](../type-aliases/CreateTableHookOptions.md)\<`TFeatures`\> + +## Returns + +`object` + +### appFeatures + +```ts +appFeatures: TFeatures; +``` + +### createAppColumnHelper() + +```ts +createAppColumnHelper: () => ColumnHelper; +``` + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +#### Returns + +`ColumnHelper`\<`TFeatures`, `TData`\> + +### createAppTable() + +```ts +createAppTable: (tableOptions, selector?) => AppAlpineTable; +``` + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +#### Parameters + +##### tableOptions + +`Omit`\<`TableOptions`\<`TFeatures`, `TData`\>, `"features"`\> + +##### selector? + +(`state`) => `unknown` + +#### Returns + +[`AppAlpineTable`](../type-aliases/AppAlpineTable.md)\<`TFeatures`, `TData`\> diff --git a/docs/framework/alpine/reference/functions/flexRender.md b/docs/framework/alpine/reference/functions/flexRender.md new file mode 100644 index 0000000000..0c7178925b --- /dev/null +++ b/docs/framework/alpine/reference/functions/flexRender.md @@ -0,0 +1,45 @@ +--- +id: flexRender +title: flexRender +--- + +# Function: flexRender() + +```ts +function flexRender(render, props): any; +``` + +Defined in: [flexRender.ts:22](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/flexRender.ts#L22) + +Renders an Alpine table value with the provided context props. + +Use this lower-level helper for custom header, cell, or footer renderers when +you already have the render function and context. `FlexRender` is the +convenience wrapper for table cell/header/footer objects. Renderers typically +return a string of markup that you render into the DOM with `x-html`. + +## Type Parameters + +### TProps + +`TProps` *extends* `object` + +## Parameters + +### render + +`any` + +### props + +`TProps` + +## Returns + +`any` + +## Example + +```ts +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` diff --git a/docs/framework/alpine/reference/index.md b/docs/framework/alpine/reference/index.md new file mode 100644 index 0000000000..c5f4fea221 --- /dev/null +++ b/docs/framework/alpine/reference/index.md @@ -0,0 +1,21 @@ +--- +id: "@tanstack/alpine-table" +title: "@tanstack/alpine-table" +--- + +# @tanstack/alpine-table + +## Type Aliases + +- [AlpineTable](type-aliases/AlpineTable.md) +- [AppAlpineTable](type-aliases/AppAlpineTable.md) +- [AppColumnHelper](type-aliases/AppColumnHelper.md) +- [CreateTableHookOptions](type-aliases/CreateTableHookOptions.md) +- [FlexRenderProps](type-aliases/FlexRenderProps.md) + +## Functions + +- [createTable](functions/createTable.md) +- [createTableHook](functions/createTableHook.md) +- [flexRender](functions/flexRender.md) +- [FlexRender](functions/FlexRender-1.md) diff --git a/docs/framework/alpine/reference/type-aliases/AlpineTable.md b/docs/framework/alpine/reference/type-aliases/AlpineTable.md new file mode 100644 index 0000000000..6f797022eb --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/AlpineTable.md @@ -0,0 +1,40 @@ +--- +id: AlpineTable +title: AlpineTable +--- + +# Type Alias: AlpineTable\ + +```ts +type AlpineTable = Table & object; +``` + +Defined in: [createTable.ts:14](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTable.ts#L14) + +## Type Declaration + +### flexRender + +```ts +flexRender: typeof flexRender; +``` + +A lower-level helper to render the content of a cell, header, or footer from a render function and its context. + +### FlexRender + +```ts +FlexRender: typeof FlexRender; +``` + +A convenience helper to render a cell, header, or footer object. Call from `x-html`, e.g. `FlexRender({ header })`. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` diff --git a/docs/framework/alpine/reference/type-aliases/AppAlpineTable.md b/docs/framework/alpine/reference/type-aliases/AppAlpineTable.md new file mode 100644 index 0000000000..cb8e891da0 --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/AppAlpineTable.md @@ -0,0 +1,22 @@ +--- +id: AppAlpineTable +title: AppAlpineTable +--- + +# Type Alias: AppAlpineTable\ + +```ts +type AppAlpineTable = AlpineTable; +``` + +Defined in: [createTableHook.ts:16](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTableHook.ts#L16) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` diff --git a/docs/framework/alpine/reference/type-aliases/AppColumnHelper.md b/docs/framework/alpine/reference/type-aliases/AppColumnHelper.md new file mode 100644 index 0000000000..fbf273cbc5 --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/AppColumnHelper.md @@ -0,0 +1,22 @@ +--- +id: AppColumnHelper +title: AppColumnHelper +--- + +# Type Alias: AppColumnHelper\ + +```ts +type AppColumnHelper = ReturnType; +``` + +Defined in: [createTableHook.ts:21](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTableHook.ts#L21) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` diff --git a/docs/framework/alpine/reference/type-aliases/CreateTableHookOptions.md b/docs/framework/alpine/reference/type-aliases/CreateTableHookOptions.md new file mode 100644 index 0000000000..1c5dbdbcbf --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/CreateTableHookOptions.md @@ -0,0 +1,18 @@ +--- +id: CreateTableHookOptions +title: CreateTableHookOptions +--- + +# Type Alias: CreateTableHookOptions\ + +```ts +type CreateTableHookOptions = Omit, "columns" | "data" | "state">; +``` + +Defined in: [createTableHook.ts:11](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTableHook.ts#L11) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` diff --git a/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md b/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md new file mode 100644 index 0000000000..68970001dd --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md @@ -0,0 +1,59 @@ +--- +id: FlexRenderProps +title: FlexRenderProps +--- + +# Type Alias: FlexRenderProps\ + +```ts +type FlexRenderProps = + | { + cell: Cell; + footer?: never; + header?: never; +} + | { + cell?: never; + footer?: never; + header: Header; +} + | { + cell?: never; + footer: Header; + header?: never; +}; +``` + +Defined in: [flexRender.ts:49](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/flexRender.ts#L49) + +Simplified wrapper of `flexRender`. Use this utility function to render headers, cells, or footers with custom markup. +Only one prop (`cell`, `header`, or `footer`) may be passed. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` = `CellData` + +## Example + +```html + + + +``` + +This replaces calling `flexRender` directly like this: +```ts +flexRender(cell.column.columnDef.cell, cell.getContext()) +flexRender(header.column.columnDef.header, header.getContext()) +flexRender(footer.column.columnDef.footer, footer.getContext()) +``` diff --git a/docs/framework/angular/angular-table.md b/docs/framework/angular/angular-table.md deleted file mode 100644 index d992a7ecea..0000000000 --- a/docs/framework/angular/angular-table.md +++ /dev/null @@ -1,313 +0,0 @@ ---- -title: Angular Table ---- - -The `@tanstack/angular-table` adapter is a wrapper around the core table logic. Most of it's job is related to managing -state the "angular signals" way, providing types and the rendering implementation of cell/header/footer templates. - -## Exports - -`@tanstack/angular-table` re-exports all of `@tanstack/table-core`'s APIs and the following: - -### `createAngularTable` - -Accepts an options function or a computed value that returns the table options, and returns a table. - -```ts -import {createAngularTable} from '@tanstack/angular-table' - -export class AppComponent { - data = signal([]) - - table = createAngularTable(() => ({ - data: this.data(), - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - })) -} - -// ...render your table in template - -``` - -### `FlexRender` - -An Angular structural directive for rendering cell/header/footer templates with dynamic values. - -FlexRender supports any type of content supported by Angular: - -- A string, or a html string via `innerHTML` -- A [TemplateRef](https://angular.dev/api/core/TemplateRef) -- A [Component](https://angular.dev/api/core/Component) wrapped into `FlexRenderComponent` - -You can just use the `cell.renderValue` or `cell.getValue` APIs to render the cells of your table. However, -these APIs will only spit out the raw cell values (from accessor functions). -If you are using the `cell: () => any` column definition options, you will want to use the `FlexRenderDirective` from the adapter. - -Cell column definition is **reactive** and runs into an **injection context**, then you can inject services or make use of signals to automatically modify the rendered content. - -#### Example - -```ts -@Component({ - imports: [FlexRenderDirective], - //... -}) -class YourComponent {} -``` - -```angular-html - - -@for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getVisibleCells(); track cell.id) { - - - - {{ cell }} - -
-
- - } - -} - -``` - -#### Rendering a Component - -To render a Component into a specific column header/cell/footer, you can pass a `FlexRenderComponent` instantiated with -your `ComponentType, with the ability to include parameters such as inputs, outputs and a custom injector. - -```ts -import {flexRenderComponent} from "./flex-render-component"; -import {ChangeDetectionStrategy, input, output} from "@angular/core"; - -@Component({ - template: ` - ... - `, - standalone: true, - changeDetectionStrategy: ChangeDetectionStrategy.OnPush, - host: { - '(click)': 'clickEvent.emit($event)' - } -}) -class CustomCell { - readonly content = input.required(); - readonly cellType = input(); - - // An output that will emit for every cell click - readonly clickEvent = output(); -} - -class AppComponent { - columns: ColumnDef[] = [ - { - id: 'custom-cell', - header: () => { - const translateService = inject(TranslateService); - return translateService.translate('...'); - }, - cell: (context) => { - return flexRenderComponent( - MyCustomComponent, - { - injector, // Optional injector - inputs: { - // Mandatory input since we are using `input.required() - content: context.row.original.rowProperty, - // cellType? - Optional input - }, - outputs: { - clickEvent: () => { - // Do something - } - } - } - ) - }, - }, - ] -} -``` - -Underneath, this utilizes -the [ViewContainerRef#createComponent](https://angular.dev/api/core/ViewContainerRef#createComponent) api. -Therefore, you should declare your custom inputs using the @Input decorator or input/model signals. - -You can still access the table cell context through the `injectFlexRenderContext` function, which returns the context -value based on the props you pass to the `FlexRenderDirective`. - -```ts - -@Component({ - // ... -}) -class CustomCellComponent { - // context of a cell component - readonly context = injectFlexRenderContext>(); - // context of a header/footer component - readonly context = injectFlexRenderContext>(); -} -``` - -Alternatively, you can render a component into a specific column header, cell, or footer by passing the component type -to the corresponding column definitions. These column definitions will be provided to the `flexRender` directive along -with the `context`. - -```ts -class AppComponent { - columns: ColumnDef[] = [ - { - id: 'select', - header: () => TableHeadSelectionComponent, - cell: () => TableRowSelectionComponent, - }, - ] -} -``` - -```angular-html - - {{ headerCell }} - -``` - -Properties of `context` provided in the `flexRender` directive will be accessible to your component. -You can explicitly define the context properties required by your component. -In this example, the context provided to flexRender is of type HeaderContext. -Input signal `table`, which is a property of HeaderContext together with `column` and `header` properties, -is then defined to be used in the component. If any of the context properties are -needed in your component, feel free to use them. Please take note that only input signal is supported, -when defining access to context properties, using this approach. - -```angular-ts -@Component({ - template: ` - - `, - // ... -}) -export class TableHeadSelectionComponent { - //column = input.required>() - //header = input.required>() - table = input.required>() -} -``` - -#### Rendering a TemplateRef - -In order to render a TemplateRef into a specific column header/cell/footer, you can pass the TemplateRef into the column -definition. - -You can access the TemplateRef data via the `$implicit` property, which is valued based on what is passed in the props -field of flexRender. - -In most cases, each TemplateRef will be rendered with the $implicit context valued based on the cell type in this way: - -- Header: `HeaderContext` -- Cell: `CellContext`, -- Footer: `HeaderContext` - -```angular-html - - - - {{ cell }} - -
-
- - - - -``` - -Full example: - -```angular-ts -import type { - CellContext, - ColumnDef, - HeaderContext, -} from '@tanstack/angular-table' -import {Component, TemplateRef, viewChild} from '@angular/core' - -@Component({ - template: ` - - @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getVisibleCells(); track cell.id) { - - - - {{ cell }} - -
-
- - } - - } - - - - {{ context.getValue() }} - - - {{ context.getValue() }} - - `, -}) -class AppComponent { - customHeader = - viewChild.required }>>( - 'customHeader' - ) - customCell = - viewChild.required }>>( - 'customCell' - ) - - columns: ColumnDef[] = [ - { - id: 'customCell', - header: () => this.customHeader(), - cell: () => this.customCell(), - }, - ] -} -``` diff --git a/docs/framework/angular/guide/aggregation.md b/docs/framework/angular/guide/aggregation.md new file mode 100644 index 0000000000..8b61a0797a --- /dev/null +++ b/docs/framework/angular/guide/aggregation.md @@ -0,0 +1,269 @@ +--- +title: Aggregation (Angular) Guide +--- + +## Examples + +- [Aggregation](../examples/aggregation) +- [Grouped Aggregation](../examples/grouped-aggregation) + +Aggregation is independent from column grouping. Register `rowAggregationFeature` +whenever columns calculate totals or aggregated values. Add +`columnGroupingFeature` separately only when the table also groups rows. + +## Aggregation Setup + +Register only the built-in functions referenced by name. Passing a definition +directly to a column does not require a registry entry. + +```ts +import { + rowAggregationFeature, + aggregationFn_count, + aggregationFn_extent, + aggregationFn_mean, + aggregationFn_sum, + tableFeatures, + injectTable, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowAggregationFeature, + aggregationFns: { + count: aggregationFn_count, + extent: aggregationFn_extent, + mean: aggregationFn_mean, + sum: aggregationFn_sum, + }, +}) + +const table = injectTable(() => ({ + features, + columns, + data, +})) +``` + +The aggregation feature does not require a grouped row model. This makes grand +totals and custom row-subset totals available in otherwise ordinary tables. + +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + +## Column Aggregations + +A column accepts one aggregation or an array. A single entry returns a scalar; +multiple entries return an object keyed by the aggregation name or descriptor +`id`. + +```ts +columnHelper.accessor('amount', { + aggregationFn: 'sum', +}) + +columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], +}) +``` + +String values remain backward-compatible. Use descriptors when a result needs +a stable custom key or options. + +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + +## Grand Totals and Row Subsets + +Call `column.getAggregationValue()` without arguments to aggregate the default +pre-grouped row model. Filtering is included; grouping, sorting, expansion, and +pagination do not change that default total. + +```ts +footer: ({ column }) => column.getAggregationValue().toLocaleString() +``` + +Pass one options object with rows from any row model to choose a different set: + +```ts +column.getAggregationValue({ rows: table.getCoreRowModel().rows }) +column.getAggregationValue({ rows: table.getRowModel().rows }) +column.getAggregationValue({ rows: table.getFilteredSelectedRowModel().rows }) +column.getAggregationValue({ rows: table.getCoreRowModel().rows.slice(0, 3) }) +column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) +``` + +Depth is relative to the supplied row array. `0` selects those roots, `1` +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` + +## Grouped Aggregation + +Grouped aggregation composes two independent features. Register both, add the +grouped row-model slot, and configure aggregation functions on the columns that +should produce grouped values. + +```ts +const features = tableFeatures({ + rowAggregationFeature, + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), + aggregationFns: { sum: aggregationFn_sum }, +}) + +columnHelper.accessor('visits', { + aggregationFn: 'sum', + aggregatedCell: ({ getValue }) => getValue().toLocaleString(), + footer: ({ column }) => column.getAggregationValue().toLocaleString(), +}) +``` + +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. + +## Custom Aggregation Definitions + +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers + +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/angular/guide/cell-selection.md b/docs/framework/angular/guide/cell-selection.md new file mode 100644 index 0000000000..378969aa8c --- /dev/null +++ b/docs/framework/angular/guide/cell-selection.md @@ -0,0 +1,386 @@ +--- +title: Cell Selection (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Cell Selection](../examples/cell-selection) + +### Cell Selection Setup + +Here's how you set up your table to use cell selection features. Adding the cell selection feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + cellSelectionFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ cellSelectionFeature }) + +export class App { + readonly data = signal(defaultData) + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Cell Selection (Angular) Guide + +The cell selection feature keeps track of spreadsheet-style rectangular selections. A user can click a cell, drag across a block of cells, Shift-click to extend, and Ctrl/Cmd-drag to add or subtract a rectangle based on whether the starting cell is selected. Let's take a look at some common use cases. + +### Access Cell Selection State + +The table instance already manages the cell selection state for you. You can access the selection or values derived from it through a few APIs. + +- `table.atoms.cellSelection.get()` - returns the current cell selection (a signal read, so it tracks automatically in templates, `computed(...)`, and `effect(...)`) +- `getSelectedCellCount()` - returns how many cells are selected +- `getSelectedCellIds()` - returns the ids of every selected cell +- `getCellSelectionRowIds()` / `getCellSelectionColumnIds()` - returns the rows and columns the selection touches +- `getSelectedCellRangesData()` - returns each final positive selection region's values as a row-major grid + +```ts +console.log(table.atoms.cellSelection.get()) //get the cell selection state +console.log(table.getSelectedCellCount()) //3 +console.log(table.getSelectedCellIds()) //['0_firstName', '0_lastName', '1_firstName'] +console.log(table.getSelectedCellRangesData()) //[[['Tanner', 'Linsley'], ['Kevin', 'Vandy']]] +``` + +Reads of `table.atoms.cellSelection.get()` are tracked inside Angular reactive contexts, so they stay fresh automatically. Outside one, the same call is a plain snapshot. + +The expansion APIs (`getSelectedCellIds`, `getSelectedCellRangesData`) are memoized and pull-based. They cost nothing unless you actually call them, so a table that only highlights cells never pays to enumerate a large selection. + +### Cell Selection State Shape + +`CellSelectionState` is an ordered array of range operations, each stored as its two defining corners: + +```ts +type CellSelectionRange = { + anchorRowId: string + anchorColumnId: string + focusRowId: string + focusColumnId: string + operation?: 'include' | 'exclude' +} + +type CellSelectionState = Array +``` + +The `anchor` corner is where the selection started and stays put. The `focus` corner is the one that moves while dragging or Shift-extending. Storing both corners, rather than a normalized min/max rectangle, is what makes Shift-extend and "collapse back to the active cell" possible. + +Ranges are applied in order. An omitted `operation` is an inclusion for backward compatibility; an `exclude` range subtracts its rectangle from the selection produced so far. This compact operation log means a "select all except these cells" interaction does not build a map with one entry per selected cell. + +### Manage Cell Selection State + +If you need access to the selection elsewhere in your application, you can own the state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. + +```ts +import { createAtom } from '@tanstack/angular-store' +import { + injectTable, + tableFeatures, + cellSelectionFeature, + type CellSelectionState, +} from '@tanstack/angular-table' + +const features = tableFeatures({ cellSelectionFeature }) + +export class App { + readonly cellSelectionAtom = createAtom([]) + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + atoms: { cellSelection: this.cellSelectionAtom }, + })) +} +``` + +The classic controlled-state pattern also works: + +```ts +export class App { + readonly cellSelection = signal([]) + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + state: { cellSelection: this.cellSelection() }, + onCellSelectionChange: (updater) => { + this.cellSelection.update((old) => + typeof updater === 'function' ? updater(old) : updater, + ) + }, + })) +} +``` + +> [!NOTE] +> a drag emits one change per cell boundary the pointer crosses, so `onCellSelectionChange` fires repeatedly during a drag. If you are syncing selection to a server or a URL, debounce it or commit on `mouseup`. + +### Useful Row Ids + +Cell selection is keyed by row id and column id, so a meaningful row id matters here for the same reason it does with row selection. Use the `getRowId` table option to key selection by something stable from your data. + +```ts +readonly table = injectTable(() => ({ + features, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +})) +``` + +### Enable Cell Selection Conditionally + +Cell selection is enabled by default for every cell. Use the `enableCellSelection` table option to turn it off entirely, or pass a function for per-cell control. + +```ts +readonly table = injectTable(() => ({ + features, + //... + enableCellSelection: (cell) => cell.row.original.age > 18, //only adults' cells are selectable +})) +``` + +A column def can also opt out, which is the common case for checkbox or action columns. A column-level `false` wins over the table option. + +```ts +columnHelper.accessor('actions', { + enableCellSelection: false, //this column can never be selected +}) +``` + +A cell that cannot be selected is skipped even when a rectangle is drawn straight through it, and `moveCellSelection` steps over its column rather than landing on it. Use `cell.getCanSelect()` to decide whether to attach selection handlers in your UI. + +### Mouse Interactions + +Two cell handlers drive every mouse interaction: + +- `cell.getSelectionStartHandler()` - bind to `onMouseDown` +- `cell.getSelectionExtendHandler()` - bind to `onMouseEnter` + +```html + + + {{ renderCell }} + + +``` + +You do not need to handle `mouseup` yourself. The start handler attaches its own document-level `mouseup` listener and removes it when the drag ends, so releasing the pointer outside the table still finishes the drag correctly. If your table renders into another document, such as an iframe or a popout window, pass that document in: `cell.getSelectionStartHandler(myDocument)`. + +#### Drag Selection + +Pressing down on a cell starts a new single-cell range, and every cell the pointer then enters moves that range's focus corner. Set `enableCellSelectionDrag: false` to require explicit clicks instead. + +#### Shift Range Selection + +Shift-clicking moves the active range's focus corner to the clicked cell, keeping its anchor fixed. The active cell therefore stays where the selection started, matching spreadsheet behavior. + +The handler recognizes Shift when the event exposes either `event.shiftKey` or `event.nativeEvent.shiftKey`. You can disable range behavior or replace the detection: + +```ts +readonly table = injectTable(() => ({ + features, + //... + enableCellRangeSelection: false, + + // For example, use the platform modifier instead of Shift: + // isCellRangeSelectionEvent: event => Boolean(event.metaKey), +})) +``` + +#### Multiple Ranges + +Ctrl-clicking or Cmd-clicking an unselected cell adds a new inclusive rectangle. Starting the same modified interaction on a selected cell adds an exclusion instead, so clicking removes that cell and dragging subtracts the whole rectangle. Whether the drag includes or excludes is fixed when it starts; shrinking an exclusion drag restores cells that leave its rectangle. Set `enableMultiCellRangeSelection: false` to disable both behaviors, or override `isMultiCellRangeSelectionEvent` to change the modifier. + +#### Programmatic Range Operations + +`table.selectCellRange(range)` replaces the current selection. Pass `{ mode: 'include' }` to append an inclusion or `{ mode: 'exclude' }` to append an exclusion. The older `{ additive: true }` option remains as a deprecated alias for include mode; `mode` wins if both options are supplied. `table.getCellSelectionBounds()` resolves the operation log into deterministic, disjoint positive rectangles. + +### Render Cell Selection UI + +TanStack Table does not dictate how you render selected cells. These cell APIs give you everything you need: + +- `cell.getIsSelected()` - whether this cell falls inside any range +- `cell.getIsFocused()` - whether this is the active cell (an excluded anchor can be focused without being selected) +- `cell.getSelectionEdges()` - which sides sit on the selection boundary +- `cell.getTabIndex()` - `0` for the focused cell and `-1` otherwise, for roving tabindex + +`getSelectionEdges()` returns `{ top, right, bottom, left }`, where a side is `true` when the neighboring cell in that direction is not itself selected. That is what lets you draw a single continuous outline around a selection, including around a union of separate rectangles, without every cell inspecting its neighbors. + +```tsx +function getCellClassName(cell) { + // most cells are unselected, so bail before asking for edges + if (!cell.getIsSelected()) { + return cell.getIsFocused() ? 'cell cell-focused' : 'cell' + } + + const edges = cell.getSelectionEdges() + + return [ + 'cell', + 'cell-selected', + cell.getIsFocused() && 'cell-focused', + edges.top && 'cell-edge-top', + edges.right && 'cell-edge-right', + edges.bottom && 'cell-edge-bottom', + edges.left && 'cell-edge-left', + ] + .filter(Boolean) + .join(' ') +} +``` + +> [!TIP] +> draw the outline with `box-shadow: inset ...` rather than `border`. On a `border-collapse` table a thicker border widens the shared grid line, which makes rows change height as cells become selected. A box-shadow never affects layout. + +### Keyboard Navigation + +Cell selection ships no keyboard handling of its own. Instead it exposes imperative APIs so a dedicated library, such as [TanStack Hotkeys](https://tanstack.com/hotkeys), can drive it: + +- `table.moveCellSelection(direction)` - collapse the selection to a single cell one step away +- `table.extendCellSelection(direction)` - move the active range's focus corner, keeping its anchor +- `table.setFocusedCell(rowId, columnId)` - collapse the selection to one specific cell +- `table.selectAllCells()` - select every selectable cell +- `table.resetCellSelection(true)` - clear the selection + +`direction` is `'up'`, `'down'`, `'left'`, or `'right'`. + +```ts +import { injectHotkeys } from '@tanstack/angular-hotkeys' + +export class App { + readonly grid = viewChild>('grid') + + constructor() { + injectHotkeys( + [ + { + hotkey: 'ArrowUp', + callback: () => this.table.moveCellSelection('up'), + }, + { + hotkey: 'ArrowDown', + callback: () => this.table.moveCellSelection('down'), + }, + { + hotkey: 'Shift+ArrowDown', + callback: () => this.table.extendCellSelection('down'), + }, + { hotkey: 'Mod+A', callback: () => this.table.selectAllCells() }, + { + hotkey: 'Escape', + callback: () => this.table.resetCellSelection(true), + }, + ], + () => ({ target: this.grid()?.nativeElement ?? null }), + ) + } +} +``` + +Scope the hotkeys to the grid element rather than the document, or arrow keys and Escape will hijack inputs elsewhere on the page. + +### Copying a Selection + +`getSelectedCellRangesData()` returns raw values indexed as `[regionIndex][rowIndex][columnIndex]`. A region is one of the final disjoint positive rectangles after all include and exclude operations are applied, so it does not necessarily correspond one-to-one with stored state. Turning that into clipboard text is left to your application, because the delimiter, the representation of `null`, and any quoting rules are decisions only you can make. + +```ts +function escapeTsvValue(value: unknown) { + const text = value == null ? '' : String(value) + const safeText = + typeof value === 'string' && /^[\t\r ]*[=+@-]/.test(value) + ? `'${text}` + : text + // spreadsheets expect a quoted field once it contains a delimiter, a newline, + // or a quote, with inner quotes doubled + return /["\t\n\r]/.test(safeText) + ? `"${safeText.replace(/"/g, '""')}"` + : safeText +} + +function toTsv(ranges: Array>>) { + return ranges + .map((grid) => + grid.map((row) => row.map(escapeTsvValue).join('\t')).join('\n'), + ) + .join('\n\n') +} + +navigator.clipboard.writeText(toTsv(table.getSelectedCellRangesData())) +``` + +### How Ranges Survive Table Changes + +Ranges store row and column ids, not positions, so they follow their corner cells rather than screen coordinates. + +- **Sorting, filtering, and column reordering** keep the corners pinned and recompute what sits between them. A range from "row A to row B" still runs from A to B after a sort, even though different rows now fall in between. +- **Column pinning** is accounted for in render order, so a rectangle stays visually contiguous when a column is pinned. +- **Hiding a column** that a corner sits on makes the range inert. Nothing renders as selected, but the range stays in state and comes back when the column is shown again. +- **Pagination** resolves against the pre-pagination order, so a range can span pages and lights up correctly on whichever page you are viewing. + +Because a reorder can widen a selection onto columns the user never picked, some applications prefer to clear the selection whenever the column layout changes. That is a userland decision; an Angular effect can implement it: + +```ts +let isFirstLayout = true + +effect(() => { + // read the atoms so this tracks only the layout slices + this.table.atoms.columnOrder.get() + this.table.atoms.columnPinning.get() + this.table.atoms.columnVisibility.get() + + untracked(() => { + if (isFirstLayout) { + isFirstLayout = false + return + } + this.table.resetCellSelection(true) + }) +}) +``` + +### Resetting Cell Selection + +`table.resetCellSelection()` restores `initialState.cellSelection`. Pass `true` to ignore initial state and clear the selection entirely. + +The selection also resets automatically whenever `data` changes, because new data can invalidate the row ids a range points at, or silently re-select cells if the new data happens to reuse ids. Turn that off with `autoResetCellSelection: false`, and note that `autoResetAll` overrides it. + +```ts +readonly table = injectTable(() => ({ + features, + //... + autoResetCellSelection: false, //keep ranges across data changes +})) +``` + +### Performance + +Angular's signals track these reads, so a selection change marks only the +components that actually read it. There is no equivalent of React's per-row +`Subscribe` to reach for here, and the example renders its cells plainly. + +Measured on a table with a thousand rows and twelve columns, a drag updates in +roughly 15ms per move with plain reads and `ChangeDetectionStrategy.OnPush`. + +The per-cell reads are cheap by design. `cell.getIsSelected()` resolves the +cell's row and column index and compares them against a memoized cache of the +selection bounds, which is a handful of integer comparisons. If a very large +table does become a bottleneck, reach for +[virtualization](./virtualization) so that only visible rows exist in the DOM, +rather than for a subscription pattern. + +Note that Angular flushes change detection asynchronously, so a test that reads +the DOM synchronously after a click can beat the update. Poll the assertion +rather than reading once. diff --git a/docs/framework/angular/guide/cell-spanning.md b/docs/framework/angular/guide/cell-spanning.md new file mode 100644 index 0000000000..eac2651d61 --- /dev/null +++ b/docs/framework/angular/guide/cell-spanning.md @@ -0,0 +1,141 @@ +--- +title: Cell Spanning (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Cell Spanning](../examples/cell-spanning) + +### Cell Spanning Setup + +Here's how you set up your table to use cell spanning features. Adding the cell spanning feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + cellSpanningFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ cellSpanningFeature }) + +export class App { + readonly data = signal(defaultData) + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Cell Spanning (Angular) Guide + +The cell spanning feature merges adjacent body cells into one rendered cell, the way `rowspan` and `colspan` merge cells in a plain HTML table or a spreadsheet. Row spans are derived from the data: adjacent rows that share a value in an opted-in column merge into one vertically spanning cell. Column spans are declared per row for things like full-width summary rows. + +The feature is stateless. Spans are always recomputed from the rows that are actually rendered, so sorting, filtering, pagination, and row pinning simply change which rows are adjacent and the spans follow. There is nothing to persist and nothing to reset. + +### Enable Row Spanning per Column + +Opt a column into value-based row spanning with `spanRows` on its column def: + +```ts +const columns = [ + columnHelper.accessor('region', { + spanRows: true, // adjacent rows with equal region values merge + }), +] +``` + +`spanRows: true` merges adjacent rows whose values are the same value, compared with `Object.is`. Nullish values never merge under the default comparison, since a merged block of blanks reads as a rendering bug and joins semantically unrelated rows. + +Pass a predicate to control run boundaries yourself. The run is anchored: every candidate row is tested against the run's first row, which keeps runs transitive by construction. + +```ts +columnHelper.accessor('createdAt', { + spanRows: ({ anchorValue, value }) => + sameMonth(anchorValue as Date, value as Date), +}) +``` + +### Rendering Spanned Cells + +A covered cell reports a span of `0`, and the renderer skips it. This is the same convention as [`header.rowSpan`](../../../guide/headers#header-row-spanning). + +```html + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + @if (cell.getRowSpan() > 0 && cell.getColSpan() > 0) { + + +
+
+ + } } + + } + +``` + +`cell.getIsCovered()` is a convenience for the same check, so `@if (!cell.getIsCovered())` also works when you do not need the span numbers separately. + +### Column Spanning and Summary Rows + +Declare horizontal spans with `spanColumns` on the column that should carry the merged content. The count is resolved per row and measured in the order columns actually render, so hidden columns are not counted and column reordering is handled for you. + +```ts +columnHelper.accessor('label', { + spanColumns: ({ row }) => (row.original.isSummary ? Infinity : 1), +}) +``` + +Values larger than the available room are clamped to the end of the cell's pinned region, so `Infinity` means "the rest of my region". A column span can never cross the boundary between start-pinned, center, and end-pinned columns. + +When a cell spans rows and columns at once, the merged block is a rectangle: the anchor cell reports both spans and every other cell in the rectangle reports `0` on at least one axis. Cells only join a vertical run when their column spans match, so a full-width summary row never merges into the data run above it. + +### Spanning and Sorting, Filtering, and Pagination + +Spans are derived from the final row model, never stored, so every row model change recomputes them: + +- Sorting changes adjacency. Sorting by the spanned column clusters equal values and produces the largest runs; sorting by an unrelated column usually shatters them. +- Filtering removes rows. When a filter removes the middle of a run, the remaining neighbors become adjacent and merge. +- Pagination clips runs. A run never crosses a page boundary; the next page opens a fresh cell even when the value continues. +- Pinned rows render in separate sections, so a run never crosses a pinned section boundary either. + +### Disable Cell Spanning + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + enableCellSpanning: false, // document-wide kill switch +})) + +columnHelper.accessor('status', { + enableCellSpanning: false, // per-column opt out +}) +``` + +### Selecting Merged Cells + +`cellSelectionFeature` composes with cell spanning. When both features are registered, a selection rectangle expands to fully enclose every merged cell it touches, so a merge is always entirely selected or entirely unselected. This applies to subtractions too: excluding any part of a merge deselects the whole merge. Arrow-key navigation treats a merge as a single stop, `getSelectedCellCount()` counts a merge once, and `getSelectedCellIds()` returns only the cells that render. `getSelectedCellRangesData()` still returns the full row-major lattice grid, since covered cells carry real underlying values. + +The expansion happens when the selection bounds are derived, not when the selection is stored. Stored corners stay stable while sorting, paging, or toggling `enableCellSpanning` changes which cells merge; the derived selection follows the current spans. + +### Known Limitations + +- Row virtualization needs extra care: if a run's anchor row is scrolled out of the rendered window, the covered rows render nothing. Read `table.getCellSpanIndex()` to find the anchor and render a clamped span at the top of the window. +- Grouped columns ignore `spanRows`, since grouping already collapses repeated values into group rows, and grouped rows never join a run in any column. +- Footer groups and `` rendering are unaffected by cell spanning. diff --git a/docs/framework/angular/guide/column-faceting.md b/docs/framework/angular/guide/column-faceting.md new file mode 100644 index 0000000000..c0a91d11f0 --- /dev/null +++ b/docs/framework/angular/guide/column-faceting.md @@ -0,0 +1,338 @@ +--- +title: Faceting (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Faceted Filters](../examples/filters-faceted) +- [Bucketed Faceted Filters](../examples/filters-faceted-bucketed) + +### Faceting Setup + +Here's how you set up your table to use faceting features. Adding the faceting feature enables the related APIs. If you use client-side faceting, also set up `filteredRowModel` and `facetedRowModel` after their features, since row model slots are type-checked. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnFacetingFeature, + columnFilteringFeature, + createFacetedRowModel, + createFacetedUniqueValues, + createFacetedMinMaxValues, + createFilteredRowModel, + filterFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + facetedRowModel: createFacetedRowModel(), // if using client-side faceting + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + filterFns, +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Faceting (Angular) Guide + +### What is Faceting? + +Faceting derives information that can be used to build filtering interfaces. For a given column, faceting can answer questions such as: + +- Which values are available? +- How often does each value occur? +- What is the minimum and maximum value among the available rows? +- Which rows should be used for a custom facet calculation? + +For example, an application could use faceting to render a plan filter like this: + +```text +Plan +☐ Free 128 +☐ Pro 47 +☐ Enterprise 9 +``` + +The plan names and counts are derived from the table's faceted row model. If a filter on another column changes, such as `Region = Europe`, the plan counts update to describe only the rows in that region. + +Faceting does not apply filters to the table. It provides values, counts, ranges, or rows that you can use to build a filter UI. The column filtering feature owns the filter state and determines which rows match the selected filter values. + +#### Faceting vs Row Aggregation + +Faceting and row aggregation both summarize data, but they serve different purposes. Faceting produces metadata for filter controls, such as available values, occurrence counts, or a numeric range. Row aggregation computes result values over a set of rows, such as a sum, average, or total, for display in footers or grouped rows. + +Faceted counts do not create aggregate rows or use a column's `aggregationFn`. A useful way to distinguish the features is: + +- Filtering answers: Which rows remain? +- Faceting answers: Which filtering choices remain? +- Row aggregation answers: What summary value can be calculated from these rows? + +### How Column Faceting Responds to Filters + +A column's faceted row model includes rows that pass every applicable filter except that column's own filter. This lets a facet continue to show alternative choices while the user edits it. + +Consider a table with `Region` and `Plan` filters: + +1. The user selects `Region = Europe`. +2. The `Plan` facet applies the region filter and recalculates its plan counts. +3. The user selects `Plan = Pro`. +4. The table displays only European Pro rows. +5. The `Plan` facet still calculates its choices from all European rows because it excludes its own `Plan` filter. + +Other facets do apply the selected plan filter. For example, a `Status` facet would now describe only European Pro rows. This is what allows multiple facets to narrow each other. + +Client-side faceting needs both `filteredRowModel` and `facetedRowModel` to provide this behavior. Without a filtered row model, the faceted row model falls back to the pre-filtered rows, so its values will not react to other column filters. + +### Faceting APIs + +Use the faceting API that matches the filter interface you are building: + +| API | Result | Common uses | +| --------------------------------- | --------------------------------------- | ------------------------------------------------------ | +| `column.getFacetedRowModel()` | Rows that pass the other active filters | Custom facet calculations | +| `column.getFacetedUniqueValues()` | A `Map` of values to occurrence counts | Checkboxes, select menus, and autocomplete suggestions | +| `column.getFacetedMinMaxValues()` | A `[min, max]` tuple or `undefined` | Number inputs and range sliders | + +The row model factories registered in `tableFeatures` enable these APIs: + +- `createFacetedRowModel()` is required for client-side faceting. +- `createFacetedUniqueValues()` is required for unique values and counts. +- `createFacetedMinMaxValues()` is required for numeric minimum and maximum values. + +Register only the factories your table uses. The complete setup near the top of this guide registers all three. + +### Unique Values and Counts + +`column.getFacetedUniqueValues()` returns a `Map` whose keys are facet values and whose values are occurrence counts. You can turn that map into a sorted list for an autocomplete or select control: + +```ts +const suggestions = Array.from(column.getFacetedUniqueValues().entries()) + .sort(([valueA], [valueB]) => String(valueA).localeCompare(String(valueB))) + .slice(0, 5_000) +``` + +Each entry contains both the value and its count: + +```html + +``` + +For a scalar column, each row normally contributes one value, so the occurrence count is also a row count. A row can contribute more than one facet value by defining the column's `getUniqueValues` option. In that case, the counts describe occurrences and their total can be greater than the number of rows. + +```ts +columnHelper.accessor('tags', { + header: 'Tags', + getUniqueValues: (row) => row.tags, +}) +``` + +If you want each count to represent rows, make sure `getUniqueValues` returns each value no more than once per row. + +### Reactive Facet Controls in Angular + +Derive facet values with an Angular `computed` signal inside the component that renders the controls. Calling a faceting API from the computed function tracks the adapter's reactive table state. + +```ts +@Component({ + selector: 'app-facet-options', + template: ` + @for (entry of values(); track entry[0]) { + + } + `, +}) +export class FacetOptions { + readonly column = input.required>() + readonly selected = computed( + () => (this.column().getFilterValue() ?? []) as Array, + ) + readonly values = computed(() => + Array.from(this.column().getFacetedUniqueValues().entries()).map( + ([value, count]) => [String(value), count] as const, + ), + ) + + isSelected(value: string): boolean { + return this.selected().includes(value) + } + + toggleValue(value: string): void { + const selected = this.selected() + this.column().setFilterValue( + selected.includes(value) + ? selected.filter((selectedValue) => selectedValue !== value) + : [...selected, value], + ) + } +} +``` + +The filter function for the column still determines how the selected values match rows. See the [Column Filtering Guide](./column-filtering) for filter functions and filter state, or the [Faceted Filters example](../examples/filters-faceted) for a complete implementation. + +### Minimum and Maximum Values + +`column.getFacetedMinMaxValues()` returns the numeric range available after applying the other active filters. It returns `undefined` when there are no numeric values. + +```ts +readonly range = computed( + () => this.column().getFacetedMinMaxValues() ?? [0, 1], +) +``` + +```html + +``` + +The minimum and maximum describe the values that are available to the filter UI. Your column's filter function determines how a selected value or range filters rows. + +### Bucketed Faceting for Continuous Values + +Raw unique values are not always useful. Dates, file sizes, durations, prices, and measurements can produce hundreds or thousands of distinct values. These columns are often easier to filter when their values are placed into meaningful buckets: + +```text +Last login +☐ Today +☐ Yesterday +☐ This week +☐ This month +☐ Older +``` + +You can use the column's `getUniqueValues` option to return a bucket key for faceting while keeping the original accessor value for rendering and other table features. + +```ts +type StorageBucket = + 'under-1-gb' | '1-to-10-gb' | '10-to-100-gb' | '100-gb-plus' + +const GB = 1024 ** 3 + +function getStorageBucket(value: number): StorageBucket { + if (value < GB) return 'under-1-gb' + if (value < 10 * GB) return '1-to-10-gb' + if (value < 100 * GB) return '10-to-100-gb' + return '100-gb-plus' +} + +const storageBucketFilter = constructFilterFn({ + resolveDataValue: (value) => getStorageBucket(value as number), + filter: (bucket, selected: Array) => selected.includes(bucket), + autoRemove: (selected: Array) => selected.length === 0, +}) + +columnHelper.accessor('storageBytes', { + header: 'Storage', + getUniqueValues: (row) => [getStorageBucket(row.storageBytes)], + filterFn: storageBucketFilter, +}) +``` + +Faceting and filtering should use the same bucket definitions so the displayed counts match the rows selected by each bucket. The column keeps its raw numeric value, so there is no need to create a hidden derived column only for faceting. See the [Bucketed Faceted Filters example](../examples/filters-faceted-bucketed) for complete date and storage bucket filters. + +### Client-Side Faceting and Performance + +The built-in client-side faceting row models are memoized. They recalculate when their input rows or relevant filter state changes. The cost still depends on the number of rows, columns, and unique values in the table. + +For columns with many unique values, consider these options: + +- Render only the first or most relevant values instead of every map entry. +- Let users search the available values before rendering a long list. +- Bucket continuous or high-cardinality values into useful ranges. +- Move faceting to the server when the complete dataset is not available in the browser. + +Avoid sorting or converting a large facet map repeatedly in unrelated components. Derive and render facet options close to the reactive component that consumes the relevant filter state. + +### Custom Server-Side Faceting + +When filtering is performed on the server, the rows loaded into the browser may not contain enough information to calculate complete facet values or counts. In that case, calculate the facets on the server and provide custom `facetedUniqueValues` and `facetedMinMaxValues` factories. + +Each factory receives the table and a column ID, then returns a function that resolves the faceted result. The regular column APIs will return the server-provided values. + +Factories are resolved once per table and column, but the function each factory returns runs on every read; the table does not cache its result. Read live values inside that returned function (from a signal, store, or `table.options.meta`) so updated server facets show up immediately, and memoize inside the factory if the calculation is expensive. + +```ts +const serverFacets = signal(initialServerFacets) + +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + return new Map(serverFacets().uniqueValues[columnId] ?? []) + }, + facetedMinMaxValues: (_table, columnId) => () => { + return serverFacets().minMaxValues[columnId] + }, +}) + +export class App { + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +To match the built-in column faceting behavior, a server query for one column should apply the other active filters but exclude that column's own filter. This keeps alternative choices available in the current facet while allowing facets to narrow each other. + +You can also fetch facet values and pass them directly to your filter components without using the TanStack Table faceting APIs. + +### Global Faceting + +Global faceting derives values across every leaf column that can participate in global filtering. It is useful for autocomplete suggestions or other metadata associated with a global filter. The global faceted row model applies active column filters and excludes the global filter itself. + +If the table uses global filtering, register `globalFilteringFeature` so the row filtering pipeline evaluates the global filter. The same faceting factories used by column faceting also power these table APIs: + +```ts +const globalFacetedRows = table.getGlobalFacetedRowModel().flatRows + +const suggestions = Array.from(table.getGlobalFacetedUniqueValues().entries()) + +const [min, max] = table.getGlobalFacetedMinMaxValues() ?? [0, 1] +``` + +Custom faceting factories receive the internal `__global__` column ID for global requests. You can branch on that ID when the server returns separate column and global facet results: + +```ts +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + if (columnId === '__global__') { + return new Map(globalFacets.uniqueValues) + } + + return new Map(columnFacets[columnId]?.uniqueValues) + }, +}) +``` diff --git a/docs/framework/angular/guide/column-filtering.md b/docs/framework/angular/guide/column-filtering.md new file mode 100644 index 0000000000..31115f06dc --- /dev/null +++ b/docs/framework/angular/guide/column-filtering.md @@ -0,0 +1,468 @@ +--- +title: Column Filtering (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Bucketed Faceted Filters](../examples/filters-faceted-bucketed) +- [Fuzzy Search](../examples/filters-fuzzy) + +### Column Filtering Setup + +Here's how you set up your table to use column filtering features. Adding the column filtering feature enables the related APIs. If you use client-side filtering, also set up `filteredRowModel` after its feature, since row model slots are type-checked. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFn_includesString, + filterFn_inNumberRange, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +> [!NOTE] +> The `filterFns` registry above lists only the built-in filter functions this table uses. Spreading the entire built-in `filterFns` registry (`filterFns: { ...filterFns }`) still works, but it puts every built-in filter function in your bundle. Register just the functions you use, or pass a function directly to the `filterFn` column option with no registration at all. + +## Column Filtering (Angular) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on column filtering, which is a filter that is applied to a single column's accessor value. + +TanStack table supports both client-side and manual server-side filtering. This guide will go over how to implement and customize both, and help you decide which one is best for your use-case. + +### Client-Side vs Server-Side Filtering + +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. + +The client-side filtered row model also invokes the page-index auto-reset hook when column filtering inputs change. Whether the page index resets depends on the `autoResetPageIndex`, `autoResetAll`, and `manualPagination` options. If filtering is manual and this row model is omitted or bypassed, a column filter state change does not invoke that hook, so reset server-side pagination in the filter change handler when needed. + +### Manual Server-Side Filtering + +If you have decided that you need to implement server-side filtering instead of using the built-in client-side filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to features, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +const features = tableFeatures({ columnFilteringFeature }) + +readonly table = injectTable(() => ({ + features, + data, + columns, + manualFiltering: true, +})) +``` + +> [!NOTE] +> When using manual filtering, many of the options that are discussed in the rest of this guide will have no effect. When `manualFiltering` is set to `true`, the table instance will not apply any filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the `data` that you pass to it as-is. + +### Client-Side Filtering + +If you are using the built-in client-side filtering features, add the `columnFilteringFeature` and the `filteredRowModel` factory to your features. Import `createFilteredRowModel` and the filter functions you need from TanStack Table: + +```ts +import { + injectTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFn_includesString, + filterFn_inNumberRange, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +readonly table = injectTable(() => ({ + features, + data, + columns, +})) +``` + +### Column Filter State + +Whether or not you use client-side or server-side filtering, you can take advantage of the built-in column filter state management that TanStack Table provides. There are many table and column APIs to mutate and interact with the filter state and retrieve the column filter state. + +The column filtering state is defined as an array of objects with the following shape: + +```ts +interface ColumnFilter { + id: string + value: unknown +} +type ColumnFiltersState = ColumnFilter[] +``` + +Since the column filter state is an array of objects, you can have multiple column filters applied at once. + +#### Accessing Column Filter State + +You can access the column filter state from the table instance with `table.atoms.columnFilters.get()`. In Angular, table atom reads are signal reads, so reading the atom in a template expression, `computed(...)`, or `effect(...)` automatically tracks updates. Use `table.store.get()` only when you need a flat snapshot of the whole state, such as debug JSON. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + //... +})) + +// signal-reactive in templates, computed(...), and effect(...) +this.table.atoms.columnFilters.get() +``` + +However, if you need access to the column filter state outside of the table, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state in other parts of your application, you can own the column filter state slice yourself. The recommended way in v9 is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the filter values can be used elsewhere (such as in a query key for server-side filtering) without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' + +export class App { + readonly columnFiltersAtom = createAtom([]) // can set initial column filter state here + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + //... + atoms: { + columnFilters: this.columnFiltersAtom, // table filter APIs now update columnFiltersAtom + }, + })) + + // read the atom wherever you need the value (e.g. for a query key) + // this.columnFiltersAtom.get() +} +``` + +Alternatively, the v8-style `state.columnFilters` plus `onColumnFiltersChange` pattern is still supported. In Angular this means owning the slice with an Angular signal, as shown in the [Basic External State example](../examples/basic-external-state). It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly columnFilters = signal([]) +//... +readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + //... + state: { + columnFilters: this.columnFilters(), + }, + onColumnFiltersChange: (updater) => + typeof updater === 'function' + ? this.columnFilters.update(updater) + : this.columnFilters.set(updater), +})) +``` + +#### Initial Column Filter State + +If you do not need to control the column filter state in your own state management or scope, but you still want to set an initial column filter state, you can use the `initialState` table option instead of `state`. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +})) +``` + +> [!NOTE] +> Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 18 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `startsWith` - Case-insensitive string prefix match +- `endsWith` - Case-insensitive string suffix match +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `empty` - The row's value is nullish or whitespace-only (the filter value is an on/off flag) +- `notEmpty` - The row's value is not nullish or whitespace-only (the filter value is an on/off flag) +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `inDateRange` - Inclusive `[min, max]` date range accepting `Date` objects, timestamps, or date strings (blank endpoints are open-ended) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom filter functions, either inline as the `filterFn` column option, or by name in the `filterFns` registry slot on `tableFeatures`. + +#### Custom Filter Functions + +> [!NOTE] +> These filter functions only run during client-side filtering. + +Whether you register a custom filter function in the `filterFns` slot on `tableFeatures` or pass it directly as a `filterFn` column option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = ( + row, // Row + columnId: string, + filterValue: any, + addMeta?: (meta: FilterMeta) => void, +): boolean => ... +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // reference a custom filter function registered in features + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + } +] +//... +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }, +}) + +readonly table = injectTable(() => ({ + features, + columns, + data, +})) +``` + +> **TypeScript Note:** For `filterFn: 'myCustomFilterFn'` string references to typecheck, register the function in the `filterFns` slot on `tableFeatures` (as shown above). Alternatively, skip the registry entirely by passing the function directly to the `filterFn` column option. See the [Fuzzy Search example](../examples/filters-fuzzy) for a complete registration example. + +##### Customize Filter Function Behavior + +You can attach a few other properties to filter functions to customize their behavior: + +- `filterFn.resolveFilterValue` - This optional "hanging" method on any given `filterFn` allows the filter function to transform/sanitize/format the filter value before it is passed to the filter function. The table applies it once per filter (not once per row), so it is also the right place for expensive preparation work. + +- `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. + +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. + +The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: + +```ts +const startsWithFilterFn = constructFilterFn({ + // compare the (resolved) row value against the (resolved) filter value + filter: (dataValue, filterValue) => + Boolean(dataValue?.startsWith(filterValue)), + // normalize the filter value once, before any rows are tested + resolveFilterValue: (value) => String(value).toLowerCase().trim(), + // normalize each row's value before it reaches the comparator + resolveDataValue: (value) => String(value ?? '').toLowerCase(), + // remove the filter value from filter state if it is falsy (empty string in this case) + autoRemove: (value) => !value, +}) +``` + +Keeping the comparison in `filter` and the normalization in the resolvers pays off when you need a variant of an existing filter function. The definition is attached to the returned function, so you can spread any filter function built with `constructFilterFn` and override only what differs. For example, a version of `includesString` that also ignores diacritics (so a search for "eric" matches "Éric"): + +```ts +const normalize = (value: unknown) => + String(value ?? '') + .toLowerCase() + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + +const includesStringIgnoreDiacritics = constructFilterFn({ + ...filterFn_includesString, // reuse the comparator and autoRemove behavior + resolveFilterValue: normalize, + resolveDataValue: normalize, +}) +``` + +Register the variant by name in the `filterFns` registry or pass it directly to the `filterFn` column option, just like any other custom filter function. + +> [!NOTE] +> The table applies `resolveFilterValue` once per filter before any rows are tested. If you ever call a filter function directly (outside of a table), resolve the filter value yourself: `myFilterFn(row, columnId, myFilterFn.resolveFilterValue?.(rawValue) ?? rawValue)`. + +### Customize Column Filtering + +There are a lot of table and column options that you can use to further customize the column filtering behavior. + +#### Disable Column Filtering + +By default, column filtering is enabled for all columns. You can disable the column filtering for all columns or for specific columns by using the `enableColumnFilters` table option or the `enableColumnFilter` column option. You can also turn off both column and global filtering by setting the `enableFilters` table option to `false`. + +Disabling column filtering for a column will cause the `column.getCanFilter` API to return `false` for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + columns, + data, + enableColumnFilters: false, // disable column filtering for all columns +})) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +readonly table = injectTable(() => ({ + features, + columns, + data, + filterFromLeafRows: true, // filter and search through sub-rows +})) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +readonly table = injectTable(() => ({ + features, + columns, + data, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +})) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `onChange` or `onBlur` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/angular/guide/column-ordering.md b/docs/framework/angular/guide/column-ordering.md new file mode 100644 index 0000000000..dda502d1d1 --- /dev/null +++ b/docs/framework/angular/guide/column-ordering.md @@ -0,0 +1,202 @@ +--- +title: Column Ordering (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Ordering](../examples/column-ordering) + +### Column Ordering Setup + +Here's how you set up your table to use column ordering features. Adding the column ordering feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnOrderingFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnOrderingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Column Ordering (Angular) Guide + +By default, columns are ordered in the order they are defined in the `columns` array. However, you can manually specify the column order using the `columnOrder` state. Other features like column pinning and grouping can also affect the column order. + +### What Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual **Column Ordering** - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +> [!NOTE] +> `columnOrder` state will only affect unpinned columns if used in conjunction with column pinning. + +### Column Order State + +If you don't provide a `columnOrder` state, TanStack Table will just use the order of the columns in the `columns` array. However, you can provide an array of string column ids to the `columnOrder` state to specify the order of the columns. + +#### Default Column Order + +If all you need to do is specify the initial column order, you can just specify the `columnOrder` state in the `initialState` table option. + +```ts +const features = tableFeatures({ columnOrderingFeature }) + +readonly table = injectTable(() => ({ + features, + //... + initialState: { + columnOrder: ['columnId1', 'columnId2', 'columnId3'], + }, + //... +})) +``` + +> [!NOTE] +> If you are using the `state` table option to also specify the `columnOrder` state, the `initialState` will have no effect. Only specify particular states in either `initialState` or `state`, not both. + +#### Managing Column Order State + +If you need to dynamically change the column order, or set the column order after the table has been initialized, you can manage the `columnOrder` state just like any other table state. + +In v9, the recommended way to own a state slice is with an external atom (created with `createAtom` from `@tanstack/angular-store`) passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the column order without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import { + injectTable, + tableFeatures, + columnOrderingFeature, +} from '@tanstack/angular-table' +import type { ColumnOrderState } from '@tanstack/angular-table' + +const features = tableFeatures({ columnOrderingFeature }) + +export class App { + readonly columnOrderAtom = createAtom([ + 'columnId1', + 'columnId2', + 'columnId3', + ]) + + readonly table = injectTable(() => ({ + features, + //... + atoms: { + columnOrder: this.columnOrderAtom, + }, + //... + })) + + // read this.columnOrderAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.columnOrder` plus `onColumnOrderChange` pattern is still supported. In Angular this means owning the slice with an Angular signal. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnOrderingFeature }) + +readonly columnOrder = signal(['columnId1', 'columnId2', 'columnId3']) +//... +readonly table = injectTable(() => ({ + features, + //... + state: { + columnOrder: this.columnOrder(), + //... + }, + onColumnOrderChange: (updater) => + typeof updater === 'function' + ? this.columnOrder.update(updater) + : this.columnOrder.set(updater), + //... +})) +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, hook the drop event of your drag-and-drop solution up to `table.setColumnOrder`. With Angular CDK's [drag-drop](https://material.angular.dev/cdk/drag-drop/overview) module (the same module the official [Row DnD example](../examples/row-dnd) uses to reorder rows), a horizontal `cdkDropList` over the header cells can drive the column order with `moveItemInArray`: + +One prerequisite: the default `columnOrder` state is an empty array, and `moveItemInArray` on an empty array does nothing. Seed the order with the full list of column ids first, either in `initialState` (as below), in an external atom, or in your own signal. + +```ts +import { moveItemInArray } from '@angular/cdk/drag-drop' +import type { CdkDragDrop } from '@angular/cdk/drag-drop' + +const features = tableFeatures({ columnOrderingFeature }) + +export class App { + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + initialState: { + // seed the order with every column id so there is something to reorder + columnOrder: columns.map((column) => column.id!), + }, + })) + + // reorder columns after drag & drop + dropColumn(event: CdkDragDrop>) { + this.table.setColumnOrder((prevColumnOrder) => { + const newColumnOrder = [...prevColumnOrder] + moveItemInArray(newColumnOrder, event.previousIndex, event.currentIndex) + return newColumnOrder + }) + } +} +``` + +Once the order is initialized, `table.setColumnOrder` works the same whether the table manages the `columnOrder` state internally, you control it with `state` + `onColumnOrderChange`, or you own it with an external atom. + +### Column Ordering APIs + +Use `table.setColumnOrder` to update the column order state directly. Use `table.resetColumnOrder` to reset the order to `initialState.columnOrder`, or pass `true` to clear the order state. + +```ts +table.setColumnOrder(['lastName', 'firstName', 'age']) +table.resetColumnOrder() +table.resetColumnOrder(true) +``` + +Columns expose helpers for reading their current position after column pinning, manual ordering, and grouping have been applied. + +```ts +column.getIndex() +column.getIndex('start') +column.getIndex('center') +column.getIndex('end') + +column.getIsFirstColumn() +column.getIsLastColumn() +``` + +These helpers are useful for styling column boundaries or building drag-and-drop targets that need to know the current rendered order. + +#### Drag and Drop Column Reordering Suggestions (Angular) + +TanStack Table is not opinionated about which drag-and-drop solution you use. Here are a few suggestions: + +1. Use [Angular CDK drag-drop](https://material.angular.dev/cdk/drag-drop/overview) (`@angular/cdk/drag-drop`) if you want a library. It is what the official Angular [Row DnD example](../examples/row-dnd) uses (`CdkDropList`, `CdkDrag`, and the `moveItemInArray` utility), it is maintained by the Angular team, and it handles touch support for you. There is no official Angular column DnD example yet, but the same directives work for reordering header cells in a horizontal drop list. + +2. Consider native browser drag events (`dragstart`, `dragenter`, `dragend`) with your own signal or atom state if you want zero dependencies. This can be very lightweight, but you will need to do extra work for proper touch support on mobile. + +3. If you evaluate other DnD libraries, check their maintenance status, framework compatibility, bundle size, and how well they handle semantic `` markup before committing. Many popular DnD libraries (such as DnD Kit) are React-only and will not work with Angular. diff --git a/docs/framework/angular/guide/column-pinning.md b/docs/framework/angular/guide/column-pinning.md new file mode 100644 index 0000000000..8f9a6af3e3 --- /dev/null +++ b/docs/framework/angular/guide/column-pinning.md @@ -0,0 +1,206 @@ +--- +title: Column Pinning (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) + +### Column Pinning Setup + +Here's how you set up your table to use column pinning features. Adding the column pinning feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnPinningFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnPinningFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Column Pinning (Angular) Guide + +TanStack Table offers state and APIs helpful for implementing column pinning features in your table UI. You can implement column pinning in multiple ways. You can either split pinned columns into their own separate tables, or you can keep all columns in the same table, but use the pinning state to order the columns correctly and use sticky CSS to pin the columns to the start or end. + +`start` and `end` are logical pinning regions. In LTR languages/layouts, `start` usually corresponds to left and `end` to right. In RTL languages/layouts, `start` usually corresponds to right and `end` to left. + +### How Column Pinning Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. **Column Pinning** - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +The only way to change the order of the pinned columns is in the `columnPinning.start` and `columnPinning.end` state itself. `columnOrder` state will only affect the order of the unpinned ("center") columns. + +### Column Pinning State + +Managing the `columnPinning` state is optional, and usually not necessary unless you are adding persistent state features. TanStack Table will already keep track of the column pinning state for you. Manage the `columnPinning` state just like any other table state if you need to. + +In v9, the recommended way to own a state slice is with an external atom (created with `createAtom` from `@tanstack/angular-store`) passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import { + injectTable, + tableFeatures, + columnPinningFeature, +} from '@tanstack/angular-table' +import type { ColumnPinningState } from '@tanstack/angular-table' + +const features = tableFeatures({ columnPinningFeature }) + +export class App { + readonly columnPinningAtom = createAtom({ + start: [], + end: [], + }) + + readonly table = injectTable(() => ({ + features, + //... + atoms: { + columnPinning: this.columnPinningAtom, + }, + //... + })) + + // read this.columnPinningAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.columnPinning` plus `onColumnPinningChange` pattern is still supported. In Angular this means owning the slice with an Angular signal. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly columnPinning = signal({ + start: [], + end: [], +}) + +readonly table = injectTable(() => ({ + features, + //... + state: { + columnPinning: this.columnPinning(), + //... + }, + onColumnPinningChange: (updater) => + typeof updater === 'function' + ? this.columnPinning.update(updater) + : this.columnPinning.set(updater), + //... +})) +``` + +### Pin Columns by Default + +A very common use case is to pin some columns by default. You can do this by either initializing the `columnPinning` state with the pinned columnIds, or by using the `initialState` table option: + +```ts +readonly table = injectTable(() => ({ + features, + //... + initialState: { + columnPinning: { + start: ['expand-column'], + end: ['actions-column'], + }, + //... + }, + //... +})) +``` + +### Useful Column Pinning APIs + +> [!NOTE] +> These APIs are available when using `columnPinningFeature`. + +There are a handful of useful Column API methods to help you implement column pinning features: + +- `column.getCanPin`: Use to determine if a column can be pinned. +- `column.pin`: Use to pin a column to the start or end. Or use to unpin a column. +- `column.getIsPinned`: Use to determine where a column is pinned. +- `column.getPinnedIndex`: Use to read the column's index within its pinned column group. +- `column.getStart`: Use to provide the correct `start` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `end` CSS value for a pinned column. +- `column.getIsLastColumn`: Use to determine if a column is the last column in its pinned group. Useful for adding a box-shadow. +- `column.getIsFirstColumn`: Use to determine if a column is the first column in its pinned group. Useful for adding a box-shadow. + +Use `table.setColumnPinning` to update the pinning state directly. Use `table.resetColumnPinning` to reset to `initialState.columnPinning`, or pass `true` to clear both pinned column arrays. + +```ts +table.setColumnPinning({ + start: ['firstName'], + end: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```ts +table.getStartLeafColumns() +table.getCenterLeafColumns() +table.getEndLeafColumns() + +table.getStartVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getEndVisibleLeafColumns() + +table.getStartHeaderGroups() +table.getCenterHeaderGroups() +table.getEndHeaderGroups() + +table.getStartFooterGroups() +table.getCenterFooterGroups() +table.getEndFooterGroups() + +table.getStartFlatHeaders() +table.getCenterFlatHeaders() +table.getEndFlatHeaders() + +table.getStartLeafHeaders() +table.getCenterLeafHeaders() +table.getEndLeafHeaders() +``` + +You can also request pinned leaf columns by region with `table.getPinnedLeafColumns(position)` and visible pinned leaf columns with `table.getPinnedVisibleLeafColumns(position)`. + +```ts +table.getPinnedLeafColumns('start') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('end') + +table.getPinnedVisibleLeafColumns('start') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('end') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'start'` or `'end'` to check one pinned side. + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getStartHeaderGroups`, `table.getCenterHeaderGroups`, `table.getEndHeaderGroups`, `row.getStartVisibleCells`, `row.getCenterVisibleCells`, and `row.getEndVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/angular/guide/column-resizing.md b/docs/framework/angular/guide/column-resizing.md new file mode 100644 index 0000000000..b56e288fd9 --- /dev/null +++ b/docs/framework/angular/guide/column-resizing.md @@ -0,0 +1,257 @@ +--- +title: Column Resizing (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) + +### Column Resizing Setup + +Here's how you set up your table to use column resizing features. Column resizing depends on column sizing, so add `columnSizingFeature` before `columnResizingFeature`. Adding the column resizing feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnSizingFeature, + columnResizingFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Column Resizing (Angular) Guide + +TanStack Table provides built-in column resizing state and APIs for implementing column resizing in your table UI with a variety of options for UX and performance. + +Column resizing builds on column sizing. If you only need to define starting, minimum, or maximum widths, see the [Column Sizing Guide](./column-sizing). + +### Enable Column Resizing + +To use column resizing, add `columnSizingFeature` and then `columnResizingFeature` to your features. The `column.getCanResize()` API will return `true` by default for all columns, but you can either disable column resizing for all columns with the `enableColumnResizing` table option, or disable column resizing on a per-column basis with the `enableResizing` column option. + +```ts +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + injectTable, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns = [ + { + accessorKey: 'id', + enableResizing: false, // disable resizing for just this column + size: 200, // starting column size + }, + //... +] + +readonly table = injectTable(() => ({ + features, + columns, + data, +})) +``` + +### Column Resize Mode + +By default, the column resize mode is set to `"onEnd"`. This means that the `column.getSize()` API will not return the new column size until the user has finished resizing (dragging) the column. Usually a small UI indicator will be displayed while the user is resizing the column. + +Even though the Angular adapter wires table state to signals, every drag frame in `"onChange"` mode updates the `columnSizing` state, and any template that calls `column.getSize()` in every header and data cell recomputes on each frame. For large or complex tables, the `"onEnd"` column resize mode can be a good default option to avoid stuttering or lagging while the user resizes columns. That is not to say that you cannot achieve 60 fps column resizing in Angular, but for big tables you may need to compute column widths once per frame in a `computed(...)` and apply them as CSS variables instead of reading sizes cell by cell. + +> Advanced column resizing performance tips will be discussed [down below](#advanced-column-resizing-performance). + +If you want to change the column resize mode to `"onChange"` for immediate column resizing renders, you can do so with the `columnResizeMode` table option. + +```ts +readonly table = injectTable(() => ({ + //... + columnResizeMode: 'onChange', // change column resize mode to "onChange" +})) +``` + +### Column Resize Direction + +By default, TanStack Table assumes that the table markup is laid out in a left-to-right direction. For right-to-left layouts, you may need to change the column resize direction to `"rtl"`. + +```ts +readonly table = injectTable(() => ({ + //... + columnResizeDirection: 'rtl', // change column resize direction to "rtl" for certain locales +})) +``` + +### Connect Column Resizing APIs to UI + +There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI. + +#### Column Size APIs + +To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs: + +```ts +header.getSize() +column.getSize() +cell.column.getSize() +``` + +How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes. + +```html + +``` + +Though, as discussed in the [advanced column resizing performance section](#advanced-column-resizing-performance), you may want to consider using CSS variables to apply column sizes to your markup. + +#### Column Resize APIs + +TanStack Table provides a pre-built event handler to make your drag interactions easy to implement. These event handlers are just convenience functions that call other internal APIs to update the column sizing state and re-render the table. Use `header.getResizeHandler()` to connect to your column resize drag interactions, for both mouse and touch events. + +```html +
+``` + +#### Column Resize Indicator with Column Resizing State + +TanStack Table keeps track of a `columnResizing` state object that you can use to render a column resize indicator UI. + +```html +
+``` + +The `columnResizing` state stores transient drag information: + +```ts +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +You rarely need to manage this transient drag state yourself, but if you do, the recommended v9 approach is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can observe the resize state without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import type { columnResizingState } from '@tanstack/angular-table' + +export class App { + readonly columnResizingAtom = createAtom({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, + }) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + atoms: { + columnResizing: this.columnResizingAtom, + }, + })) + + // read this.columnResizingAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.columnResizing` plus `onColumnResizingChange` pattern is still supported. In Angular this means owning the slice with an Angular signal. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly columnResizing = signal({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +readonly table = injectTable(() => ({ + features, + columns, + data, + state: { + columnResizing: this.columnResizing(), + }, + onColumnResizingChange: (updater) => + typeof updater === 'function' + ? this.columnResizing.update(updater) + : this.columnResizing.set(updater), +})) +``` + +### Column Resizing APIs + +Use `header.getResizeHandler()` to connect mouse or touch events to the resizing logic. Use `column.getCanResize()` to decide whether to render a resize handle, and `column.getIsResizing()` to render active resizing UI. + +```ts +header.getResizeHandler() +column.getCanResize() +column.getIsResizing() +``` + +The table instance exposes APIs for the transient resize state through `table.setColumnResizing`. + +```ts +table.setColumnResizing((old) => ({ + ...old, + deltaOffset: 12, +})) + +table.resetHeaderSizeInfo() +table.resetHeaderSizeInfo(true) +``` + +### Advanced Column Resizing Performance + +If every header and data cell in your template reads `column.getSize()`, every cell binding recomputes on each `"onChange"` drag frame. The [performant column resizing example](../examples/column-resizing-performant) shows how to reduce a drag to a single style update using signals. + +1. **Compute all column widths in one `computed`.** Read `table.atoms.columnSizing.get()` to track sizing changes, then build the CSS variable map inside `untracked(...)` so no other reads register as dependencies. Bind the result once on the `
+ +
` element (`[style]="tableStyle()"`). +2. **Reference the variables in cell styles** with `[style.width]="'calc(var(--col-' + cell.column.id + '-size) * 1px)'"`, so a resize only updates the variables on the `
` element instead of re-evaluating per-cell width bindings. +3. **Keep components on `ChangeDetectionStrategy.OnPush`** so Angular only re-checks templates whose tracked signals actually changed. + +Because nothing in the table body reads resize state for its width, no per-cell recomputation happens during a drag. diff --git a/docs/framework/angular/guide/column-sizing.md b/docs/framework/angular/guide/column-sizing.md new file mode 100644 index 0000000000..d8e1685ede --- /dev/null +++ b/docs/framework/angular/guide/column-sizing.md @@ -0,0 +1,179 @@ +--- +title: Column Sizing (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Sizing](../examples/column-sizing) + +### Column Sizing Setup + +Here's how you set up your table to use column sizing features. Adding the column sizing feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnSizingFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnSizingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Column Sizing (Angular) Guide + +The column sizing feature lets you optionally set the width of each column including min and max widths. + +If you want users to dynamically change column widths by dragging column headers, see the [Column Resizing Guide](./column-resizing). + +### Column Widths + +Columns by default are given the following measurement options: + +```ts +export const defaultColumnSizing = { + size: 150, + minSize: 20, + maxSize: Number.MAX_SAFE_INTEGER, +} +``` + +These defaults can be overridden by both `tableOptions.defaultColumn` and individual column defs, in that order. + +```ts +const features = tableFeatures({ columnSizingFeature }) + +const columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +readonly table = injectTable(() => ({ + features, + defaultColumn: { + size: 200, // starting column size + minSize: 50, // enforced during column resizing + maxSize: 500, // enforced during column resizing + }, + //... +})) +``` + +The column "sizes" are stored in the table state as numbers, and are usually interpreted as pixel unit values, but you can hook up these column sizing values to your css styles however you see fit. + +As a headless utility, table logic for column sizing is really only a collection of states that you can apply to your own layouts how you see fit (our example above implements 2 styles of this logic). You can apply these width measurements in a variety of ways: + +- semantic `table` elements or any elements being displayed in a table css mode +- `div/span` elements or any elements being displayed in a non-table css mode + - Block level elements with strict widths + - Absolutely positioned elements with strict widths + - Flexbox positioned elements with loose widths + - Grid positioned elements with loose widths +- Really any layout mechanism that can interpolate cell widths into a table structure. + +Each of these approaches has its own tradeoffs and limitations which are usually opinions held by a UI/component library or design system, luckily not you 😉. + +### Column Sizing APIs + +Use the column and header APIs to read the calculated size and offsets for rendering. These values come from the `columnSizing` state and the column definition defaults. + +```ts +column.getSize() +header.getSize() + +column.getStart() // start offset in the current column flow +column.getStart('start') +column.getStart('center') +column.getStart('end') + +column.getAfter() // end offset in the current column flow +column.getAfter('start') +column.getAfter('center') +column.getAfter('end') + +column.resetSize() +``` + +The table instance also exposes total size helpers. These are useful when building scroll containers, split pinned-column tables, or CSS variables for column widths. + +```ts +table.getTotalSize() +table.getStartTotalSize() +table.getCenterTotalSize() +table.getEndTotalSize() +``` + +If you need to update sizing state directly, use `table.setColumnSizing`. Use `table.resetColumnSizing` to reset to `initialState.columnSizing`, or pass `true` to reset to the feature default. + +```ts +table.setColumnSizing({ + firstName: 180, + age: 80, +}) + +table.resetColumnSizing() +table.resetColumnSizing(true) +``` + +### Managing Column Sizing State + +If you need to own the `columnSizing` state yourself (for example, to persist user-set column widths), the recommended v9 approach is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the sizing state without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import type { ColumnSizingState } from '@tanstack/angular-table' + +const features = tableFeatures({ columnSizingFeature }) + +export class App { + readonly columnSizingAtom = createAtom({}) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + atoms: { + columnSizing: this.columnSizingAtom, + }, + })) + + // read this.columnSizingAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.columnSizing` plus `onColumnSizingChange` pattern is still supported. In Angular this means owning the slice with an Angular signal. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnSizingFeature }) + +readonly columnSizing = signal({}) + +readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + state: { + columnSizing: this.columnSizing(), + }, + onColumnSizingChange: (updater) => + typeof updater === 'function' + ? this.columnSizing.update(updater) + : this.columnSizing.set(updater), +})) +``` diff --git a/docs/framework/angular/guide/column-visibility.md b/docs/framework/angular/guide/column-visibility.md new file mode 100644 index 0000000000..508fcdaaf3 --- /dev/null +++ b/docs/framework/angular/guide/column-visibility.md @@ -0,0 +1,193 @@ +--- +title: Column Visibility (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Visibility](../examples/column-visibility) + +### Column Visibility Setup + +Here's how you set up your table to use column visibility features. Adding the column visibility feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnVisibilityFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Column Visibility (Angular) Guide + +The column visibility feature allows table columns to be hidden or shown dynamically. In v9, add `columnVisibilityFeature` to your `features` to enable this. There is a dedicated `columnVisibility` state and APIs for managing column visibility dynamically. + +### Column Visibility State + +The `columnVisibility` state is a map of column IDs to boolean values. A column will be hidden if its ID is present in the map and the value is `false`. If the column ID is not present in the map, or the value is `true`, the column will be shown. + +If you need to own the `columnVisibility` state yourself (for example, to persist user preferences), the recommended v9 approach is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the visibility state without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import { + injectTable, + tableFeatures, + columnVisibilityFeature, +} from '@tanstack/angular-table' +import type { ColumnVisibilityState } from '@tanstack/angular-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +export class App { + readonly columnVisibilityAtom = createAtom({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }) + + readonly table = injectTable(() => ({ + features, + //... + atoms: { + columnVisibility: this.columnVisibilityAtom, + }, + })) + + // read this.columnVisibilityAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.columnVisibility` plus `onColumnVisibilityChange` pattern is still supported. In Angular this means owning the slice with an Angular signal. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnVisibilityFeature }) + +readonly columnVisibility = signal({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +readonly table = injectTable(() => ({ + features, + //... + state: { + columnVisibility: this.columnVisibility(), + //... + }, + onColumnVisibilityChange: (updater) => + typeof updater === 'function' + ? this.columnVisibility.update(updater) + : this.columnVisibility.set(updater), +})) +``` + +Alternatively, if you don't need to manage the column visibility state outside of the table, you can still set the initial default column visibility state using the `initialState` option. + +> [!NOTE] +> If `columnVisibility` is provided to both `initialState` and `state`, the `state` initialization will take precedence and `initialState` will be ignored. Do not provide `columnVisibility` to both `initialState` and `state`, only one or the other. + +```ts +const features = tableFeatures({ columnVisibilityFeature }) + +readonly table = injectTable(() => ({ + features, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +})) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```ts +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessorKey: 'name', // can be hidden + }, +] +``` + +### Column Visibility Toggle APIs + +There are several column API methods that are useful for rendering column visibility toggles in the UI. + +- `column.getCanHide` - Useful for disabling the visibility toggle for a column that has `enableHiding` set to `false`. +- `column.getIsVisible` - Useful for setting the initial state of the visibility toggle. +- `column.toggleVisibility` - Useful for toggling the visibility of a column. +- `column.getToggleVisibilityHandler` - Shortcut for hooking up the `column.toggleVisibility` method to a UI event handler. + +```html +@for (column of table.getAllLeafColumns(); track column.id) { + +} +``` + +### Column Visibility Aware Table APIs + +When you render your header, body, and footer cells, there are a lot of API options available. You may see APIs like `table.getAllLeafColumns` and `row.getAllCells`, but if you use these APIs, they will not take column visibility into account. Instead, you need to use the "visible" variants of these APIs, such as `table.getVisibleLeafColumns` and `row.getVisibleCells`. + +```html +
+ + + @for (column of table.getVisibleLeafColumns(); track column.id) { + + } + + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + } + + } + +
{{ column.id }}
+ {{ renderCell }} +
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/angular/guide/composable-tables.md b/docs/framework/angular/guide/composable-tables.md new file mode 100644 index 0000000000..1046aeb445 --- /dev/null +++ b/docs/framework/angular/guide/composable-tables.md @@ -0,0 +1,320 @@ +--- +title: Composable Tables (createTableHook) Guide +--- + +`createTableHook` creates an app-specific table factory. Use it to define shared features, row models, and default table options once, then create each Angular table with the columns and data that are unique to that table. + +The same API can also register reusable table, cell, and header components, but component registration is optional. Start with shared options and features first; add reusable components only when your app needs standardized table UI pieces. + +## Examples + +- [Basic App Table](../examples/basic-app-table) - Minimal `createTableHook` usage without the larger component registry. +- [Composable Tables](../examples/composable-tables) - Richer Users and Products tables sharing `src/app/table.ts` and reusable components. + +## Start With Shared Features and Options + +Create one app table hook and put the feature set, row models, and shared defaults there. This example makes sorting available to every table created by `injectAppTable`. + +```ts +import { + createSortedRowModel, + createTableHook, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { injectAppTable, createAppColumnHelper } = createTableHook({ + features, + debugTable: true, + enableSortingRemoval: false, +}) +``` + +Options passed to `createTableHook` become defaults for every table created by `injectAppTable`. The `features` option is also bound to the returned column helper, so column definitions know that sorting APIs are available. + +## Create App Columns + +Create one column helper per row type. The helper is already bound to your app's feature set, so each table does not need to thread `typeof features` through its column definitions. + +```ts +type Person = { + firstName: string + lastName: string + age: number + visits: number +} + +const columnHelper = createAppColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), +]) +``` + +## Create A Table + +Create each table with `injectAppTable`. The call site provides table-specific inputs such as `columns` and `data`; shared features and defaults come from the hook. + +```ts +export class UsersTable { + readonly data = signal>([]) + + readonly table = injectAppTable(() => ({ + key: 'users-table', + columns, + data: this.data(), + })) +} +``` + +## Render With The Normal Table APIs + +You can render the table with the same table instance APIs used by a standalone `injectTable` table. This simple path does not require `appCell`, `appHeader`, `appFooter`, or registered components. + +```html + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+ + {{ renderCell }} + +
+``` + +## Override Shared Defaults Per Table + +Options passed to `injectAppTable` override defaults from `createTableHook`. Use this for the few tables that need different behavior without creating a separate app hook. + +```ts +readonly table = injectAppTable(() => ({ + key: 'sortable-users-table', + columns, + data: this.data(), + enableSortingRemoval: true, +})) +``` + +## Optional: Reusable Components + +The richer composable-tables example also uses `createTableHook` as a component registry. Use this when several tables should share the same toolbar controls, cell renderers, header renderers, or footer renderers. + +### Component Registry Setup + +The composable tables example keeps the shared setup in `src/app/table.ts`. That file creates one app-specific table factory and exports the helpers used by the rest of the example. + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTableHook, + filterFns, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' + +import { + PaginationControls, + RowCount, + TableToolbar, +} from './components/table-components' +import { + CategoryCell, + NumberCell, + PriceCell, + ProgressCell, + RowActionsCell, + StatusCell, + TextCell, +} from './components/cell-components' +import { + ColumnFilter, + FooterColumnId, + FooterSum, + SortIndicator, +} from './components/header-components' + +const features = tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortFns, + filterFns, +}) + +export const { + createAppColumnHelper, + injectAppTable, + injectTableContext, + injectTableCellContext, + injectTableHeaderContext, +} = createTableHook({ + features, + getRowId: (row) => row.id, + tableComponents: { + PaginationControls, + RowCount, + TableToolbar, + }, + cellComponents: { + TextCell, + NumberCell, + ProgressCell, + StatusCell, + CategoryCell, + PriceCell, + RowActionsCell, + }, + headerComponents: { + SortIndicator, + ColumnFilter, + FooterColumnId, + FooterSum, + }, +}) +``` + +This file is the source of truth for the feature set, row model pipeline, row IDs, and registered components used by both tables in the example. + +### Returned Helpers + +| Helper | Purpose | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `injectAppTable` | Creates a table with the app's shared `features` (including row model factories), defaults, and registered components already attached. | +| `createAppColumnHelper` | Creates column helpers where `cell`, `header`, and `footer` contexts know about the registered components. | +| `injectTableContext` | Reads the current table inside registered table components like `PaginationControls`. | +| `injectTableCellContext` | Reads the current cell inside registered cell components like `TextCell`. | +| `injectTableHeaderContext` | Reads the current header/footer inside registered header components like `SortIndicator`. | + +### Component Columns + +Use `createAppColumnHelper()` instead of the base column helper when column definitions should render registered components. + +```ts +import { flexRenderComponent } from '@tanstack/angular-table' +import { createAppColumnHelper } from '../../table' +import type { Person } from '../../makeData' + +const personColumnHelper = createAppColumnHelper() + +readonly columns = personColumnHelper.columns([ + personColumnHelper.accessor('firstName', { + header: 'First Name', + footer: ({ header }) => flexRenderComponent(header.FooterColumnId), + cell: ({ cell }) => flexRenderComponent(cell.TextCell), + }), + personColumnHelper.accessor('age', { + header: 'Age', + footer: ({ header }) => flexRenderComponent(header.FooterSum), + cell: ({ cell }) => flexRenderComponent(cell.NumberCell), + }), +]) +``` + +The registered components are available through the enhanced `cell` and `header` objects because the column helper is bound to the `createTableHook` configuration. + +### Component Table Rendering + +Create each table with `injectAppTable`. Per-table options provide the data and columns; shared features and row models come from `src/app/table.ts`. + +```ts +table = injectAppTable(() => ({ + key: 'users-table', + columns: this.columns, + data: this.data(), + debugTable: true, +})) +``` + +The Angular table instance is augmented with: + +- `table.PaginationControls`, `table.RowCount`, and `table.TableToolbar` +- `table.appCell(cell)` for enhanced cell component types in templates +- `table.appHeader(header)` for enhanced header component types in templates +- `table.appFooter(footer)` for enhanced footer component types in templates + +Registered table components can access the table through Angular DI: + +```ts +export class PaginationControls { + readonly table = injectTableContext() +} +``` + +In templates, use the Angular rendering helpers with the app wrappers: + +```html +@for (_header of headerGroup.headers; track _header.id) { @let header = +table.appHeader(_header); + + + + {{ value }} + + + {{ value }} + + +} +``` + +### Reusing The Component Registry + +The example has separate Users and Products table components. Both import `createAppColumnHelper` and `injectAppTable` from `src/app/table.ts`, so they share sorting, filtering, pagination, row IDs, toolbar controls, cell renderers, and header/footer renderers while keeping their own data and columns. + +If different product areas need incompatible defaults, create another `createTableHook` setup file and export a second set of app helpers from there. + +## When To Use This Pattern + +Use `createTableHook` when multiple tables should share features, row models, default options, or conventions. Use the standalone `injectTable` API for a one-off table. Add the component registry only when the app wants standardized reusable table UI pieces. diff --git a/docs/framework/angular/guide/custom-features.md b/docs/framework/angular/guide/custom-features.md new file mode 100644 index 0000000000..e9b0b77432 --- /dev/null +++ b/docs/framework/angular/guide/custom-features.md @@ -0,0 +1,378 @@ +--- +title: Custom Features (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Custom Plugin](../examples/custom-plugin) + +## Custom Features (Angular) Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers when TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable`, `injectTable`, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrappers around their adapter's table creation function (the equivalent of `injectTable` in Angular) to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking: you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + initTableInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initCellInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + cell: Cell, + ) => void + initColumnInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + column: Column, + ) => void + initHeaderGroupInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + headerGroup: HeaderGroup, + ) => void + initHeaderInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + header: Header, + ) => void + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void + resetTableInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods does: + +#### Default Options and Initial State + +
+ +##### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Resizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-resizing/columnResizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +##### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-pagination/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### initTableInstanceData and resetTableInstanceData + +Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier. + +Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`. + +Keep API assignment in `constructTableAPIs`; initialization and reset hooks are for data owned by the feature. + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is exclusively responsible for adding methods to the `table` instance. It runs after all feature-owned table instance data has been initialized. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype and initHeaderInstanceData + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. The `initHeaderInstanceData` method is available for per-header instance data or caches that cannot live on the shared prototype. It runs during header construction, before sub-headers are populated and before the header is linked to its header group. Headers are reconstructed whenever header groups recompute, so it reruns on every rebuild. + +
+ +##### initHeaderGroupInstanceData + +The `initHeaderGroupInstanceData` method is available for per-header-group instance data. Header groups have no shared prototype, so this is their only per-instance extension point. It runs after a header group's `depth`, `id`, and fully populated `headers` array have been assigned, and reruns whenever header groups are rebuilt. + +
+ +##### assignColumnPrototype and initColumnInstanceData + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. The `initColumnInstanceData` method is available for per-column instance data or caches that cannot live on the shared prototype. For example, the [Aggregation](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.ts) feature uses it to set up a per-column aggregation cache. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype and initCellInstanceData + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, Column Grouping adds `getIsGrouped` and `getIsPlaceholder`, while Aggregation adds `getIsAggregated`. The `initCellInstanceData` method is available for per-cell instance data or caches that cannot live on the shared prototype. Cells are constructed lazily on first access per row/column pair and cached, so it runs once per cell instance. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full [custom-plugin](../examples/custom-plugin) example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/angular-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // initCellInstanceData: (cell) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // initColumnInstanceData: (column) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, + // initHeaderInstanceData: (header) => {}, + // if you need to add header group instance data... + // initHeaderGroupInstanceData: (headerGroup) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +readonly table = injectTable(() => ({ + features, + columns, + data, + //.. +})) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs, options, and state in your application. The [custom-plugin example](../examples/custom-plugin) controls the `density` state with an Angular signal and the new `onDensityChange` option: + +```ts +const features = tableFeatures({ densityPlugin }) + +export class App { + readonly density = signal('md') + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + //... + state: { + density: this.density(), // passing the density state to the table, TS is still happy :) + }, + onDensityChange: (updater) => + typeof updater === 'function' + ? this.density.update(updater) + : this.density.set(updater), + })) +} +``` + +```html + + + + {{ renderCell }} + +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `signal`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/angular/guide/expanding.md b/docs/framework/angular/guide/expanding.md new file mode 100644 index 0000000000..36d24b1705 --- /dev/null +++ b/docs/framework/angular/guide/expanding.md @@ -0,0 +1,339 @@ +--- +title: Expanding (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Expanding](../examples/expanding) + +### Expanding Setup + +Here's how you set up your table to use expanding features. Adding the expanding feature enables the related APIs. If you use client-side expanding, also set up `expandedRowModel` after its feature, since row model slots are type-checked. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), // if using client-side expanding + // manualExpanding: true, // if using manual server-side expanding +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Expanding Feature (Angular) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` and the `expandedRowModel` factory to your features: + +```ts +import { + injectTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), +}) + +readonly table = injectTable(() => ({ + features, + // other options... +})) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are child rows that inherit the same column structure as their parent rows. If your data object already includes expanded row data, use the `getSubRows` function to specify these child rows. If your data object does not contain expanded row data, it can be treated as custom expanded data, which is discussed in the next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { + id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 }, + ], + }, + { + id: 3, + name: 'Doe', + age: 40, + children: [{ id: 4, name: 'Alice', age: 10 }], + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +readonly table = injectTable(() => ({ + features, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +})) +``` + +> [!NOTE] +> You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +```html + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + {{ renderCell }} + + } + + @if (row.getIsExpanded()) { + + + + + + } } + +``` + +### Expanded rows state + +If you need access to the expanded state of the rows in other parts of your application, you can own the `expanded` state slice yourself. The recommended way in v9 is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the expanded value can be read anywhere in your app without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' + +export class App { + readonly expandedAtom = createAtom({}) + + readonly table = injectTable(() => ({ + features, + // other options... + atoms: { + expanded: this.expandedAtom, // expanding APIs now update expandedAtom + }, + })) + + // read this.expandedAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.expanded` plus `onExpandedChange` pattern is still supported. In Angular this means owning the slice with an Angular signal, as shown in the [Basic External State example](../examples/basic-external-state). It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly expanded = signal({}) + +readonly table = injectTable(() => ({ + features, + // other options... + state: { + expanded: this.expanded(), + }, + onExpandedChange: (updater) => + typeof updater === 'function' + ? this.expanded.update(updater) + : this.expanded.set(updater), +})) +``` + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows whose IDs are present as keys in the record and have a value of true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. For example, you can add a button UI within the column definition. + +```ts +const columns = [ + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'age', + header: 'Age', + }, + { + header: 'Children', + cell: ({ row }) => row.getCanExpand(), + }, +] +``` + +```html +@if (row.getCanExpand()) { + +} +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```ts +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```ts +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, filtering starts from the parent rows and moves downwards. If a parent row is excluded by the filter, all of its child rows are excluded too. You can change this with the `filterFromLeafRows` option. When it is enabled, filtering starts from the leaf (child) rows and moves upwards, so a parent row is included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. The `maxLeafRowFilterDepth` option sets the maximum depth of child rows that the filter considers. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns, +}) + +//... +readonly table = injectTable(() => ({ + features, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +})) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parent's page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +readonly table = injectTable(() => ({ + features, + // other options... + paginateExpandedRows: false, +})) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Row Pinning Guide](./row-pinning) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Auto Reset Expanded State + +If you are also using the grouping feature, the `expanded` state is automatically reset whenever the grouped row model recomputes, such as when the `data` or the grouping state changes. This default is automatically disabled when `manualExpanding` is `true`, but it can be overridden by explicitly assigning a boolean value to the `autoResetExpanded` table option. There is also a global `autoResetAll` table option that disables (or enables) every auto-reset behavior at once. + +A common reason to set `autoResetExpanded: false` is editing data while viewing the table (for example, inline cell editing). Every edit updates `data`, which recomputes the row models and would otherwise collapse the user's expanded rows. If you also use the pagination feature, pair it with `autoResetPageIndex: false` so the current page is kept as well. + +```ts +const features = tableFeatures({ + rowExpandingFeature, + columnGroupingFeature, + expandedRowModel: createExpandedRowModel(), + // the auto-reset only fires when the grouped row model recomputes + groupedRowModel: createGroupedRowModel(), + aggregationFns, +}) + +export class App { + readonly table = injectTable(() => ({ + features, + // other options... + autoResetExpanded: false, // keep expanded state when data changes + // autoResetAll: false, // or turn off all auto resets at once + })) +} +``` + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const features = tableFeatures({ rowExpandingFeature }) + +readonly table = injectTable(() => ({ + features, + // other options... + manualExpanding: true, +})) +``` diff --git a/docs/framework/angular/guide/flex-render.md b/docs/framework/angular/guide/flex-render.md new file mode 100644 index 0000000000..850d75f8fe --- /dev/null +++ b/docs/framework/angular/guide/flex-render.md @@ -0,0 +1,423 @@ +--- +title: FlexRender (Angular) Guide +--- + +The `@tanstack/angular-table` adapter provides structural directives and dependency injection primitives for rendering table content in Angular templates. + +## `FlexRender` vs `flexRender` + +Angular uses the names for two related template concepts: + +- `FlexRender` is the exported tuple of rendering directives. Add it to a component's `imports` array. +- `*flexRender` is the lower-level structural directive. +- `*flexRenderCell`, `*flexRenderHeader`, and `*flexRenderFooter` are table-aware shorthand selectors included in `FlexRender`. + +`FlexRender` is the rendering primitive. +It is exported as a tuple of two directives: + +- `FlexRenderDirective`: the base structural directive (`*flexRender`) +- `FlexRenderCell`: shorthand directives (`*flexRenderCell`, `*flexRenderHeader`, `*flexRenderFooter`) + +Import `FlexRender` to get both: + +```ts +import { Component } from '@angular/core' +import { FlexRender } from '@tanstack/angular-table' + +@Component({ + imports: [FlexRender], + templateUrl: './app.html', +}) +export class AppComponent {} +``` + +### How it works + +`FlexRender` is an Angular **structural directive**. Internally, it resolves the column definition's `header`, `cell`, or `footer` function and renders the result using [`ViewContainerRef`](https://angular.dev/api/core/ViewContainerRef): + +- **Primitives** (`string`, `number`): rendered via `createEmbeddedView` into the host `ng-template`. The value is exposed as the template's implicit context (`let value`). +- **`TemplateRef`**: rendered via `createEmbeddedView`. The render context (`CellContext`, `HeaderContext`) is passed as `$implicit`. +- **`flexRenderComponent(...)`**: rendered via `createComponent` with explicit `inputs`, `outputs`, `bindings`, `directives`, and `injector`. +- **Component type** (`Type`): rendered via [`createComponent`](https://angular.dev/api/core/ViewContainerRef#createComponent). All properties from the render context are set as component inputs through [`ComponentRef.setInput`](https://angular.dev/api/core/ComponentRef#setInput). + +Column definition functions (`header`, `cell`, `footer`) are called inside [`runInInjectionContext`](https://angular.dev/api/core/runInInjectionContext), which means you can call `inject()`, use signals, and access DI tokens directly in your render logic. + +## Cell rendering + +Prefer the shorthand directives for standard rendering: + +| Directive | Input | Column definition | +| ------------------- | -------- | ------------------ | +| `*flexRenderCell` | `Cell` | `columnDef.cell` | +| `*flexRenderHeader` | `Header` | `columnDef.header` | +| `*flexRenderFooter` | `Header` | `columnDef.footer` | + +Each shorthand resolves the correct column definition function and render context automatically through a `computed` signal, so no manual `props` mapping is needed. + +### Example + +```html + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + @if (!header.isPlaceholder) { + + {{ value }} + + } + + } + + } + + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + + {{ value }} + + + } + + } + +``` + +## Cell rendering with custom props + +When you need full control over the `props` passed to the render function, use `*flexRender` directly. + +`FlexRenderDirective` accepts two inputs: + +- `flexRender`: the render definition (a column def function, a string, a `TemplateRef`, a component type, or a `flexRenderComponent(...)` wrapper) +- `flexRenderProps`: the props object passed to the render function and used as the implicit template context + +Standard usage: + +```html + + {{ rendered }} + +``` + +You can pass a custom props object to override the default context shape: + +```html + + {{ rendered }} + +``` + +Inside rendered components, the full props object is available via [`injectFlexRenderContext()`](#injectflexrendercontext). + +## Component rendering + +You can render Angular components from column definitions in two ways: + +### Using `flexRenderComponent` + +`flexRenderComponent(component, options?)` wraps a component type with explicit options for `inputs`, `outputs`, `injector`, `bindings`, and `directives`. + +Use this when you need to: + +- pass custom inputs not derived from the render context +- subscribe to component outputs +- provide a custom `Injector` +- use creation-time `bindings` (Angular v20+) +- apply host directives and binding values at runtime + +```ts +import { flexRenderComponent, type ColumnDef } from '@tanstack/angular-table' + +const columns: ColumnDef[] = [ + { + id: 'custom-cell', + cell: (ctx) => + flexRenderComponent(CustomCellComponent, { + inputs: { + content: ctx.row.original.firstName, + }, + outputs: { + clicked: (value) => { + console.log(value) + }, + }, + }), + }, +] +``` + +#### How inputs and outputs work + +**Inputs** are applied through [`ComponentRef.setInput(key, value)`](https://angular.dev/api/core/ComponentRef#setInput). This works with both `input()` signals and `@Input()` decorators. Inputs are diffed on every change detection cycle using `KeyValueDiffers`; only changed values trigger `setInput`. + +For object-like inputs, updates are reference-based: if the object reference is stable, Angular's default input equality semantics prevent unnecessary updates. + +**Outputs** work through `OutputEmitterRef` subscriptions. The factory reads the component instance property by name, checks that it is an `OutputEmitterRef`, and subscribes to it. When the output emits, the corresponding callback from `outputs` is invoked. Subscriptions are cleaned up automatically when the component is destroyed. + +#### `bindings` API (Angular v20+) + +`flexRenderComponent` also accepts `bindings` and `directives`, forwarded directly to [`ViewContainerRef.createComponent`](https://angular.dev/api/core/ViewContainerRef#createComponent) at creation time. + +This supports Angular programmatic rendering APIs for passing host directives and binding values at runtime. + +Unlike `inputs`/`outputs` (which are applied imperatively after creation), `bindings` are applied **at creation time**, so they participate in the component's initial change detection cycle. + +```ts +import { + inputBinding, + outputBinding, + twoWayBinding, + signal, +} from '@angular/core' +import { flexRenderComponent } from '@tanstack/angular-table' + +readonly name = signal('Ada') + +cell: () => + flexRenderComponent(EditableNameCellComponent, { + bindings: [ + inputBinding('value', this.name), + outputBinding('valueChange', value => { + console.log('changed', value) + }), + twoWayBinding('value', this.name), + ], + }) +``` + +> Avoid mixing `bindings` with `inputs`/`outputs` on the same property. `bindings` are applied at creation, while `inputs`/`outputs` are applied post-creation, so mixing them can lead to double initialization or conflicting values. + +See the Angular docs for details: + +- [Programmatic rendering: Binding inputs/outputs/directives](https://angular.dev/guide/components/programmatic-rendering#binding-inputs-outputs-and-setting-host-directives-at-creation) +- [`inputBinding`](https://angular.dev/api/core/inputBinding), [`outputBinding`](https://angular.dev/api/core/outputBinding), [`twoWayBinding`](https://angular.dev/api/core/twoWayBinding) + +### Returning a component class + +Return a component class from `header`, `cell`, or `footer`. + +The render context properties (`table`, `column`, `header`, `cell`, `row`, `getValue`, etc.) are automatically set as component inputs via `ComponentRef.setInput(...)`. + +Define input signals matching the context property names you need: + +```ts +import { Component, input } from '@angular/core' +import type { ColumnDef, Table, CellContext } from '@tanstack/angular-table' + +const columns: ColumnDef[] = [ + { + id: 'select', + header: () => TableHeadSelectionComponent, + cell: () => TableRowSelectionComponent, + }, +] + +@Component({ + template: ` + + `, +}) +export class TableHeadSelectionComponent { + readonly table = input.required>() + // column = input.required>() + // header = input.required>() +} +``` + +Only properties declared with `input()` / `input.required()` are set; other context properties are silently ignored. You can also access the full context via [`injectFlexRenderContext()`](#injectflexrendercontext). + +## TemplateRef rendering + +You can return a `TemplateRef` from column definitions. The render context is passed as the template's `$implicit` context. + +Use `viewChild(...)` to capture template references: + +```ts +import { Component, TemplateRef, viewChild } from '@angular/core' +import type { + CellContext, + ColumnDef, + HeaderContext, +} from '@tanstack/angular-table' + +@Component({ + template: ` + + {{ context.column.id }} + + + + {{ context.getValue() }} + + `, +}) +export class AppComponent { + readonly customHeader = + viewChild.required< + TemplateRef<{ $implicit: HeaderContext }> + >('customHeader') + readonly customCell = + viewChild.required }>>( + 'customCell', + ) + + readonly columns: ColumnDef[] = [ + { + id: 'templated', + header: () => this.customHeader(), + cell: () => this.customCell(), + }, + ] +} +``` + +`TemplateRef` rendering uses `createEmbeddedView` with an injector that includes the [DI context tokens](#dependency-injection). For reusable render blocks shared across multiple screens, prefer standalone components over `TemplateRef`. + +## Dependency injection + +`FlexRender` automatically provides DI tokens when rendering components and templates. These tokens are created in the `#getInjector` method of the renderer, which builds a child `Injector` with the render context properties. + +### `injectFlexRenderContext` + +`injectFlexRenderContext()` returns the full props object passed to `*flexRender`. The return type depends on the column definition slot: + +- In a `cell` definition: `CellContext` +- In a `header`/`footer` definition: `HeaderContext` + +```ts +import { Component } from '@angular/core' +import { + injectFlexRenderContext, + type CellContext, +} from '@tanstack/angular-table' + +@Component({ + template: ` + {{ context.getValue() }} + + `, +}) +export class InteractiveCellComponent { + readonly context = injectFlexRenderContext>() +} +``` + +Internally, the renderer wraps the context in a `Proxy` so that property access always reflects the latest values, even after re-renders. + +### Context directives + +Three optional directives let you expose table, header, and cell context to **any descendant** in the template, not just components rendered by `*flexRender`. + +This eliminates prop drilling: instead of passing data through multiple `input()` layers, any nested component or directive can inject the context directly. + +| Directive | Selector | Token | Inject helper | +| --------------------- | ----------------------- | -------------------------- | ---------------------------- | +| `TanStackTable` | `[tanStackTable]` | `TanStackTableToken` | `injectTableContext()` | +| `TanStackTableHeader` | `[tanStackTableHeader]` | `TanStackTableHeaderToken` | `injectTableHeaderContext()` | +| `TanStackTableCell` | `[tanStackTableCell]` | `TanStackTableCellToken` | `injectTableCellContext()` | + +Import them alongside `FlexRender`: + +```ts +import { + FlexRender, + TanStackTable, + TanStackTableHeader, + TanStackTableCell, +} from '@tanstack/angular-table' + +@Component({ + imports: [FlexRender, TanStackTable, TanStackTableHeader, TanStackTableCell], + templateUrl: './app.html', +}) +export class AppComponent {} +``` + +Apply them in the template to establish injection scopes: + +```html + + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + } + + } +
+ +
+ +
+``` + +Any component nested inside a `[tanStackTableCell]` host can inject the cell context: + +```ts +import { Component } from '@angular/core' +import { injectTableCellContext } from '@tanstack/angular-table' + +@Component({ + template: ` + + `, +}) +export class CellActionsComponent { + readonly cell = injectTableCellContext() + + onAction() { + console.log('Cell:', this.cell()) + } +} +``` + +```html + + + + +``` + +Each directive uses Angular's `providers` array to register a factory that reads its own input signal. + +This means the token is scoped to the directive's host element and its descendants. Multiple `[tanStackTableCell]` directives on different elements provide independent contexts. + +### Automatic token injection in FlexRender + +When `FlexRender` renders a component or template, it also provides DI tokens automatically based on the render context shape. In the renderer's `#getInjector` method, if the context object contains `table`, `cell`, or `header` properties, the corresponding `TanStackTableToken`, `TanStackTableCellToken`, or `TanStackTableHeaderToken` tokens are provided in the child injector. + +This means that even **without** the context directives, components rendered via `*flexRender` can use `injectTableContext()`, `injectTableCellContext()`, and `injectTableHeaderContext()`. The context directives are only needed for components that live **outside** the `*flexRender` rendering tree (e.g. sibling components in the same ``). diff --git a/docs/framework/angular/guide/fuzzy-filtering.md b/docs/framework/angular/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..8a31ad3cb0 --- /dev/null +++ b/docs/framework/angular/guide/fuzzy-filtering.md @@ -0,0 +1,222 @@ +--- +title: Fuzzy Filtering (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Fuzzy Search](../examples/filters-fuzzy) + +### Fuzzy Filtering Setup + +Here's how you set up your table to use fuzzy filtering features. Adding the fuzzy filtering feature enables the related APIs. If you use client-side fuzzy filtering and sorting, also set up `filteredRowModel` and `sortedRowModel` after their features, since row model slots are type-checked. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + metaHelper, +} from '@tanstack/angular-table' +import type { RankingInfo } from '@tanstack/match-sorter-utils' + +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + sortedRowModel: createSortedRowModel(), // if using client-side sorting + // manualSorting: true, // if using manual server-side sorting + filterFns: { fuzzy: fuzzyFilter }, // fuzzyFilter defined below + sortFns: { fuzzy: fuzzySort }, // fuzzySort defined below + filterMeta: metaHelper(), +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +> [!NOTE] +> The `filterFns` and `sortFns` registries above list only the custom `fuzzy` functions this guide uses. Spreading the entire built-in registries (`filterFns: { ...filterFns, fuzzy: fuzzyFilter }`) still works, but it puts every built-in function in your bundle. Register just the functions you use, or pass functions directly to the `filterFn` and `sortFn` column options with no registration. + +## Fuzzy Filtering (Angular) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement client-side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> [!NOTE] +> You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked to work better with TanStack Table's row by row filtering approach. + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +First, define the filter meta shape and the features type that includes it: + +```typescript +import { rankItem } from '@tanstack/match-sorter-utils' +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import type { FilterFn, TableFeatures, RowData } from '@tanstack/angular-table' + +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} +type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } +``` + +Then define the fuzzy filter function using those types: + +```typescript +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta?.({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the `rankItem` function from the `@tanstack/match-sorter-utils` library to rank the item. We then store the ranking information in the filter meta of the row (the `addMeta` callback is optional, so call it with optional chaining), and return whether the item passed the ranking criteria. + +Register the fuzzy filter and the filter meta slot in `tableFeatures` instead of using `declare module` augmentation: + +```typescript +import { metaHelper } from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { fuzzy: fuzzyFilter }, + filterMeta: metaHelper(), +}) +``` + +The `filterMeta` slot types the per-row filter metadata for this table. The `fuzzy` key in `filterFns` lets you reference the function by the string `'fuzzy'` in column `filterFn` options and `globalFilterFn`. + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, register the fuzzy filter function in the `filterFns` slot of `tableFeatures` and reference it in the `globalFilterFn` option of the table: + +```typescript +import { + injectTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + metaHelper, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), // needed if you want sorting with fuzzy rank + filterFns: { fuzzy: fuzzyFilter }, + sortFns: { fuzzy: fuzzySort }, + filterMeta: metaHelper(), +}) + +readonly table = injectTable(() => ({ + features, + columns, + data, + globalFilterFn: 'fuzzy', +})) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, register your fuzzy filter function in the `filterFns` slot of `tableFeatures` (as shown in the setup snippet above). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```typescript +const column = [ + { + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +] +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```typescript +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFn_alphanumeric } from '@tanstack/angular-table' +import type { SortFn } from '@tanstack/angular-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId].itemRank!, + rowB.columnFiltersMeta[columnId].itemRank!, + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFn_alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then pass this sorting function directly to the `sortFn` option of the column definition: + +```typescript +{ + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', // using our custom fuzzy filter function (registered above) + sortFn: fuzzySort, // pass our custom fuzzy sort function directly +} +``` + +> [!NOTE] +> `fuzzySort` can also be referenced by the string `'fuzzy'` if it is registered in the `sortFns` slot of `tableFeatures` (as shown in the setup snippet above). Passing the function directly to `sortFn` skips the need to register it. diff --git a/docs/framework/angular/guide/global-filtering.md b/docs/framework/angular/guide/global-filtering.md new file mode 100644 index 0000000000..3f5cbce0f9 --- /dev/null +++ b/docs/framework/angular/guide/global-filtering.md @@ -0,0 +1,276 @@ +--- +title: Global Filtering (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Filters](../examples/filters) +- [Fuzzy Search](../examples/filters-fuzzy) + +### Global Filtering Setup + +Here's how you set up your table to use global filtering features. Global filtering depends on column filtering, so add `columnFilteringFeature` before `globalFilteringFeature`. Adding the global filtering feature enables the related APIs. If you use client-side filtering, also set up `filteredRowModel` after its feature, since row model slots are type-checked. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, + createFilteredRowModel, + filterFn_includesString, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + filterFns: { includesString: filterFn_includesString }, +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +> [!NOTE] +> The `filterFns` registry above lists only the built-in filter function this table uses. Spreading the entire built-in `filterFns` registry (`filterFns: { ...filterFns }`) still works, but it puts every built-in filter function in your bundle. Register just the functions you use, or pass a function directly to the `globalFilterFn` option with no registration at all. + +## Global Filtering (Angular) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. + +The client-side filtered row model also invokes the page-index auto-reset hook when global filtering inputs change. Whether the page index resets depends on the `autoResetPageIndex`, `autoResetAll`, and `manualPagination` options. If filtering is manual and this row model is omitted or bypassed, a global filter state change does not invoke that hook, so reset server-side pagination in the filter change handler when needed. + +### Manual Server-Side Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to features, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +import { + injectTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature }) + +readonly table = injectTable(() => ({ + features, + data, + columns, + manualFiltering: true, +})) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` (along with its required `columnFilteringFeature` prerequisite) and the `filteredRowModel` factory to your features: + +```ts +import { + injectTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, + createFilteredRowModel, + filterFn_includesString, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { includesString: filterFn_includesString }, +}) + +readonly table = injectTable(() => ({ + features, + // other options... +})) +``` + +### Global Filter Function + +The `globalFilterFn` option sets the filter function used for global filtering. The filter function can be a string that references a filter function (built-in or custom) registered in the `filterFns` slot of `tableFeatures`, or a filter function passed directly. + +```ts +readonly table = injectTable(() => ({ + features, + // filteredRowModel and filterFns are registered in features + data, + columns, + globalFilterFn: 'includesString', // built-in filter function +})) +``` + +By default there are 12 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom global filter function and pass it directly to the `globalFilterFn` table option, as shown [below](#custom-global-filter-function). + +### Global Filter State + +The `globalFilter` state slice holds the current global filter value, usually a search string (the slice is typed as `any` so custom global filter functions can accept other value shapes). Read it with `table.atoms.globalFilter.get()`. In Angular, table atom reads are signal reads, so reading the atom in a template expression, `computed(...)`, or `effect(...)` automatically tracks updates. + +If you need access to the global filter state outside of the table, you can own the slice yourself. The recommended way in v9 is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the filter value can be used elsewhere (such as in a query key for server-side filtering) without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' + +export class App { + readonly globalFilterAtom = createAtom('') + + readonly table = injectTable(() => ({ + features, + // filteredRowModel and filterFns are registered in features + // other options... + atoms: { + globalFilter: this.globalFilterAtom, // table.setGlobalFilter now updates globalFilterAtom + }, + })) + + // read the atom wherever you need the value (e.g. for a query key) + // this.globalFilterAtom.get() +} +``` + +Alternatively, the v8-style `state.globalFilter` plus `onGlobalFilterChange` pattern is still supported. In Angular this means owning the slice with an Angular signal, as shown in the [Basic External State example](../examples/basic-external-state). It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly globalFilter = signal('') + +readonly table = injectTable(() => ({ + features, + // filteredRowModel and filterFns are registered in features + // other options... + state: { + globalFilter: this.globalFilter(), + }, + onGlobalFilterChange: (updater) => + typeof updater === 'function' + ? this.globalFilter.update(updater) + : this.globalFilter.set(updater), +})) +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. Read the value reactively with `table.atoms.globalFilter.get()` and update it with `table.setGlobalFilter`. + +```html + +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the `globalFilterFn` option. + +> [!NOTE] +> It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering). + +```ts +const customFilterFn = (row, columnId, filterValue) => { + return // true if the row should be included in the filtered rows +} + +readonly table = injectTable(() => ({ + features, + // filteredRowModel and filterFns are registered in features + // other options... + globalFilterFn: customFilterFn, +})) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table `initialState` option. However, if you are controlling the slice yourself, set the starting value on your external atom or Angular signal instead. + +```ts +readonly table = injectTable(() => ({ + features, + // filteredRowModel and filterFns are registered in features + // other options... + initialState: { + globalFilter: 'search term', // if not controlling globalFilter state, set initial state here + }, +})) +``` + +> [!NOTE] +> Do not use both `initialState.globalFilter` and a controlled `globalFilter` (via `atoms` or `state`) at the same time, as the controlled value will override `initialState.globalFilter`. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + // filteredRowModel and filterFns are registered in features + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +})) +``` + +### Global Filter APIs + +There are several APIs that are useful for hooking up your global filter UI: + +- `table.setGlobalFilter` - Set the global filter value. Useful for connecting a search input's `input` event handler. +- `table.resetGlobalFilter` - Reset the global filter value to its initial state, or clear it with `table.resetGlobalFilter(true)`. +- `table.getGlobalFilterFn` - Returns the filter function currently used for global filtering. +- `table.getGlobalAutoFilterFn` - Returns the default global filter function (currently `includesString`). +- `column.getCanGlobalFilter` - Returns whether a column participates in global filtering. Useful for debugging which columns are searched. diff --git a/docs/framework/angular/guide/grouping.md b/docs/framework/angular/guide/grouping.md new file mode 100644 index 0000000000..ef3938ef31 --- /dev/null +++ b/docs/framework/angular/guide/grouping.md @@ -0,0 +1,219 @@ +--- +title: Grouping (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Grouping](../examples/grouping) + +> [!NOTE] +> `columnGroupingFeature` and `rowAggregationFeature` are now separate features. Register either one independently, or register both when grouped rows should also calculate aggregate values. See the [Aggregation Guide](./aggregation) for aggregation setup. + +### Grouping Setup + +Here's how you set up your table to use grouping features. Adding the grouping feature enables the related APIs. If you use client-side grouping, also set up `groupedRowModel` after its feature, since row model slots are type-checked. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), // if using client-side grouping + // manualGrouping: true, // if using manual server-side grouping +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Grouping (Angular) Guide + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +Grouping can also affect column order. There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +### Client-Side vs Server-Side Grouping + +Grouping should operate over the complete dataset when its groups are meant to describe all rows. Use client-side grouping when the browser has the complete dataset. Use manual server-side grouping when the server returns only a page or another subset, or when the server needs to perform grouping and aggregation. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. + +The client-side grouped row model invokes the page-index and expanded-state auto-reset hooks when its inputs change. Whether those states reset depends on the `autoResetPageIndex`, `autoResetExpanded`, `autoResetAll`, `manualPagination`, and `manualExpanding` options. If grouping is manual and this row model is omitted or bypassed, a grouping state change does not invoke those hooks, so reset dependent server-side state in the grouping change handler when needed. + +### Client-Side Grouping + +To use the grouping feature, add the `columnGroupingFeature` and the `groupedRowModel` factory to your features. The grouped row model is responsible for grouping the rows based on the grouping state. + +```ts +import { + injectTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), +}) + +readonly table = injectTable(() => ({ + features, + // other options... +})) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```ts +const features = tableFeatures({ + columnGroupingFeature, + rowExpandingFeature, + groupedRowModel: createGroupedRowModel(), + expandedRowModel: createExpandedRowModel(), +}) + +readonly table = injectTable(() => ({ + features, + // other options... +})) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```ts +table.setGrouping(['column1', 'column2']) +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```ts +table.resetGrouping() +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```ts +readonly table = injectTable(() => ({ + features, + // other options... + groupedColumnMode: 'reorder', +})) +``` + +### Manual Grouping + +If you are doing server-side grouping, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to group the rows before passing them to the table. + +```ts +const features = tableFeatures({ columnGroupingFeature }) + +readonly table = injectTable(() => ({ + features, + // other options... + manualGrouping: true, +})) +``` + +> [!NOTE] +> There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Controlled Grouping State + +If you need access to the grouping state in other parts of your application, you can own the `grouping` state slice yourself. The recommended way in v9 is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the grouping value can be read anywhere in your app (such as in a query key for server-side grouping) without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import type { GroupingState } from '@tanstack/angular-table' + +export class App { + readonly groupingAtom = createAtom([]) + + readonly table = injectTable(() => ({ + features, + // other options... + atoms: { + grouping: this.groupingAtom, // grouping APIs now update groupingAtom + }, + })) + + // read this.groupingAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.grouping` plus `onGroupingChange` pattern is still supported. In Angular this means owning the slice with an Angular signal, as shown in the [Basic External State example](../examples/basic-external-state). It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly grouping = signal([]) + +readonly table = injectTable(() => ({ + features, + // other options... + state: { + grouping: this.grouping(), + }, + onGroupingChange: (updater) => + typeof updater === 'function' + ? this.grouping.update(updater) + : this.grouping.set(updater), +})) +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```ts +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +``` + +Rows expose grouping helpers for grouped row rendering: + +```ts +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose grouping and placeholder helpers: + +```ts +cell.getIsGrouped() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```ts +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/angular/guide/migrating.md b/docs/framework/angular/guide/migrating.md new file mode 100644 index 0000000000..951a18972c --- /dev/null +++ b/docs/framework/angular/guide/migrating.md @@ -0,0 +1,998 @@ +--- +title: Migrating to TanStack Table V9 (Angular) +--- + +## What's New in TanStack Table V9 + +TanStack Table V9 delivers major performance improvements, hundreds of bug fixes, new and refreshed features, and optional helpers for composing and managing tables. Despite the scale of the release, the headless model, core table logic, column definitions, and rendering patterns remain familiar. Here are the key changes: + +### 1. Better Performance + +- **Lower memory usage**: The core architecture now shares more behavior across table objects, with some large-table scenarios seeing up to 90% memory savings. +- **Faster client-side row models**: Sorting, filtering, and aggregation paths have improved algorithms and memoization, with many scenarios seeing up to 40-70% speed improvements. +- **Better column resizing performance**: The same architectural and memoization work also speeds up column resizing. + +### 2. State Management Overhaul + +- **TanStack Store foundation**: The internal state system has been rebuilt on [TanStack Store](https://tanstack.com/store), providing a reactive, framework-agnostic foundation. +- **Angular signal integration**: Table atoms are backed by signals. Use `computed(...)` when you want selector-style derivation or custom equality, and keep reads scoped to the state you actually need. +- **External state remains supported**: You can still use `state` plus `on[State]Change` by owning slices with Angular signals. + +### 3. Type-Safety Improvements + +- **New and revamped type helpers**: There are helpers for defining columns, custom filters, sorts, aggregations, column and table meta, shared table options and components, and more. +- **Per-table meta types**: `tableMeta`, `columnMeta`, and `filterMeta` slots let you type meta for a specific table instead of globally augmenting shared interfaces. **No more global declaration merging required!** +- **Feature-gated APIs**: APIs only exist when their feature is registered, and `tableFeatures()` validates feature prerequisites at the type level. + +### 4. Tree Shaking and Extensibility + +- **Import only the features you use**: Tables that only need sorting do not ship filtering, pagination, or other unused feature code. +- **Tree-shakeable row models and functions**: Row model factories and `filterFns` / `sortFns` / `aggregationFns` now live on `tableFeatures()`, so unused processing code can be dropped. +- **Custom features use the same system**: Your own feature plugins can register state, options, and APIs alongside the built-in features. See the [Custom Features Guide](./custom-features.md). + +### 5. Composability + +- **`tableOptions`**: Compose reusable table configuration, including features, row models, and default options. +- **`createTableHook`**: Create reusable, strongly typed Angular table factories with pre-bound features, row models, default options, and component registries. + +### 6. New and Refreshed Features + +- **New Features** + - **Cell Selection**: `cellSelectionFeature` adds spreadsheet-style rectangular cell range selection, with drag, Shift-extend, and multiple disjoint ranges. See the [Cell Selection Guide](./cell-selection.md). + - **Cell Spanning**: `cellSpanningFeature` merges body cells across rows and columns (`spanRows` / `spanColumns`, with span-aware cell selection), and header groups now compute `header.rowSpan` so shallow columns can span header rows. See the [Cell Spanning Guide](./cell-spanning.md). +- **Refreshed Features** + - **More capable features**: Aggregation, Row Selection, Column Pinning, and Column Resizing have all been made more feature rich (multiple aggregation definitions per column, Shift range selection, logical `start`/`end` pinning, and more). + - **New core APIs**: New table and row APIs (like `table.getMaxSubRowDepth()`, `row.getDisplayIndex()`) round out the core feature set. + +### 7. Modern Builds + +- **ESM-only**: UMD and CJS builds have been dropped. Packages ship as modern ESM. +- **TypeScript target `ES2022`**: Compiled output now targets ES2022. +- **Smaller install size**: Published packages no longer ship `src` or source maps, which reduces install footprint. + +### The Good News: Most Upgrades Are Opt-in + +While v9 is a significant upgrade, **you don't have to adopt everything at once**: + +- **Don't want to think about tree-shaking yet?** You can start with `stockFeatures` to include most commonly used features. +- **Your table markup is largely unchanged.** How you render ``, ``, ``, ` + @for (row of table.getTopRows(); track row.id) { + + + + } @for (row of table.getCenterRows(); track row.id) { + + + + } @for (row of table.getBottomRows(); track row.id) { + + + + } + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```ts +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + enableRowPinning: row => row.original.status !== 'archived', +})) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + keepPinnedRows: false, +})) +``` diff --git a/docs/framework/angular/guide/row-selection.md b/docs/framework/angular/guide/row-selection.md new file mode 100644 index 0000000000..a98829d7db --- /dev/null +++ b/docs/framework/angular/guide/row-selection.md @@ -0,0 +1,278 @@ +--- +title: Row Selection (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Row Selection](../examples/row-selection) +- [Row Selection (Signals)](../examples/row-selection-signal) + +### Row Selection Setup + +Here's how you set up your table to use row selection features. Adding the row selection feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + rowSelectionFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowSelectionFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Row Selection (Angular) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you. You can access the row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the current row selection state +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +In Angular, table atom reads are signal reads, so reading `table.atoms.rowSelection.get()` in a template expression, `computed(...)`, or `effect(...)` automatically tracks updates. + +> [!NOTE] +> If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +If you need easy access to the selected row ids in other parts of your application (for example, to make API calls with them), you can own the row selection state slice yourself. The recommended way in v9 is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the selection value can be read anywhere in your app without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import { + injectTable, + tableFeatures, + rowSelectionFeature, +} from '@tanstack/angular-table' +import type { RowSelectionState } from '@tanstack/angular-table' + +const features = tableFeatures({ rowSelectionFeature }) + +export class App { + readonly rowSelectionAtom = createAtom({}) + + readonly table = injectTable(() => ({ + features, + //... + atoms: { + rowSelection: this.rowSelectionAtom, // selection APIs now update rowSelectionAtom + }, + })) + + // read this.rowSelectionAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.rowSelection` plus `onRowSelectionChange` pattern is still supported. In Angular this means owning the slice with an Angular signal, as shown in the [Row Selection (Signals) example](../examples/row-selection-signal). It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly rowSelection = signal({}) + +readonly table = injectTable(() => ({ + features, + //... + onRowSelectionChange: (updater) => + typeof updater === 'function' + ? this.rowSelection.update(updater) + : this.rowSelection.set(updater), + state: { + rowSelection: this.rowSelection(), + }, +})) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +readonly table = injectTable(() => ({ + features, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +})) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +readonly table = injectTable(() => ({ + //... + enableRowSelection: row => row.original.age > 18, //only enable row selection for adults +})) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +readonly table = injectTable(() => ({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +})) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +readonly table = injectTable(() => ({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +})) +``` + +Sub-row selection also applies to the select-all APIs. When a parent row blocks sub-row selection, `table.toggleAllRowsSelected()` and `table.toggleAllPageRowsSelected()` skip that parent's descendants, and `table.getIsAllRowsSelected()` and `table.getIsAllPageRowsSelected()` ignore those descendants when deciding whether everything is selected. + +Selecting a parent row writes the parent id and its selectable descendant ids into the row selection state. Deselecting a child afterwards does not remove the parent id by default, since some tables treat the state ids as literal selections. Pass the `deselectParents` option to the toggle APIs to remove ancestor ids whenever a row is deselected: + +```ts +row.getToggleSelectedHandler({ deselectParents: true }) +// or +row.toggleSelected(false, { deselectParents: true }) +``` + +### Shift Range Selection + +`row.getToggleSelectedHandler()` supports Shift range selection by default. After an ordinary selectable-row interaction establishes an anchor, Shift-selecting another row selects or deselects the inclusive interval between them. The clicked checkbox's resulting checked value controls the whole range, and the clicked endpoint becomes the anchor for the next Shift interaction. + +The handler recognizes Shift when the event exposes either `event.shiftKey` or `event.nativeEvent.shiftKey`. You can disable range behavior or replace event detection: + +Bind an Angular checkbox handler with `(click)`, not `(change)`, so the handler receives the click event and its `shiftKey` modifier. + +```ts +readonly table = injectTable(() => ({ + // ... + enableRowRangeSelection: false, + + // For example, use the platform modifier instead of Shift: + // isRowRangeSelectionEvent: event => + // Boolean((event as { metaKey?: boolean }).metaKey), +})) +``` + +Range selection follows the table's current logical display order, including filtering, grouping, sorting, and expansion. With client-side pagination, ranges can cross pages because the complete pre-pagination order is used. With manual/server pagination, only rows loaded in the current `data` can participate. + +By default, a parent encountered in a range recursively toggles its selectable descendants when sub-row selection is enabled. Pass `selectChildren: false` when only rows explicitly present in the display-order interval should change: + +```ts +const handler = row.getToggleSelectedHandler({ + selectChildren: false, +}) +``` + +The interaction anchor is preserved across sorting, filtering, grouping, expansion, pagination, and data updates while its row id remains in the display order. If filtering or data replacement removes the anchor, the next Shift interaction falls back to an ordinary row toggle and establishes a new anchor. `resetRowSelection`, either select-all API, and `table.reset()` clear the anchor. Direct calls to `row.toggleSelected()` or `table.setRowSelection()`, and external controlled-state changes, do not establish or move it. + +### Render Row Selection UI + +TanStack table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These functions automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +```html + + + +``` + +> [!NOTE] +> The `getCanSelectSubRows()` and `getIsAllSubRowsSelected()` clauses on the row checkbox only matter for tables with sub-rows. With flat data, `row.getIsSelected()` alone is enough. See the expanding example for the full pattern, including the `deselectParents` option for pruning stale parent ids when children are deselected. + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. + +```html + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + } + + } + +``` diff --git a/docs/framework/angular/guide/sorting.md b/docs/framework/angular/guide/sorting.md new file mode 100644 index 0000000000..d6618a6995 --- /dev/null +++ b/docs/framework/angular/guide/sorting.md @@ -0,0 +1,585 @@ +--- +title: Sorting (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Sorting](../examples/sorting) + +### Sorting Setup + +Here's how you set up your table to use sorting features. Adding the sorting feature enables the related APIs. If you use client-side sorting, also set up `sortedRowModel` after its feature, since row model slots are type-checked. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFn_alphanumeric, + sortFn_text, + sortFn_datetime, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), // if using client-side sorting + // manualSorting: true, // if using manual server-side sorting + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + datetime: sortFn_datetime, + }, +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +> [!NOTE] +> Spreading the entire built-in registry (`sortFns: { ...sortFns }`) still works, but it puts every built-in sorting function in your bundle. Registering just the functions you use, or passing a function directly to the `sortFn` column option, is recommended. The default `sortFn: 'auto'` resolves to `alphanumeric`, `text`, or `datetime` from the registry based on the column's data type, so register the ones your columns rely on. + +## Sorting (Angular) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```ts +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +You can access the sorting state directly from the table instance with `table.atoms.sorting.get()`. In Angular, table atom reads are signal reads, so reading the atom in a template expression, `computed(...)`, or `effect(...)` automatically tracks updates. Use `table.store.get()` only when you need a flat snapshot of the whole state, such as debug JSON. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + //... +})) + +// signal-reactive in templates, computed(...), and effect(...) +this.table.atoms.sorting.get() +``` + +However, if you need access to the sorting state outside of the table, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state in other parts of your application, you can own the sorting state slice yourself. The recommended way in v9 is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the sorting value can be used elsewhere (such as in a query key for server-side sorting) without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' + +export class App { + readonly sortingAtom = createAtom([]) // can set initial sorting state here + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + //... + atoms: { + sorting: this.sortingAtom, // table sorting APIs now update sortingAtom + }, + })) + + // read the atom wherever you need the value (e.g. for a query key) + // this.sortingAtom.get() +} +``` + +Alternatively, the v8-style `state.sorting` plus `onSortingChange` pattern is still supported. In Angular this means owning the slice with an Angular signal, as shown in the [Basic External State example](../examples/basic-external-state). It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly sorting = signal([]) +//... +readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + //... + state: { + sorting: this.sorting(), + }, + onSortingChange: (updater) => + typeof updater === 'function' + ? this.sorting.update(updater) + : this.sorting.set(updater), +})) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + //... + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +})) +``` + +> [!NOTE] +> Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. + +The client-side sorted row model also invokes the page-index auto-reset hook when sorting inputs change. Whether the page index resets depends on the `autoResetPageIndex`, `autoResetAll`, and `manualPagination` options. If sorting is manual and this row model is omitted or bypassed, a sorting state change does not invoke that hook, so reset server-side pagination in the sorting change handler when needed. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorted row model, but you want to disable it, you can use the `manualSorting` table option. + +```ts +import { createAtom } from '@tanstack/angular-store' + +const features = tableFeatures({ rowSortingFeature }) // feature needed for sorting state/APIs + +export class App { + readonly sortingAtom = createAtom([]) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + manualSorting: true, // use pre-sorted row model instead of sorted row model + atoms: { + sorting: this.sortingAtom, + }, + })) + + // read this.sortingAtom.get() in your server-side query logic +} +``` + +Hoisting the sorting state into your own scope (with an external atom or the `state.sorting` plus `onSortingChange` pattern) is covered in the [Controlled Sorting State](#controlled-sorting-state) section above. + +> [!NOTE] +> When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` and the `sortedRowModel` factory to your features. Import `createSortedRowModel` and the individual sorting functions you use from TanStack Table: + +```ts +import { + injectTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFn_alphanumeric, + sortFn_text, + sortFn_datetime, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + datetime: sortFn_datetime, + }, +}) + +readonly table = injectTable(() => ({ + features, + columns, + data, +})) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions, either inline as the `sortFn` column option, or by name in the sorting function registry that you pass to `createSortedRowModel`. + +#### Custom Sorting Functions + +Whether you register a custom sorting function in the registry passed to `createSortedRowModel` or pass it directly as a `sortFn` column option, it should have the following signature: + +```ts +//optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = ( + rowA: Row, + rowB: Row, + columnId: string, +) => { + return //-1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> [!NOTE] +> The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take care of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and is expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```ts +const myCustomSortFn: SortFn = (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0 + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + datetime: sortFn_datetime, + myCustomSortFn, + }, +}) + +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // reference a custom sorting function registered in the features sortFns slot + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + } +] +//... +readonly table = injectTable(() => ({ + features, + columns, + data, +})) +``` + +> **TypeScript Note:** For `sortFn: 'myCustomSortFn'` string references to typecheck, register the function in the `sortFns` slot on `tableFeatures` (as shown above). Alternatively, skip the registry entirely by passing the function directly to the `sortFn` column option. + +#### Customize Sorting Function Behavior + +Sorting functions support an optional "hanging" property: + +- `sortFn.resolveDataValue` - normalizes each row's value before the two sides are compared. It is honored by every sorting function built with the `constructSortFn` helper, which includes all built-in sorting functions. + +The `constructSortFn` helper builds a sorting function from a value-level comparator (`sort`) plus that optional resolver. Keeping the comparison in `sort` and the normalization in `resolveDataValue` means a variant of an existing sorting function only has to swap the resolver. The definition is attached to the returned function, so you can spread any sorting function built with `constructSortFn` and override only what differs. + +For example, a version of `alphanumeric` that ignores diacritics, so that "Éric Bernard" sorts next to "Eric Brandon" instead of after "Zak O'Sullivan": + +```ts +const stripDiacritics = (value: string) => + value.normalize('NFD').replace(/\p{Diacritic}/gu, '') + +const alphanumericIgnoreDiacritics = constructSortFn({ + ...sortFn_alphanumeric, // reuse the comparator + resolveDataValue: (value) => + stripDiacritics(sortFn_alphanumeric.resolveDataValue!(value)), +}) + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { alphanumeric: sortFn_alphanumeric, alphanumericIgnoreDiacritics }, +}) +``` + +The same pattern works when defining a new sorting function from scratch: + +```ts +const byLastName = constructSortFn({ + sort: (dataValueA, dataValueB) => + dataValueA === dataValueB ? 0 : dataValueA > dataValueB ? 1 : -1, + resolveDataValue: (value) => + String(value ?? '') + .split(' ') + .at(-1) ?? '', +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```ts +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + columns, + data, + enableSorting: false, // disable sorting for the entire table +})) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, //sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, //sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + columns, + data, + sortDescFirst: true, //sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +})) +``` + +> [!NOTE] +> You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +If not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be passed to the sorting function like any other value with no special handling; the sorting function is responsible for handling them +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> [!NOTE] +> `'first'` and `'last'` options are available in v9. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this (the first direction depends on the column's data type and the `sortDescFirst` option, as discussed [above](#sorting-direction); a string column is shown here): + +`'none' -> 'asc' -> 'desc' -> 'none' -> 'asc' -> 'desc' -> ...` + +If you disable sorting removal, the `'none'` state is skipped after the first sort: + +`'none' -> 'asc' -> 'desc' -> 'asc' -> 'desc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (sorting can never return to 'none' once applied) +})) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```ts +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + columns, + data, + enableMultiSort: false, // disable multi-sorting for the entire table +})) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +})) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +})) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + enableMultiRemove: false, // disable the ability to remove multi-sorts +})) +``` + +### Reset Sorting When Data Changes + +Sorting state is preserved when the `data` option changes by default. Set `autoResetSorting: true` to reset sorting whenever a new data reference is processed. The reset restores `initialState.sorting`, or an empty sorting state when no initial value was provided. + +This option responds only to data changes. Changing sorting, filters, or grouping does not trigger it. The global `autoResetAll` option overrides `autoResetSorting` when explicitly set. + +Be careful when combining this option with manual/server-side sorting: a server response normally replaces `data`, so enabling the reset can immediately clear the sorting state that requested that response. + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/angular/guide/table-state.md b/docs/framework/angular/guide/table-state.md index 40c732299f..7f966e7e54 100644 --- a/docs/framework/angular/guide/table-state.md +++ b/docs/framework/angular/guide/table-state.md @@ -2,216 +2,331 @@ title: Table State (Angular) Guide --- +## Examples + +Want to skip to the implementation? Check out these examples: + +- [Basic injectTable](../examples/basic-inject-table) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) +- [Row Selection (Signals)](../examples/row-selection-signal) +- [With TanStack Query](../examples/with-tanstack-query) + ## Table State (Angular) Guide -TanStack Table has a simple underlying internal state management system to store and manage the state of the table. It also lets you selectively pull out any state that you need to manage in your own state management. This guide will walk you through the different ways in which you can interact with and manage the state of the table. +> **If you boil TanStack Table down to one sentence: TanStack Table is a large state-management coordinator for table states.** + +Understanding this guide is fundamental to understanding how TanStack Table works and how to interact with it for the best results. + +### Do you need to Manage External State? + +You usually do NOT need to manage table state yourself. If you pass nothing to `initialState`, `atoms`, `state`, or any of the `on[State]Change` table options, TanStack Table will manage its own state internally. + +There will be situations where you need to customize how you interact with the internal table state, or even hoist it up to your own scopes. TanStack Table lets you read, subscribe to, or own the state slices that matter to your app. This guide explains how table state works in Angular, how to read it, and when to use Angular signals or external state. + +### State in v9 + +TanStack Table v9 overhauled state management around TanStack Store. TanStack Store uses the `alien-signals` implementation and supports performant derived state. For Angular, the table adapter supplies reactivity bindings so table state atoms are backed by Angular signals. + +A table instance has a few state surfaces: + +- `table.baseAtoms` are the internal writable atoms created from the resolved initial state. +- `table.atoms` are readonly derived atoms exposed per registered state slice. +- `table.store` is the readonly flat TanStack Store derived by putting all of the registered `table.atoms` together. + +The Angular adapter provides `angularReactivity(injector)` as the table's reactivity binding. Core readonly atoms are Angular `computed` values, writable atoms are Angular `signal` values, and subscriptions bridge through `toObservable(computed(...), { injector })`. `injectTable` reruns the options initializer when Angular signals read inside it change, then calls `table.setOptions`. + +The returned table is also signal-reactive: table state and table APIs are wired for Angular signals, so you can consume table methods inside `computed(...)` and `effect(...)` and have those computations update when the underlying atom reads change. + +### Feature-based State + +State slices are only created for the features that are registered in `features`. This keeps TanStack Table tree-shakeable and gives TypeScript more accurate state inference. + +```ts +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), +})) + +this.table.atoms.pagination.get() +this.table.atoms.sorting.get() + +// this.table.atoms.rowSelection // TypeScript error unless rowSelectionFeature is registered +``` + +If `features` does not include a feature, its state should not be available in `table.atoms`, `table.store.get()`, `initialState`, `state`, or `atoms`. ### Accessing Table State -You do not need to set up anything special in order for the table state to work. If you pass nothing into either `state`, `initialState`, or any of the `on[State]Change` table options, the table will manage its own state internally. You can access any part of this internal state by using the `table.getState()` table instance API. +There are two different questions when reading table state: + +- Do you only need the current value? +- Or should an Angular signal, computed value, effect, or template update when that value changes? + +Use a direct atom read for the current value. Because Angular table atoms are backed by Angular signals, the same read also participates in Angular dependency tracking when it happens inside a template, `computed(...)`, or `effect(...)`. + +#### Reading State + +The simplest and most performant way to read a state value is to read the matching atom: ```ts -table = createAngularTable(() => ({ - columns: this.columns, +const pagination = this.table.atoms.pagination.get() +const sorting = this.table.atoms.sorting.get() +``` + +Use `table.store.get()` when you need the current flat state shape, such as debug JSON: + +```ts +const tableState = this.table.store.get() +const stateJson = JSON.stringify(this.table.store.get(), null, 2) +``` + +Atom reads are signal reads in Angular. If `this.table.atoms.pagination.get()` is used in a template expression, `computed(...)`, or `effect(...)`, Angular tracks it and updates when that atom changes. + +#### Selecting State with Angular computed + +Use Angular's native `computed(...)` when you want to derive a value from table state or apply a custom equality function. For object or array slices, pass `shallow` to avoid unnecessary downstream work when the selected value is structurally unchanged. + +```ts +import { computed } from '@angular/core' +import { shallow } from '@tanstack/angular-table' + +readonly table = injectTable(() => ({ + features, + columns, data: this.data(), - //... })) -someHandler() { - console.log(this.table.getState()) //access the entire internal state - console.log(this.table.getState().rowSelection) //access just the row selection state -} +readonly pagination = computed( + () => this.table.atoms.pagination.get(), + // if you want to pass a custom equality function + // { equal: shallow }, +) + +readonly pageIndex = computed(() => this.pagination().pageIndex) +``` + +You can also select from the flat store snapshot if that is more convenient, but prefer direct atoms for narrow render reads. + +```ts +readonly pagination = computed( + () => this.table.store.get().pagination, + { equal: shallow }, +) +``` + +Use `computed(...)` for selection, derivation, and equality control. You do not need it just to make an atom reactive; the atom already is backed by an Angular signal. + +### Setting Table State + +You should almost never need to set table state directly. TanStack Table features expose dedicated APIs for interacting with their state, and those APIs are the safest way to make changes. + +```ts +this.table.nextPage() +this.table.previousPage() +this.table.setPageIndex(0) +this.table.setPageSize(25) ``` +Use APIs like `table.setSorting(...)`, `table.setColumnFilters(...)`, `column.toggleVisibility()`, or `row.toggleSelected()` instead of manually editing the underlying state object. + +If you only care about setting starting values, use `initialState`. If you want to reset a state slice back to its initial value, use that feature's reset API. + +If you really do need to write a state slice directly, the low-level write surface for internally owned state is the matching base atom: + +```ts +this.table.baseAtoms.pagination.set((old) => ({ + ...old, + pageIndex: 0, +})) +``` + +Direct base atom writes should be rare. If a slice is owned by an external atom passed through `atoms`, write to that external atom instead; `table.atoms.pagination` will read from the external atom, not the internal base atom. + ### Custom Initial State -If all you need to do for certain states is customize their initial default values, you still do not need to manage any of the state yourself. You can simply set values in the `initialState` option of the table instance. +If you only need to customize the starting value for some table state, use `initialState`. You still do not need to manage that state yourself. + +`initialState` only applies to registered state slices. It is used to create the table's initial state and is also used by reset APIs such as `table.resetSorting()` or `table.resetPagination()`. Changing the `initialState` object later does not reset table state. -```jsx -table = createAngularTable(() => ({ - columns: this.columns, +```ts +readonly table = injectTable(() => ({ + features, + columns, data: this.data(), initialState: { - columnOrder: ['age', 'firstName', 'lastName'], //customize the initial column order - columnVisibility: { - id: false //hide the id column by default - }, - expanded: true, //expand all rows by default sorting: [ { id: 'age', - desc: true //sort by age in descending order by default - } - ] + desc: true, + }, + ], + pagination: { + pageIndex: 0, + pageSize: 25, + }, }, - //... })) ``` -> **Note**: Only specify each particular state in either `initialState` or `state`, but not both. If you pass in a particular state value to both `initialState` and `state`, the initialized state in `state` will take overwrite any corresponding value in `initialState`. +> [!NOTE] +> Do not provide the same state slice in multiple ownership places unless you intentionally want one to win. For a slice like `pagination`, prefer exactly one of `initialState.pagination`, `atoms.pagination`, or `state.pagination` as the source of truth. External atoms take precedence over external `state`; external `state` syncs into the table's internal base atom. + +#### Resetting to Initial State + +Feature reset APIs reset to `table.initialState` by default. Many reset APIs also accept `true` to reset to that feature's blank/default state instead: + +```ts +this.table.resetSorting() +this.table.resetPagination() +this.table.resetPagination(true) +``` + +Slice reset APIs like `resetPagination()` update through that feature's state updater and can update externally owned state. The core `table.reset()` API resets the internal base atoms, so do not use it as the primary way to reset state that is owned outside the table. ### Controlled State -If you need easy access to the table state in other areas of your application, TanStack Table makes it easy to control and manage any or all of the table state in your own state management system. You can do this by passing in your own state and state management functions to the `state` and `on[State]Change` table options. +If you need easy access to table state in other parts of your application, you can control individual state slices. In v9, external atoms are the recommended way to do this because they preserve the atomic state model and keep fine-grained subscriptions intact. -#### Individual Controlled State +#### External Atoms -You can control just the state that you need easy access to. You do NOT have to control all of the table state if you do not need to. It is recommended to only control the state that you need on a case-by-case basis. +Use external atoms when the app should own one or more table state slices. Create stable writable atoms with `createAtom` from `@tanstack/angular-store` (class fields work well, since they are created once per component instance) and pass them to the table's `atoms` option. The table's derived `table.atoms.` reads then come from your atom, and they stay signal-reactive in templates, `computed(...)`, and `effect(...)` just like internally owned slices. -In order to control a particular state, you need to both pass in the corresponding `state` value and the `on[State]Change` function to the table instance. +To consume the external atom itself inside Angular's reactive contexts, wrap it with `injectAtom` (or `injectSelector`) from `@tanstack/angular-store`, which returns an Angular signal. A plain `.get()` read returns the current snapshot and is fine in event handlers and other imperative code. -Let's take filtering, sorting, and pagination as an example in a "manual" server-side data fetching scenario. You can store the filtering, sorting, and pagination state in your own state management, but leave out any other state like column order, column visibility, etc. if your API does not care about those values. +This is especially useful for server-side data fetching. Pagination, sorting, or filters often belong in a query key, and external atoms let the app and the table share those values without funneling them through the `injectTable` options initializer (which re-runs whenever a signal read inside it changes). ```ts -import {signal} from '@angular/core'; -import {SortingState, ColumnFiltersState, PaginationState} from '@tanstack/angular-table' -import {toObservable} from "@angular/core/rxjs-interop"; -import {combineLatest, switchMap} from 'rxjs'; - -class TableComponent { - readonly columnFilters = signal([]) //no default filters - readonly sorting = signal([ - { - id: 'age', - desc: true, //sort by age in descending order by default - } - ]) - readonly pagination = signal({ +import { Component } from '@angular/core' +import { createAtom, injectAtom } from '@tanstack/angular-store' +import { injectQuery } from '@tanstack/angular-query-experimental' +import { + injectTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/angular-table' +import type { PaginationState } from '@tanstack/angular-table' + +const features = tableFeatures({ + rowPaginationFeature, +}) + +@Component({/* ... */}) +export class App { + readonly paginationAtom = createAtom({ pageIndex: 0, - pageSize: 15 + pageSize: 10, }) - //Use our controlled state values to fetch data - readonly data$ = combineLatest({ - filters: toObservable(this.columnFilters), - sorting: toObservable(this.sorting), - pagination: toObservable(this.pagination) - }).pipe( - switchMap(({filters, sorting, pagination}) => fetchData(filters, sorting, pagination)) - ) - readonly data = toSignal(this.data$); - - readonly table = createAngularTable(() => ({ - columns: this.columns, - data: this.data(), - //... - state: { - columnFilters: this.columnFilters(), //pass controlled state back to the table (overrides internal state) - sorting: this.sorting(), - pagination: this.pagination(), - }, - onColumnFiltersChange: updater => { //hoist columnFilters state into our own state management - updater instanceof Function - ? this.columnFilters.update(updater) - : this.columnFilters.set(updater) - }, - onSortingChange: updater => { - updater instanceof Function - ? this.sorting.update(updater) - : this.sorting.set(updater) - }, - onPaginationChange: updater => { - updater instanceof Function - ? this.pagination.update(updater) - : this.pagination.set(updater) + // an Angular signal view of the atom for reactive reads + readonly pagination = injectAtom(this.paginationAtom) + + readonly dataQuery = injectQuery(() => ({ + queryKey: ['data', this.pagination()], + queryFn: () => fetchData(this.pagination()), + })) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.dataQuery.data()?.rows ?? [], + rowCount: this.dataQuery.data()?.rowCount, + atoms: { + pagination: this.paginationAtom, }, + manualPagination: true, })) -} -//... + // table pagination APIs update paginationAtom +} ``` -#### Fully Controlled State +When using the `atoms` option for a slice, you do not need to add the matching `on[State]Change` option. For example, if you pass `atoms.pagination`, table pagination APIs update that atom directly. -Alternatively, you can control the entire table state with the `onStateChange` table option. It will hoist out the entire table state into your own state management system. Be careful with this approach, as you might find that raising some frequently changing state values up a component tree, like `columnSizingInfo` state`, might cause bad performance issues. +See the [Basic External Atoms example](../examples/basic-external-atoms) for a complete working version of this pattern (sorting and pagination owned by atoms), and the [With TanStack Query example](../examples/with-tanstack-query) for the server-side data fetching workflow (that example owns the slice with an Angular signal, but the query-key idea is the same). -A couple of more tricks may be needed to make this work. If you use the `onStateChange` table option, the initial values of the `state` must be populated with all of the relevant state values for all of the features that you want to use. You can either manually type out all of the initial state values, or use a constructor in a special way as shown below. - -```ts +#### External State +The classic `state` plus `on[State]Change` pattern is still supported. In Angular this means owning the slice with an Angular signal, passing its current value through `state`, and writing it back in the matching callback. This can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms: every signal write re-runs the `injectTable` options initializer and calls `table.setOptions`. The [Basic External State example](../examples/basic-external-state) shows this pattern in full. -class TableComponent { - // create an empty table state, we'll override it later - readonly state = signal({} as TableState); - - // create a table instance with default state values - readonly table = createAngularTable(() => ({ - columns: this.columns, - data: this.data(), - // our fully controlled state overrides the internal state - state: this.state(), - onStateChange: updater => { - // any state changes will be pushed up to our own state management - this.state.set( - updater instanceof Function ? updater(this.state()) : updater - ) - } - })) - - constructor() { - // set the initial table state - this.state.set({ - // populate the initial state with all of the default state values - // from the table instance - ...this.table.initialState, - pagination: { - pageIndex: 0, - pageSize: 15, // optionally customize the initial pagination state. - }, - }) - } -} +```ts +readonly sorting = signal([]) +readonly pagination = signal({ + pageIndex: 0, + pageSize: 10, +}) + +readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + state: { + sorting: this.sorting(), + pagination: this.pagination(), + }, + onSortingChange: (updater) => { + updater instanceof Function + ? this.sorting.update(updater) + : this.sorting.set(updater) + }, + onPaginationChange: (updater) => { + updater instanceof Function + ? this.pagination.update(updater) + : this.pagination.set(updater) + }, +})) ``` -### On State Change Callbacks +Use the per-slice `on[State]Change` callbacks to keep controlled table state slices atomic and separated. + +The v8-style `onStateChange` option (a single global state callback) is gone in v9. Use per-slice `on[State]Change` callbacks paired with `state.`, or external atoms via the `atoms` option. If you truly need to observe every state change, subscribe to `table.store` directly. -So far, we have seen the `on[State]Change` and `onStateChange` table options work to "hoist" the table state changes into our own state management. However, there are a few things about these using these options that you should be aware of. +##### On State Change Callbacks -#### 1. **State Change Callbacks MUST have their corresponding state value in the `state` option**. +The `on[State]Change` callbacks are useful when you are controlling a matching slice through the `state` option. They receive either a raw value or an updater function. -Specifying an `on[State]Change` callback tells the table instance that this will be a controlled state. If you do not specify the corresponding `state` value, that state will be "frozen" with its initial value. +If you provide an `on[State]Change` callback, also provide the corresponding value in `state`. For example, `onSortingChange` should be paired with `state.sorting`. ```ts -class TableComponent { - sorting = signal([]) - - table = createAngularTable(() => ({ - columns: this.columns, - data: this.data(), - //... - state: { - sorting: this.sorting(), // required because we are using `onSortingChange` - }, - onSortingChange: updater => { // makes the `state.sorting` controlled - updater instanceof Function - ? this.sorting.update(updater) - : this.sorting.set(updater) - } - })) +onPaginationChange: (updater) => { + updater instanceof Function + ? this.pagination.update(updater) + : this.pagination.set(updater) } ``` -#### 2. **Updaters can either be raw values or callback functions**. - -The `on[State]Change` and `onStateChange` callbacks work exactly like the `setState` functions in React. The updater values can either be a new state value or a callback function that takes the previous state value and returns the new state value. - -What implications does this have? It means that if you want to add in some extra logic in any of the `on[State]Change` callbacks, you can do so, but you need to check whether or not the new incoming updater value is a function or value. +### State Types -This is why you will see the `updater instanceof Function ? this.state.update(updater) : this.state.set(updater)` pattern in the examples above. This pattern checks if the updater is a function, and if it is, it calls the function with the previous state value to get the new state value, or the signal will require `signal.update` to be called with the updater instead of `signal.set`. +Most complex states in TanStack Table have their own TypeScript types that you can import and use. -### State Types +```ts +import { + injectTable, + type PaginationState, + type RowSelectionState, + type SortingState, + type TableState, +} from '@tanstack/angular-table' + +readonly sorting = signal([ + { + id: 'age', + desc: true, + }, +]) +``` -All complex states in TanStack Table have their own TypeScript types that you can import and use. This can be handy for ensuring that you are using the correct data structures and properties for the state values that you are controlling. +`TableState` is inferred from the features registered on that table: ```ts -import {createAngularTable, type SortingState} from '@tanstack/angular-table' - -class TableComponent { - readonly sorting = signal([ - { - id: 'age', // you should get autocomplete for the `id` and `desc` properties - desc: true, - } - ]) -} +type MyTableState = TableState ``` diff --git a/docs/framework/angular/guide/virtualization.md b/docs/framework/angular/guide/virtualization.md new file mode 100644 index 0000000000..5624afdbfa --- /dev/null +++ b/docs/framework/angular/guide/virtualization.md @@ -0,0 +1,281 @@ +--- +title: Virtualization (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Virtualized Columns](../examples/virtualized-columns) +- [Virtualized Rows](../examples/virtualized-rows) +- [Virtualized Infinite Scrolling](../examples/virtualized-infinite-scrolling) + +### Virtualization Setup + +Here's how you set up your table to use virtualization with TanStack Table. Virtualization is a rendering strategy, so TanStack Table does not need a feature or row model for it. + +Install and import the Angular virtualizer adapter from `@tanstack/angular-virtual`. TanStack Table still owns rows, columns, and table state; the virtualizer owns scroll indexes and measurements. +Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/angular/examples/table). + +## Virtualization (Angular) Guide + +The TanStack Table packages do not come with any virtualization APIs or features built in. Virtualization is a rendering strategy, not a table feature. You can use TanStack Table with any virtualization library, but the official examples use TanStack Virtual. + +TanStack Table and TanStack Virtual solve different parts of the problem: + +- TanStack Table builds the row models, columns, headers, cells, sizing, sorting, filtering, and other table state. +- TanStack Virtual decides which item indexes should be rendered for the current scroll position. +- Your table renderer maps those virtual indexes back to rows, headers, and cells. + +### When To Use Virtualization + +Use virtualization when your table has a very large number of rows, columns, or both. Virtualization keeps the DOM small by only rendering the items that are visible in the scroll viewport plus a small overscan buffer. + +Virtualization is not a replacement for server-side pagination, filtering, or sorting. If the data is virtualized on the client, the data still needs to exist on the client. If your dataset is too large to load into the browser, use server-side data operations or infinite scrolling. + +For small tables, normal rendering is simpler and usually preferable. + +### Install TanStack Virtual + +Install the Angular virtualizer adapter: + +```sh +npm install @tanstack/angular-virtual +``` + +The Angular examples use `injectVirtualizer` from `@tanstack/angular-virtual`. TanStack Table still owns rows, columns, headers, cells, sizing, sorting, filtering, and other table state; TanStack Virtual decides which item indexes should render for the current scroll position. + +The table itself is set up like any other v9 table. Declare your features with `tableFeatures()` and create the table with `injectTable`; nothing about virtualization changes the table setup. + +```ts +import { + columnSizingFeature, + rowSortingFeature, + createSortedRowModel, + sortFns, + tableFeatures, + injectTable, +} from '@tanstack/angular-table' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +const features = tableFeatures({ + columnSizingFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +export class App { + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +### The Basic Pattern + +Most virtualized table implementations follow the same pattern: + +1. Create a fixed-height scroll container. +2. Pass the scroll element to the virtualizer. +3. Use `table.getRowModel().rows` or `table.getVisibleLeafColumns()` as the source list. +4. Configure `count`, `estimateSize`, `overscan`, and optional `measureElement`. +5. Render only virtual items. +6. Use virtual offsets or spacer padding to preserve the full scroll geometry. + +Here is a compact row virtualization example: + +```ts +readonly rows = computed(() => this.table.getRowModel().rows) + +readonly rowVirtualizer = injectVirtualizer(() => ({ + count: this.rows().length, + scrollElement: this.scrollContainer()?.nativeElement, + estimateSize: () => 33, + overscan: 5, +})) +``` + +```html + + @for (virtualRow of rowVirtualizer.getVirtualItems(); track virtualRow.key) { + @let row = rows()[virtualRow.index]; + + @for (cell of row.getVisibleCells(); track cell.id) { + + } + + } + +``` + +### Virtualized Rows + +The [virtualized rows examples](../examples/virtualized-rows) show how to render large row counts while keeping the DOM small. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The core idea is that sorting, filtering, grouping, and other row-model work still comes from TanStack Table. The virtualizer reads from the final table row model: + +```ts +const rows = table.getRowModel().rows +``` + +The row virtualizer is configured with `count: rows.length`, a row height estimate, the scroll container, and an overscan value. The `tbody` is given the full virtual height with `rowVirtualizer.getTotalSize()`, while each rendered row is absolutely positioned with `transform: translateY(...)`. + +The examples render cells from the current row with APIs like `row.getVisibleCells()` or `row.getAllCells()`, depending on whether the example needs visibility-aware cells or all cells. + +The official examples use large generated datasets, commonly tens or hundreds of thousands of rows. They also support dynamic row heights by using `measureElement` when possible. The examples skip dynamic row measurement in Firefox because Firefox can measure table border height differently. + +### Virtualized Columns + +The [virtualized columns examples](../examples/virtualized-columns) show how to render large row and column counts. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +Column virtualization uses the current visible column list: + +```ts +const visibleColumns = table.getVisibleLeafColumns() +``` + +The column virtualizer is configured for horizontal virtualization: + +```ts +readonly visibleColumns = computed(() => this.table.getVisibleLeafColumns()) + +readonly columnVirtualizer = injectVirtualizer(() => ({ + count: this.visibleColumns().length, + estimateSize: index => this.visibleColumns()[index].getSize(), + scrollElement: this.scrollContainer()?.nativeElement, + horizontal: true, + overscan: 3, +})) +``` + +Column virtualization uses a different rendering strategy than row virtualization. Instead of absolutely positioning columns, the examples add fake spacer cells to the left and right: + +```ts +const virtualColumns = this.columnVirtualizer.getVirtualItems() +const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 +const virtualPaddingRight = + this.columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) +``` + +Those spacer cells preserve the horizontal scroll width while the renderer only mounts the virtual columns. This approach keeps row rendering table-like and allows dynamic row height measurement to keep working. + +### Virtualized Rows And Columns Together + +The official virtualized columns examples also virtualize rows. In those examples: + +- The row virtualizer controls vertical positioning and total body height. +- The column virtualizer controls horizontal header/cell rendering and left/right spacer cells. +- `virtualRow.index` maps to `rows[virtualRow.index]`. +- `virtualColumn.index` maps to `visibleCells[virtualColumn.index]`. + +Always use virtual indexes against the same current row and column lists returned by the table. If sorting, filtering, pagination, grouping, or column visibility changes, recompute the virtualized rows and columns from the current table state. + +### Virtualized Infinite Scrolling + +The [virtualized infinite scrolling examples](../examples/virtualized-infinite-scrolling) combine row virtualization with progressive data fetching. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The common pattern is: + +1. Fetch a page of rows. +2. Flatten fetched pages into the table `data`. +3. Use row virtualization over the loaded rows. +4. Listen to scroll events on the table container. +5. Fetch the next page when the user scrolls near the bottom. + +The Angular infinite scrolling pattern can use TanStack Query or any other data-fetching layer. + +```ts +const { scrollHeight, scrollTop, clientHeight } = scrollElement + +if (scrollHeight - scrollTop - clientHeight < 500) { + fetchNextPage() +} +``` + +If sorting is handled by the server, use manual sorting so the fetched data reflects the whole backend dataset rather than only the currently loaded rows. When sorting changes and the fetched dataset is replaced, scroll back to the top with `rowVirtualizer.scrollToIndex(0)`. + +### Dynamic Row Heights + +Dynamic row heights are useful when content can wrap or expand. They are also more complex than fixed-height rows. + +Use `estimateSize` as the virtualizer's initial guess: + +```ts +estimateSize: () => 33 +``` + +Then use `measureElement` to refine the actual row height after rendering. In Angular, pass a `measureElement` function to `injectVirtualizer` and set `data-index` on each rendered row so the virtualizer can associate measurements with the correct item. This is exactly what the [Virtualized Rows example](../examples/virtualized-rows) does: + +```ts +readonly rowVirtualizer = injectVirtualizer(() => ({ + count: this.rows().length, + scrollElement: this.scrollContainer()?.nativeElement, + estimateSize: () => 33, + // measure dynamic row height, except in firefox because it measures table border height incorrectly + measureElement: + typeof window !== 'undefined' && + navigator.userAgent.indexOf('Firefox') === -1 + ? (element) => element.getBoundingClientRect().height + : undefined, + overscan: 5, +})) +``` + +```html + +``` + +Overscan helps avoid blank regions while measurements settle. If every row has a known fixed height, skip dynamic measurement and use the fixed height estimate instead. + +### Sticky Headers And Semantic Table Markup + +The examples still use semantic table tags, but they change table layout CSS to support virtual positioning and sticky headers. + +Dynamic row virtualization commonly requires: + +```css +table { + display: grid; +} + +thead { + display: grid; + position: sticky; + top: 0; +} + +tr { + display: flex; +} +``` + +Rows are absolutely positioned inside a relatively positioned `tbody`, and cells use flex sizing so they can match `column.getSize()` or `cell.column.getSize()`. This is intentional. Native table layout does not work well with dynamic-height virtual rows that are positioned independently. + +### Performance Tips + +- Keep virtualizers near the components that render the virtualized items. +- Avoid re-rendering the full table body on every scroll. +- Keep row, column, and data references stable where possible. +- Use `overscan` deliberately. More overscan reduces visible blanking, while less overscan reduces DOM nodes. +- Avoid expensive cell renderers in very large virtualized tables. +- Test production builds. Framework development builds can be slower than production builds; profile production bundles before optimizing. +- Prefer fixed row sizes when the UI allows it. +- For column virtualization, use `column.getSize()`, `header.getSize()`, and `cell.column.getSize()` consistently. diff --git a/docs/framework/angular/quick-start.md b/docs/framework/angular/quick-start.md new file mode 100644 index 0000000000..b7f91a5df6 --- /dev/null +++ b/docs/framework/angular/quick-start.md @@ -0,0 +1,206 @@ +--- +title: Quick Start +--- + +TanStack Table is a headless table library. It manages your table's state and logic (sorting, filtering, pagination, selection, and more) while you keep 100% control over the markup and styles. This page gets you from install to a rendering Angular table, then shows how to layer on your first feature. + +## Installation + +```bash +npm install @tanstack/angular-table +``` + +## Your First Table + +The component and template below are complete. Drop them into an Angular app and you will see a working table. + +```ts +// app.ts +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { FlexRender, injectTable, tableFeatures } from '@tanstack/angular-table' +import type { ColumnDef } from '@tanstack/angular-table' + +// 1. Define the shape of your data +type Person = { + firstName: string + lastName: string + age: number +} + +// 2. Create some data with a stable reference +const defaultData: Array = [ + { firstName: 'tanner', lastName: 'linsley', age: 24 }, + { firstName: 'tandy', lastName: 'miller', age: 40 }, + { firstName: 'joe', lastName: 'dirte', age: 45 }, +] + +// 3. New in v9: declare which features this table uses (none yet) +const features = tableFeatures({}) + +// 4. Define your columns +const columns: Array> = [ + { + accessorKey: 'firstName', // accessorKey shorthand + header: 'First Name', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, // accessorFn alternative with a custom id + id: 'lastName', + header: () => 'Last Name', + cell: (info) => info.getValue(), + }, + { + accessorKey: 'age', + header: () => 'Age', + }, +] + +@Component({ + selector: 'app-root', + imports: [FlexRender], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + // 5. Own the data with a signal so updates flow into the table + readonly data = signal>([...defaultData]) + + // 6. Create the table instance + readonly table = injectTable(() => ({ + key: 'person-table', // registers this table with the devtools + features, + columns, + data: this.data(), + })) +} +``` + +```html + +
`, etc. remains the same. + +The main change is **how you define a table** with the Angular adapter, specifically the new `features` option and how row model factories are registered inside it. + +## Core Breaking Changes + +### Entrypoint Change + +The Angular adapter entrypoint to create a table instance is `injectTable`: + +```ts +// v8 +import { createAngularTable } from '@tanstack/angular-table' + +const v8Table = createAngularTable(() => ({ + // options +})) + +// v9 +import { injectTable } from '@tanstack/angular-table' + +const v9Table = injectTable(() => ({ + // options +})) +``` + +> [!NOTE] +> `injectTable` evaluates your initializer whenever any Angular signal read inside of it changes. +> Keep expensive/static values (like `columns` and `features`) as stable references outside the initializer. + +### New Required `features` Table Option + +In Table V9, you must explicitly declare which features your table uses. Features, Row Models, and Row Model processing "Fns" are defined on the new `features` table option. + +In Table V8, all features were bundled and included in the table setup. In Table V9, you import only what you need. + +```ts +// Table V8 +import { + createAngularTable, + getCoreRowModel, + getSortedRowModel, + sortingFns, +} from '@tanstack/angular-table' + +const v8Table = createAngularTable(() => ({ + columns, + data: data(), + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + sortingFns, +})) + +// Table V9 +import { + createSortedRowModel, + injectTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' + +// All table options that concern including code modules (features, row models, Fns, etc.) +const features = tableFeatures({ + rowSortingFeature, // new - import and pass the feature you want to use + sortedRowModel: createSortedRowModel(), // now row models are defined on the features object + sortFns, // now Fns are defined on the features object + // ...more features, row models, etc. +}) + +// Define stable references outside the initializer +const v9Table = injectTable(() => ({ + features, // new required option + columns: this.columns, + data: this.data(), +})) +``` + +#### Shortcut: Use `stockFeatures` for Table V8-like Behavior + +If you want all features without thinking about it (like Table V8), import `stockFeatures`: + +```ts +import { injectTable, stockFeatures } from '@tanstack/angular-table' + +class TableCmp { + readonly table = injectTable(() => ({ + features: stockFeatures, // All features included - just like Table V8 + columns: this.columns, + data: this.data(), + })) +} +``` + +#### Available Features + +| Feature | Import Name | +| ----------------- | ------------------------- | +| Column Faceting | `columnFacetingFeature` | +| Column Filtering | `columnFilteringFeature` | +| Column Grouping | `columnGroupingFeature` | +| Column Ordering | `columnOrderingFeature` | +| Column Pinning | `columnPinningFeature` | +| Column Resizing | `columnResizingFeature` | +| Column Sizing | `columnSizingFeature` | +| Column Visibility | `columnVisibilityFeature` | +| Global Filtering | `globalFilteringFeature` | +| Row Aggregation | `rowAggregationFeature` | +| Row Expanding | `rowExpandingFeature` | +| Row Pagination | `rowPaginationFeature` | +| Row Pinning | `rowPinningFeature` | +| Row Selection | `rowSelectionFeature` | +| Row Sorting | `rowSortingFeature` | + +### Row Model Factories + +Row models are the functions that process your data (filtering, sorting, pagination, etc.). In Table V9, row model factories and their `*Fns` registries move from a separate `rowModels` option into `tableFeatures`. Row model slots are type-checked, so each row model must be specified after its associated feature in the same `tableFeatures` call. + +#### Migration Mapping + +| Table V8 Option | Table V9 `tableFeatures` Slot | Table V9 Factory Function | +| -------------------------- | ----------------------------- | ----------------------------- | +| `getCoreRowModel()` | (automatic) | Not needed, always included | +| `getFilteredRowModel()` | `filteredRowModel` | `createFilteredRowModel()` | +| `getSortedRowModel()` | `sortedRowModel` | `createSortedRowModel()` | +| `getPaginationRowModel()` | `paginatedRowModel` | `createPaginatedRowModel()` | +| `getExpandedRowModel()` | `expandedRowModel` | `createExpandedRowModel()` | +| `getGroupedRowModel()` | `groupedRowModel` | `createGroupedRowModel()` | +| `getFacetedRowModel()` | `facetedRowModel` | `createFacetedRowModel()` | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues` | `createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues` | `createFacetedUniqueValues()` | + +The `filterFns`, `sortFns`, and `aggregationFns` objects are now registered as named slots on `tableFeatures` rather than passed as arguments to the factory functions. + +#### Key Change: Row Model Factories and Fn Registries Move into `tableFeatures` + +```ts +import { + tableFeatures, + createFilteredRowModel, + createSortedRowModel, + createGroupedRowModel, + createPaginatedRowModel, + filterFns, // Built-in filter functions + sortFns, // Built-in sort functions + aggregationFns, // Built-in aggregation functions +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowSortingFeature, + rowAggregationFeature, + columnGroupingFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), + groupedRowModel: createGroupedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns, + sortFns, + aggregationFns, +}) + +class TableCmp { + readonly table = injectTable(() => ({ + features, + columns: this.columns, + data: this.data(), + })) +} +``` + +#### Full Migration Example + +```ts +// v8 +import { + createAngularTable, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + filterFns, + sortingFns, +} from '@tanstack/angular-table' + +const v8Table = createAngularTable(() => ({ + columns, + data: data(), + getCoreRowModel: getCoreRowModel(), // used to be called "get*RowModel()" + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + filterFns, // used to be passed in as a root option + sortingFns, +})) + +// v9 +import { + injectTable, + tableFeatures, + columnFilteringFeature, + rowSortingFeature, + rowPaginationFeature, + createFilteredRowModel, + createSortedRowModel, + createPaginatedRowModel, + filterFns, + sortFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowSortingFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns, + sortFns, +}) + +const v9Table = injectTable(() => ({ + features, + columns, + data: data(), +})) +``` + +#### Prefer Individual Fn Imports Over Full Registries + +The `filterFns`, `sortFns`, and `aggregationFns` registry exports are now deprecated in favor of importing individual `filterFn_*`, `sortFn_*`, and `aggregationFn_*` functions and registering only the ones you use (or passing functions directly in column definitions with no registration at all). The full registries still work, but spreading them puts every built-in function in your bundle. String names, including the default `'auto'`, only resolve functions you have registered. + +```ts +// Before: registers every built-in function +import { filterFns, sortFns } from '@tanstack/angular-table' + +const features = tableFeatures({ + // ...other features and row models + filterFns, + sortFns, +}) + +// After: registers only the functions you use +import { + filterFn_includesString, + sortFn_alphanumeric, + sortFn_text, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + // ...other features and row models + filterFns: { includesString: filterFn_includesString }, + sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text }, +}) +``` + +### Instance Methods Must Be Called on Their Instance + +In v9, methods on rows, cells, columns, headers, and similar table objects are shared on the object's prototype instead of being created as arrow functions on each object. This improves memory usage, but it means destructuring those methods loses the `this` context they need to operate on the instance. + +```ts +// v8 - worked because getValue closed over the row object +const { getValue } = row +const value = getValue('name') + +// v9 - call the method on the instance +const value = row.getValue('name') +``` + +This applies to row, cell, column, header, and related instance APIs, but not to the table instance itself. Audit code that destructures methods from table objects or passes them around as bare callbacks. Prefer calling them through the original object, for example `row.getValue('name')`, `cell.getContext()`, `column.getCanSort()`, or `header.getContext()`. + +Because these methods now live on the prototype, they also do not appear as own properties in `Object.keys(instance)`, object spread, or `JSON.stringify`. A shallow clone like `{ ...row }` copies row data but does not copy row methods. The methods are still callable normally because JavaScript looks them up through the prototype chain. + +--- + +## State Management Changes + +### Accessing State + +In v8, you accessed state via `table.getState()`. In v9, read the specific +state slice from `table.atoms..get()` where possible. Use `table.store.get()` +when you need the full flat state shape, such as debug JSON. + +```ts +// v8 +const state = table.getState() +const v8 = table.getState() +const { sorting, pagination } = v8 + +// v9 - per-slice reads, preferred for Angular render code +const sorting = table.atoms.sorting.get() +const pagination = table.atoms.pagination.get() + +// v9 - full-state flat snapshot +const fullState = table.store.get() +const v9 = table.store.get() +const { sorting: v9Sorting, pagination: v9Pagination } = v9 +``` + +### Optimizing Reads with Angular Signals + +In Angular, you have a few good options for consuming table state. + +#### Option 1: Read table atoms directly + +The Angular adapter backs table atoms with Angular signals. Read the atom you care about directly in templates, effects, or computed values. + +```ts +import { computed, effect } from '@angular/core' +import { shallow } from '@tanstack/angular-table' + +class TableCmp { + readonly table = injectTable(() => ({ + features, + columns: this.columns, + data: this.data(), + })) + + // Use computed when deriving from a slice or applying equality. + private readonly pagination = computed( + () => this.table.atoms.pagination.get(), + { + equal: shallow, + }, + ) + + constructor() { + effect(() => { + const { pageIndex, pageSize } = this.pagination() + console.log('Page', pageIndex, 'Size', pageSize) + }) + } +} +``` + +#### Option 2: Use `computed(...)` for selected object slices + +Use Angular `computed(...)` when you want selector-style behavior, a derived value, or an equality function. For object/array slices, use `shallow` from `@tanstack/angular-table` to avoid unnecessary downstream work when the slice is recreated with the same values. + +```ts +import { computed, effect } from '@angular/core' +import { shallow } from '@tanstack/angular-table' + +class TableCmp { + readonly table = injectTable(() => ({ + features, + columns: this.columns, + data: this.data(), + })) + + // Provide an equality function for object slices + readonly pagination = computed(() => this.table.atoms.pagination.get(), { + equal: shallow, + }) + + constructor() { + effect(() => { + // This effect only re-runs when pagination changes + const { pageIndex, pageSize } = this.pagination() + console.log('Page', pageIndex, 'Size', pageSize) + }) + } +} +``` + +### Controlled State + +The v8-style `state` + `on[State]Change` controlled state patterns still work and remain convenient for simple integrations. For new v9 code, prefer owning state slices with external atoms via the new `atoms` table option (created with `createAtom` from `@tanstack/angular-store`), which give you fine-grained subscriptions without mirroring state through Angular signals. See the [External Atoms section of the Table State Guide](./table-state#external-atoms) and the [Basic External Atoms example](../examples/basic-external-atoms). + +```ts +import { signal } from '@angular/core' +import type { SortingState, PaginationState } from '@tanstack/angular-table' + +class TableCmp { + readonly sorting = signal([]) + readonly pagination = signal({ pageIndex: 0, pageSize: 10 }) + + readonly table = injectTable(() => ({ + features, + columns: this.columns, + data: this.data(), + state: { + sorting: this.sorting(), + pagination: this.pagination(), + }, + onSortingChange: (updater) => { + updater instanceof Function + ? this.sorting.update(updater) + : this.sorting.set(updater) + }, + onPaginationChange: (updater) => { + updater instanceof Function + ? this.pagination.update(updater) + : this.pagination.set(updater) + }, + })) +} +``` + +The v8-style `onStateChange` callback is no longer part of the v9 table state model. Use per-slice `on[State]Change` callbacks or subscribe to the table store when you need to listen to all state changes. + +```ts +const unsubscribe = this.table.store.subscribe((state) => { + console.log(state) +}) +``` + +--- + +## Feature-by-Feature Breaking Changes + +### Sorting + +Sorting-related APIs have been renamed for consistency: + +| v8 | v9 | +| --------------------------------- | ------------------------ | +| `sortingFn` (column def option) | `sortFn` | +| `column.getSortingFn()` | `column.getSortFn()` | +| `column.getAutoSortingFn()` | `column.getAutoSortFn()` | +| `SortingFn` type | `SortFn` type | +| `SortingFns` interface | `SortFns` interface | +| `sortingFns` (built-in functions) | `sortFns` | + +Update your column definitions. + +### Column Pinning + +V9 changes column pinning to use logical `start`/`end` terminology instead of the physical `left`/`right` terminology used in V8. In LTR languages/layouts, `start` usually corresponds to left and `end` to right; in RTL languages/layouts, `start` usually corresponds to right and `end` to left. There are no deprecated aliases. + +| V8 | V9 | +| ------------------------------------ | ------------------------------------ | +| `columnPinning.left` | `columnPinning.start` | +| `columnPinning.right` | `columnPinning.end` | +| `column.pin('left')` | `column.pin('start')` | +| `column.pin('right')` | `column.pin('end')` | +| `column.getIsPinned() === 'left'` | `column.getIsPinned() === 'start'` | +| `column.getIsPinned() === 'right'` | `column.getIsPinned() === 'end'` | +| `row.getLeftVisibleCells()` | `row.getStartVisibleCells()` | +| `row.getRightVisibleCells()` | `row.getEndVisibleCells()` | +| `table.getLeftHeaderGroups()` | `table.getStartHeaderGroups()` | +| `table.getRightHeaderGroups()` | `table.getEndHeaderGroups()` | +| `table.getLeftLeafColumns()` | `table.getStartLeafColumns()` | +| `table.getRightLeafColumns()` | `table.getEndLeafColumns()` | +| `table.getLeftVisibleLeafColumns()` | `table.getStartVisibleLeafColumns()` | +| `table.getRightVisibleLeafColumns()` | `table.getEndVisibleLeafColumns()` | +| `table.getLeftTotalSize()` | `table.getStartTotalSize()` | +| `table.getRightTotalSize()` | `table.getEndTotalSize()` | +| `column.getStart('left')` | `column.getStart('start')` | +| `column.getAfter('right')` | `column.getAfter('end')` | +| `column.getIndex('left')` | `column.getIndex('start')` | +| `column.getIndex('right')` | `column.getIndex('end')` | + +This rename is about logical table regions, not automatic DOM direction handling. For sticky column pinning, prefer CSS logical properties like `insetInlineStart` and `insetInlineEnd`. The `columnResizeDirection` table option is unchanged. + +The `enablePinning` option has also been split into separate options: + +```ts +// v8 +enablePinning: true + +// v9 +enableColumnPinning: true +enableRowPinning: true +``` + +### Column Sizing vs. Column Resizing Split + +In v8, column sizing and resizing were combined in a single feature. In v9, they've been split into separate features for better tree-shaking. + +| v8 | v9 | +| --------------------------------- | ----------------------------------------------- | +| `ColumnSizing` (combined feature) | `columnSizingFeature` + `columnResizingFeature` | +| `columnSizingInfo` state | `columnResizing` state | +| `setColumnSizingInfo()` | `setColumnResizing()` | +| `onColumnSizingInfoChange` option | `onColumnResizingChange` option | + +If you only need column sizing (fixed widths) without interactive resizing, you can import just `columnSizingFeature`. If you need drag-to-resize functionality, import both. + +### Grouping and Aggregation + +Aggregation is now its own feature, independent from column grouping. `stockFeatures` still includes both, so tables using it need no feature-registration change. If you declare features explicitly, add `rowAggregationFeature` whenever columns use `aggregationFn`, `aggregatedCell`, `getAggregationValue`, or `cell.getIsAggregated`. Add `columnGroupingFeature` and `groupedRowModel` only when you also group rows. + +```ts +const features = tableFeatures({ + rowAggregationFeature, + columnGroupingFeature, // only for grouped rows + groupedRowModel: createGroupedRowModel(), + aggregationFns: { sum: aggregationFn_sum }, +}) +``` + +Custom aggregation callables have changed to context-based definitions: + +```ts +// Table V8/earlier V9 betas +const total = (columnId, leafRows, childRows) => + leafRows.reduce((sum, row) => sum + row.getValue(columnId), 0) + +// Current V9 +const total = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((sum, row) => sum + Number(getValue(row)), 0), +}) +``` + +The old per-function choice between `childRows` and `leafRows` is replaced by a single depth-selected `context.rows`, controlled by the `maxAggregationDepth` column option. The default (`0`) preserves V8's direct-child grouped aggregation; use `Infinity` to aggregate terminal leaf rows. + +`column.getAggregationValue()` now takes a single options object instead of positional arguments: + +```ts +// Table V8/earlier V9 betas +column.getAggregationValue(rows, maxDepth) + +// Current V9 +column.getAggregationValue({ rows, maxDepth }) +``` + +`column.getAggregationFn()` is now `column.getAggregationFns()` because a column can run multiple definitions, and the old callable `AggregationFn`/`CreatedAggregationFn` types are replaced by `AggregationFnDef`. + +See the [Grouping Guide](./grouping) and the [Aggregation Guide](./aggregation) for full documentation of the new capabilities. + +### Row Selection + +> [!WARNING] +> **Minor breaking change:** `row.getToggleSelectedHandler()` now enables inclusive Shift range selection by default when `rowSelectionFeature` is enabled. Existing checkboxes or rows wired through this handler establish an anchor on an ordinary interaction and select or deselect the current display-order range on a Shift interaction. Direct `row.toggleSelected()` calls are unchanged. +> +> Set `enableRowRangeSelection: false` to preserve the previous non-range handler behavior. The handler must receive an event that exposes Shift directly or through `nativeEvent`; see [Shift Range Selection](./row-selection.md#shift-range-selection). + +The "some rows selected" checks were simplified to mean "at least one row is selected": + +| API | v8 | v9 | +| ----------------------------------- | --------------------------------------------------- | --------------------------------------------- | +| `table.getIsSomeRowsSelected()` | `true` when some but not all rows are selected | `true` when at least one row is selected | +| `table.getIsSomePageRowsSelected()` | `true` when some but not all page rows are selected | `true` when at least one page row is selected | + +In v8 these returned `false` once every row was selected; in v9 they stay `true`. If you use them to drive an indeterminate "select all" checkbox, gate the indeterminate state on the matching all-selected check so it clears at full selection: + +`getIsSomeRowsSelected() && !getIsAllRowsSelected()` + +### Row and Internal API Changes + +Some row APIs have changed from private to public: + +| v8 | v9 | +| ---------------------------------------- | -------------------------------------- | +| `row._getAllCellsByColumnId()` (private) | `row.getAllCellsByColumnId()` (public) | + +All other internal APIs prefixed with `_` have been removed. If you were using any of these, use their public equivalents. + +- Removed: `table._getPinnedRows()` +- Removed: `table._getFacetedRowModel()` +- Removed: `table._getFacetedMinMaxValues()` +- Removed: `table._getFacetedUniqueValues()` + +--- + +## Column Helper Changes + +The `createColumnHelper` function now requires a `TFeatures` type parameter in addition to `TData`: + +```ts +// v8 +import { createColumnHelper } from '@tanstack/angular-table' + +const columnHelperV8 = createColumnHelper() + +// v9 +import { + createColumnHelper, + tableFeatures, + rowSortingFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowSortingFeature }) +const columnHelperV9 = createColumnHelper() +``` + +### New `columns()` Helper Method + +v9 adds a `columns()` helper for better type inference when wrapping column arrays. + +```ts +const columnHelper = createColumnHelper() + +// Wrap your columns array for better type inference +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + id: 'lastName', + header: () => 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.display({ + id: 'actions', + header: 'Actions', + cell: () => 'Edit', + }), +]) +``` + +### Using with `createTableHook` + +When using `createTableHook`, you get a pre-bound `createAppColumnHelper` that only requires `TData`: + +```ts +import { + createTableHook, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { injectAppTable, createAppColumnHelper } = createTableHook({ features }) + +// TFeatures is already bound, only need TData! +const columnHelper = createAppColumnHelper() +``` + +--- + +## Rendering Changes + +### `FlexRender` + +The rendering primitives in the Angular adapter are `FlexRender` and the `*flexRender` directives. + +In v9, you can continue to render header/cell/footer content using the Angular adapter rendering utilities, but there are a few important improvements and helper APIs to be aware of. + +#### Structural directive rendering + +Angular rendering is directive-based: + +- `FlexRender` / `*flexRender` renders arbitrary render content (primitives, `TemplateRef`, component types, or `flexRenderComponent(...)` wrappers) +- The directive is responsible for mounting embedded views or components via `ViewContainerRef` + +#### Shorthand directives + +If you're rendering standard table content, prefer the shorthand helpers: + +- `*flexRenderCell="cell; let value"` +- `*flexRenderHeader="header; let value"` +- `*flexRenderFooter="footer; let value"` + +These automatically select the correct column definition (`columnDef.cell` / `header` / `footer`) and the right props (`cell.getContext()` / `header.getContext()`), so you don't need to manually provide `props:`. + +#### DI-aware render functions + context injection + +Column definition render functions (`header`, `cell`, `footer`) run inside an Angular injection context, so they can safely call `inject()` and use signals. + +When a component is rendered through the FlexRender directives, you can also access the full render props object via DI using `injectFlexRenderContext()`. + +#### Component rendering helper: `flexRenderComponent` + +If you need to render an Angular component with explicit configuration (custom `inputs`, `outputs`, `injector`, and Angular v20+ creation-time `bindings`/`directives`), return a `flexRenderComponent(Component, options)` wrapper from your column definition. + +For complete rendering details (including component rendering, `TemplateRef`, `flexRenderComponent`, and context helpers), see the [FlexRender Guide](./flex-render). + +--- + +## The `tableOptions()` Utility + +The `tableOptions()` helper provides type-safe composition of table options. It's useful for creating reusable partial configurations that can be spread into your table setup. + +### Basic Usage + +```ts +import { + injectTable, + tableOptions, + tableFeatures, + rowSortingFeature, +} from '@tanstack/angular-table' +import { isDevMode } from '@angular/core' + +const features = tableFeatures({ rowSortingFeature }) + +// Create a reusable options object with features pre-configured +const baseOptions = tableOptions({ + features, + debugTable: isDevMode(), +}) + +class TableCmp { + readonly table = injectTable(() => ({ + ...baseOptions, + columns: this.columns, + data: this.data(), + })) +} +``` + +### Composing Partial Options + +`tableOptions()` lets you omit required fields (like `data`, `columns`, or `features`) when creating partial configurations: + +```ts +import { + tableOptions, + tableFeatures, + rowSortingFeature, + columnFilteringFeature, + createSortedRowModel, + createFilteredRowModel, + filterFns, + sortFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowSortingFeature, + columnFilteringFeature, + sortedRowModel: createSortedRowModel(), + filteredRowModel: createFilteredRowModel(), + sortFns, + filterFns, +}) + +// Partial options without data or columns +const featureOptions = tableOptions({ features }) +``` + +```ts +import { injectTable, tableOptions } from '@tanstack/angular-table' + +// Another partial (inherits features from spread) +const paginationDefaults = tableOptions({ + initialState: { + pagination: { pageIndex: 0, pageSize: 25 }, + }, +}) + +class TableCmp { + readonly table = injectTable(() => ({ + ...featureOptions, + ...paginationDefaults, + columns: this.columns, + data: this.data(), + })) +} +``` + +### Using with `createTableHook` + +`tableOptions()` pairs well with `createTableHook` for building composable table factories: + +```ts +import { + createTableHook, + tableOptions, + tableFeatures, + rowSortingFeature, + rowPaginationFeature, + createSortedRowModel, + createPaginatedRowModel, + sortFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowSortingFeature, + rowPaginationFeature, + sortedRowModel: createSortedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortFns, +}) + +const sharedOptions = tableOptions({ features }) + +const { injectAppTable } = createTableHook(sharedOptions) +``` + +--- + +## `createTableHook`: Composable Table Patterns + +**This is an advanced, optional feature.** You don't need to use `createTableHook`; `injectTable` is sufficient for most use cases. + +For applications with multiple tables sharing the same configuration, `createTableHook` lets you define features, row models, and reusable components once. + +For full setup and patterns, see the [Composable Tables Guide](./composable-tables.md). + +--- + +## TypeScript Changes Summary + +### Type Generics + +Most types now require a `TFeatures` parameter: + +```txt +// v8 +type Column +type ColumnDef +type Table +type Row +type Cell + +// v9 +type Column +type ColumnDef +type Table +type Row +type Cell +``` + +### Using `typeof features` + +The easiest way to get the `TFeatures` type is with `typeof`: + +```ts +const features = tableFeatures({ + rowSortingFeature, + columnFilteringFeature, +}) + +type MyFeatures = typeof features + +const columns: ColumnDef[] = [...] +``` + +### Using `StockFeatures` + +If using `stockFeatures`, use the `StockFeatures` type: + +```ts +import type { StockFeatures, ColumnDef } from '@tanstack/angular-table' + +const columns: ColumnDef[] = [...] +``` + +### `TableMeta`/`ColumnMeta` Typing Changes + +No more declaration merging required! (Although it still works if you want to keep using it) + +Global declaration merging to extend `TableMeta` or `ColumnMeta` works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take `TFeatures` as the first type parameter. + +Optionally, v9 also adds a new way to declare meta types **per-table** without declaration merging. You can use type-only `tableMeta`/`columnMeta` slots on the `features` option, which only affect tables created with that `features` object: + +```ts +const features = tableFeatures({ + rowSortingFeature, + columnMeta: metaHelper<{ customProperty: string }>(), +}) +``` + +See the new [Table and Column Meta Guide](../../../guide/table-and-column-meta) for full details on both approaches. + +### `FilterFns`/`SortFns`/`AggregationFns`/`FilterMeta` Augmentation Replaced by Registry Slots + +In v8, making a custom function usable as a string reference (like `filterFn: 'fuzzy'`) required `declare module` augmentation of the `FilterFns` interface, and typing filter meta required augmenting `FilterMeta`. In v9, registering the function in the matching registry slot does both jobs with no global augmentation: + +```ts +// v8 +declare module '@tanstack/angular-table' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +// v9 - register in the slot; the key becomes a valid string value +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { fuzzy: fuzzyFilter }, + filterMeta: metaHelper(), +}) + +// 'fuzzy' now typechecks in column defs for tables using these features +columnHelper.accessor('name', { filterFn: 'fuzzy' }) +``` + +The same pattern applies to `sortFns` (for `sortFn` string values) and `aggregationFns` (for `aggregationFn` string values). See the [Fuzzy Filtering Guide](./fuzzy-filtering.md) for a complete example. + +### `RowData` Type Restriction + +The `RowData` type is now more restrictive: + +```ts +// v8 - very permissive +type RowData = unknown + +// v9 - must be a record or array +type RowData = Record | Array +``` + +This change improves type safety. If you were passing unusual data types, ensure your data conforms to `Record` or `Array`. + +--- + +## Migration Checklist + +- [ ] Update your table setup to v9 and define `features` using `tableFeatures()` (or use `stockFeatures`) +- [ ] Migrate `get*RowModel()` options: move row model factories into `tableFeatures` as named slots +- [ ] Move `filterFns`, `sortFns`, and `aggregationFns` into `tableFeatures` as named slots (no longer passed as factory arguments) +- [ ] Replace destructured row/cell/column/header methods with calls on the instance (for example, `row.getValue('name')`) +- [ ] Rename `sortingFn` → `sortFn` in column definitions +- [ ] Update column pinning to `start`/`end` terminology (`columnPinning.start`, `column.pin('end')`, `getStart*`/`getEnd*` APIs) +- [ ] Replace `enablePinning` with `enableColumnPinning`/`enableRowPinning` if used +- [ ] Rename `columnSizingInfo` state → `columnResizing` (and related options) +- [ ] Convert custom aggregation callables to `constructAggregationFn({ aggregate, merge? })` definitions +- [ ] Update state access: `table.getState().slice` → `table.atoms..get()` where possible; use `table.store.get()` for full-state/debug reads +- [ ] Update TypeScript types to include `TFeatures` generic +- [ ] Update `createColumnHelper()` → `createColumnHelper()` +- [ ] If you use `TableMeta`/`ColumnMeta` declaration merging, add the `TFeatures` generic to your augmentations (optionally, switch to the per-table `tableMeta`/`columnMeta` feature slots) +- [ ] Replace `declare module` augmentation of `FilterFns`/`SortFns`/`AggregationFns` with registry-slot registration, and `FilterMeta` augmentation with the `filterMeta` slot +- [ ] (Optional) Use `tableOptions()` for composable configurations +- [ ] (Optional) Use `createTableHook` for reusable table patterns + +--- + +## Examples + +Check out these examples to see v9 patterns in action: + +- [Basic (Inject Table)](../examples/basic-inject-table) +- [Basic (App Table)](../examples/basic-app-table) +- [Filters](../examples/filters) +- [Column Ordering](../examples/column-ordering) +- [Column Pinning](../examples/column-pinning) +- [Column Visibility](../examples/column-visibility) +- [Expanding](../examples/expanding) +- [Grouping](../examples/grouping) +- [Row Selection](../examples/row-selection) +- [Composable Tables](../examples/composable-tables) diff --git a/docs/framework/angular/guide/pagination.md b/docs/framework/angular/guide/pagination.md new file mode 100644 index 0000000000..ee7e6ef51c --- /dev/null +++ b/docs/framework/angular/guide/pagination.md @@ -0,0 +1,391 @@ +--- +title: Pagination (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Pagination](../examples/pagination) + +### Pagination Setup + +Here's how you set up your table to use pagination features. Adding the pagination feature enables the related APIs. If you use client-side pagination, also set up `paginatedRowModel` after its feature, since row model slots are type-checked. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), // if using client-side pagination + // manualPagination: true, // if using manual server-side pagination +}) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Pagination (Angular) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain **_ALL_** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. + +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. + +#### Should You Use Virtualization Instead? + +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` and the `paginatedRowModel` factory to your features: + +```ts +import { + injectTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) + +readonly table = injectTable(() => ({ + features, + columns, + data, +})) +``` + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, pass `-1` for `pageCount`. In that case, `getCanNextPage()` returns `true` because the table cannot detect the end, `getCanPreviousPage()` depends on the current `pageIndex`, and `getCanLastPage()` returns `false` because no finite last page is known. + +```ts +import { + injectTable, + tableFeatures, + rowPaginationFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPaginationFeature }) + +readonly table = injectTable(() => ({ + features, + columns, + data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +})) +``` + +> [!NOTE] +> Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +#### Using TanStack Query + +TanStack Query can own the request lifecycle while TanStack Table owns the controlled pagination state. See the complete [With TanStack Query example](../examples/with-tanstack-query), which includes both patterns below. + +##### Page-Index Pagination with `injectQuery` + +Include the pagination state in the query key, return the requested rows plus a total `rowCount`, and pass both to the table: + +```ts +readonly dataQuery = injectQuery(() => ({ + queryKey: [ + 'people', + 'offset', + this.pagination(), + this.sorting(), + this.globalFilter(), + ], + queryFn: () => + fetchPeople({ + pagination: this.pagination(), + sorting: this.sorting(), + globalFilter: this.globalFilter(), + }), + placeholderData: keepPreviousData, +})) + +readonly table = injectTable(() => ({ + features, + columns, + data: this.dataQuery.data()?.rows ?? [], + rowCount: this.dataQuery.data()?.rowCount, + state: { pagination: this.pagination() }, + onPaginationChange: updater => + isFunction(updater) + ? this.pagination.update(updater) + : this.pagination.set(updater), + manualPagination: true, +})) +``` + +This supports page counts, page-number navigation, and `lastPage()` because the total row count is known. + +##### Cursor-Based Pagination with `injectInfiniteQuery` + +For a cursor API, return the current rows, a `nextCursor`, and `hasNextPage`. The cursor can be the last row ID when IDs are unique and the server's ordering is stable: + +```ts +readonly dataQuery = injectInfiniteQuery(() => ({ + queryKey: [ + 'people', + 'cursor', + this.pagination().pageSize, + this.sorting(), + this.globalFilter(), + ], + queryFn: ({ pageParam }) => + fetchPeople({ + cursor: pageParam, + pageSize: this.pagination().pageSize, + sorting: this.sorting(), + globalFilter: this.globalFilter(), + }), + initialPageParam: null, + getNextPageParam: lastPage => lastPage.nextCursor, +})) + +readonly currentPage = computed( + () => this.dataQuery.data()?.pages[this.pagination().pageIndex], +) +readonly canNextPage = computed( + () => + Boolean( + this.dataQuery.data()?.pages[this.pagination().pageIndex + 1], + ) || Boolean(this.currentPage()?.hasNextPage), +) + +readonly table = injectTable(() => ({ + features, + columns, + data: this.currentPage()?.rows ?? [], + pageCount: -1, + state: { pagination: this.pagination() }, + manualPagination: true, +})) + +async goToNextPage() { + const nextPageIndex = this.pagination().pageIndex + 1 + + if (!this.dataQuery.data()?.pages[nextPageIndex]) { + const result = await this.dataQuery.fetchNextPage() + if (!result.data?.pages[nextPageIndex]) return + } + + this.table.nextPage() +} +``` + +Use `canNextPage()` for the Next button because `getCanNextPage()` cannot know when an unknown page count has reached the end. Cached pages support backward navigation. Since no finite last page is known, `getCanLastPage()` returns `false`. Reset `pageIndex` to `0` whenever sorting, filtering, or page size changes. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +For reactive reads in your templates, use `table.atoms.pagination.get()`. In Angular, table atom reads are signal reads, so reading the atom in a template expression, `computed(...)`, or `effect(...)` automatically tracks updates. + +If you need access to the `pagination` state outside of the table (a server-side query key is the most common case), you can own the slice yourself. The recommended way in v9 is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the pagination value can be used in a query key without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import { + injectTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/angular-table' +import type { PaginationState } from '@tanstack/angular-table' + +const features = tableFeatures({ rowPaginationFeature }) + +export class App { + readonly paginationAtom = createAtom({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size + }) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + atoms: { + pagination: this.paginationAtom, // table pagination APIs now update paginationAtom + }, + })) + + // read this.paginationAtom.get() wherever you need the value (e.g. for a query key) +} +``` + +Alternatively, the v8-style `state.pagination` plus `onPaginationChange` pattern is still supported. In Angular this means owning the slice with an Angular signal, as shown in the [Basic External State example](../examples/basic-external-state). It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly pagination = signal({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + onPaginationChange: (updater) => + typeof updater === 'function' + ? this.pagination.update(updater) + : this.pagination.set(updater), + state: { + pagination: this.pagination(), + }, +})) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +})) +``` + +> [!NOTE] +> Do NOT provide the `pagination` slice in more than one of the `atoms`, `state`, and `initialState` options. Controlled values (`atoms` or `state`) will overwrite `initialState`. Only use one of them. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` whenever the client-side row models recompute, such as when the `data` is updated, filters change, sorting changes, or grouping changes. This behavior is automatically disabled when `manualPagination` is `true`, but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. There is also a global `autoResetAll` table option that disables (or enables) every auto-reset behavior at once. + +> [!NOTE] +> Automatic resets run only when an included client-side row model that triggers them recomputes. If a manual server-side table omits the filtered, sorted, grouped, or other relevant row model, changing that controlled state does not trigger a page-index reset, even when `autoResetPageIndex` or `autoResetAll` is `true`. Reset `pageIndex` yourself in the corresponding change handler. + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + autoResetPageIndex: false, // turn off auto reset of pageIndex + // autoResetAll: false, // or turn off all auto resets at once +})) +``` + +A common reason to set `autoResetPageIndex: false` is editing data while viewing the table (for example, inline cell editing). Every edit updates `data`, which recomputes the row models and would otherwise snap the user back to the first page. Setting the option to a static `false` keeps the current page when the row model recomputes. If you also use the expanding feature, pair it with `autoResetExpanded: false` so expanded rows do not collapse on edits. + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `getCanLastPage`: Useful for disabling the "last page" button when no finite last page is known. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> [!NOTE] +> These pagination APIs are available when using `rowPaginationFeature`. + +```html + + + + + +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/angular/guide/row-pinning.md b/docs/framework/angular/guide/row-pinning.md new file mode 100644 index 0000000000..5ca96e5551 --- /dev/null +++ b/docs/framework/angular/guide/row-pinning.md @@ -0,0 +1,261 @@ +--- +title: Row Pinning (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Row Pinning](../examples/row-pinning) + +### Row Pinning Setup + +Here's how you set up your table to use row pinning features. Adding the row pinning feature enables the related APIs. + +```ts +import { signal } from '@angular/core' +import { + injectTable, + tableFeatures, + rowPinningFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPinningFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Row Pinning (Angular) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory. + +```ts +import { + rowPinningFeature, + tableFeatures, + injectTable, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPinningFeature }) + +readonly table = injectTable(() => ({ + features, + columns, + data, +})) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```ts +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```ts +readonly table = injectTable(() => ({ + features, + columns, + data, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +})) +``` + +If you need to manage row pinning outside of the table instance, the recommended v9 approach is an external atom (created with `createAtom` from `@tanstack/angular-store`) passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without re-running the `injectTable` options initializer on every change. + +```ts +import { createAtom } from '@tanstack/angular-store' +import type { RowPinningState } from '@tanstack/angular-table' + +export class App { + readonly rowPinningAtom = createAtom({ + top: [], + bottom: [], + }) + + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + atoms: { + rowPinning: this.rowPinningAtom, + }, + })) + + // read this.rowPinningAtom.get() wherever you need the value +} +``` + +Alternatively, the v8-style `state.rowPinning` plus `onRowPinningChange` pattern is still supported. In Angular this means owning the slice with an Angular signal. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +readonly rowPinning = signal({ + top: [], + bottom: [], +}) + +readonly table = injectTable(() => ({ + features, + columns, + data, + state: { + rowPinning: this.rowPinning(), + }, + onRowPinningChange: (updater) => + typeof updater === 'function' + ? this.rowPinning.update(updater) + : this.rowPinning.set(updater), +})) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```ts +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +}) + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```ts +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls: + +```html +@if (row.getCanPin()) { +
+ + + +
+} +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```ts +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly: + +```html +
+ {{ renderCell }} +
+ + {{ renderCell }} + +
+ + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { + + {{ headerCell }} + + } +
+ + {{ renderCell }} + +
+``` + +A few things to note: + +- `tableFeatures({})` declares which optional features the table uses. Registering only what you need keeps bundles small and gives TypeScript accurate types for the table instance. +- `injectTable` must be called in an injection context. Its initializer re-runs when Angular signals read inside it change (like `this.data()` here), and the adapter syncs the table options. +- The `FlexRender` directives (`*flexRenderHeader`, `*flexRenderCell`, `*flexRenderFooter`) render the `header`, `cell`, and `footer` definitions from your columns, whether they are plain values, templates, or components. See the [FlexRender Guide](./guide/flex-render) for `flexRenderComponent` and render context helpers. +- The `key` option is optional unless you use the [TanStack Table Devtools](../../devtools). The devtools identify tables by `key`, and you register a table with `injectTanStackTableDevtools` from `@tanstack/angular-table-devtools`. + +See the full [Basic injectTable example](./examples/basic-inject-table) for a runnable version with more columns and a footer. + +## Add a Feature: Sorting + +Features are opt-in in v9. To make columns sortable, register `rowSortingFeature` and the sorted row model in `tableFeatures`, then wire the header click handler in the template. + +```ts +// app.ts +import { + createSortedRowModel, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + rowSortingFeature, // enables sorting APIs and state + sortedRowModel: createSortedRowModel(), // client-side sorting + sortFns, +}) + +export class App { + readonly data = signal>([...defaultData]) + + readonly table = injectTable(() => ({ + key: 'person-table', + features, + columns, + data: this.data(), + })) + + sortIndicator(sortDirection: false | 'asc' | 'desc') { + if (sortDirection === 'asc') return ' 🔼' + if (sortDirection === 'desc') return ' 🔽' + return null + } +} +``` + +```html + + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + @if (!header.isPlaceholder) { +
+ + {{ headerCell }} + + {{ sortIndicator(header.column.getIsSorted()) }} +
+ } + + } + + } + +``` + +Clicking a header now toggles between ascending, descending, and unsorted. Every other feature follows this same pattern: register the feature and its row model factory (when it has one) inside `tableFeatures`, then use the APIs it adds to the table, columns, and rows. See the [Sorting Guide](./guide/sorting.md) and the [Sorting example](./examples/sorting) for custom sort functions, multi-sorting, and per-column options. + +## Where to Go Next + +**Table state.** In v9, table state is backed by TanStack Store atoms, and table atoms are bridged to Angular signals for you. You usually do not need to manage state yourself. Set `initialState` for starting values and call feature APIs like `table.setSorting(...)` or `table.nextPage()`. When your app should own a state slice, or you want fine-grained subscriptions, read the [Table State Guide](./guide/table-state.md). It is the foundational guide for everything else. + +**Feature guides.** Each feature has its own guide, such as [Column Filtering](./guide/column-filtering.md), [Pagination](./guide/pagination.md), [Row Selection](./guide/row-selection.md), and [Column Visibility](./guide/column-visibility.md). + +**Composable tables.** When multiple tables in your app share features, row models, and component conventions, define them once with `createTableHook`: + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { injectAppTable, createAppColumnHelper } = createTableHook({ features }) +``` + +See the [Composable Tables Guide](./guide/composable-tables.md) for the full pattern, including pre-bound cell and header components. + +**Examples.** Browse the runnable [Angular examples](./examples/basic-inject-table), from basic tables to feature demos, to see intended usage end to end. diff --git a/docs/framework/angular/reference/classes/FlexRenderCell.md b/docs/framework/angular/reference/classes/FlexRenderCell.md new file mode 100644 index 0000000000..7f5258f939 --- /dev/null +++ b/docs/framework/angular/reference/classes/FlexRenderCell.md @@ -0,0 +1,101 @@ +--- +id: FlexRenderCell +title: FlexRenderCell +--- + +# Class: FlexRenderCell\ + +Defined in: [packages/angular-table/src/helpers/flexRenderCell.ts:62](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/flexRenderCell.ts#L62) + +Simplified directive wrapper of `*flexRender`. + +Use this utility component to render headers, cells, or footers with custom markup. + +Only one prop (`cell`, `header`, or `footer`) may be passed based on the used selector. + +## Examples + +```html +{{cell}} +{{header}} +{{footer}} +``` + +This replaces calling `*flexRender` directly like this: +```html +{{cell}} +{{header}} +{{footer}} +``` + +Can be imported through FlexRenderCell or [FlexRender](../variables/FlexRender.md), with +the latter preferred. + +```ts +import {FlexRender} from '@tanstack/angular-table' + +@Component({ + // ... + imports: [ + FlexRender + ] +}) +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +## Constructors + +### Constructor + +```ts +new FlexRenderCell(): FlexRenderCell; +``` + +Defined in: [packages/angular-table/src/helpers/flexRenderCell.ts:132](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/flexRenderCell.ts#L132) + +#### Returns + +`FlexRenderCell`\<`TFeatures`, `TData`, `TValue`\> + +## Properties + +### cell + +```ts +readonly cell: InputSignal | undefined>; +``` + +Defined in: [packages/angular-table/src/helpers/flexRenderCell.ts:67](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/flexRenderCell.ts#L67) + +*** + +### footer + +```ts +readonly footer: InputSignal | undefined>; +``` + +Defined in: [packages/angular-table/src/helpers/flexRenderCell.ts:75](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/flexRenderCell.ts#L75) + +*** + +### header + +```ts +readonly header: InputSignal | undefined>; +``` + +Defined in: [packages/angular-table/src/helpers/flexRenderCell.ts:71](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/flexRenderCell.ts#L71) diff --git a/docs/framework/angular/reference/classes/FlexRenderComponentInstance.md b/docs/framework/angular/reference/classes/FlexRenderComponentInstance.md new file mode 100644 index 0000000000..52c97a24cb --- /dev/null +++ b/docs/framework/angular/reference/classes/FlexRenderComponentInstance.md @@ -0,0 +1,230 @@ +--- +id: FlexRenderComponentInstance +title: FlexRenderComponentInstance +--- + +# Class: FlexRenderComponentInstance\ + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:259](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L259) + +Wrapper class for a component that will be used as content for [FlexRenderDirective](FlexRenderDirective.md) + +Prefer [flexRenderComponent](../functions/flexRenderComponent.md) helper for better type-safety + +## Type Parameters + +### TComponent + +`TComponent` = `any` + +## Implements + +- [`FlexRenderComponent`](../interfaces/FlexRenderComponent.md)\<`TComponent`\> + +## Constructors + +### Constructor + +```ts +new FlexRenderComponentInstance( + component, + inputs?, + injector?, + outputs?, + directives?, +bindings?): FlexRenderComponentInstance; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:266](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L266) + +#### Parameters + +##### component + +`Type`\<`TComponent`\> + +##### inputs? + +`Inputs`\<`TComponent`\> + +##### injector? + +`Injector` + +##### outputs? + +`Outputs`\<`TComponent`\> + +##### directives? + +(`Type`\<`unknown`\> \| `DirectiveWithBindings`\<`unknown`\>)[] + +##### bindings? + +`Binding`[] + +#### Returns + +`FlexRenderComponentInstance`\<`TComponent`\> + +## Properties + +### allowedInputNames + +```ts +readonly allowedInputNames: string[] = []; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:263](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L263) + +List of allowed input names. + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`allowedInputNames`](../interfaces/FlexRenderComponent.md#allowedinputnames) + +*** + +### allowedOutputNames + +```ts +readonly allowedOutputNames: string[] = []; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:264](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L264) + +List of allowed output names. + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`allowedOutputNames`](../interfaces/FlexRenderComponent.md#allowedoutputnames) + +*** + +### bindings? + +```ts +readonly optional bindings: Binding[]; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:272](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L272) + +Bindings to apply to the root component + +#### See + +FlexRenderOptions#bindings + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`bindings`](../interfaces/FlexRenderComponent.md#bindings) + +*** + +### component + +```ts +readonly component: Type; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:267](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L267) + +The component type + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`component`](../interfaces/FlexRenderComponent.md#component) + +*** + +### directives? + +```ts +readonly optional directives: (Type | DirectiveWithBindings)[]; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:271](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L271) + +Directives that should be applied to the component. + +#### See + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`directives`](../interfaces/FlexRenderComponent.md#directives) + +*** + +### injector? + +```ts +readonly optional injector: Injector; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:269](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L269) + +Optional Injector that will be used when rendering the component. + +#### See + +FlexRenderOptions#injector + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`injector`](../interfaces/FlexRenderComponent.md#injector) + +*** + +### inputs? + +```ts +readonly optional inputs: Inputs; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:268](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L268) + +Component instance inputs. Set via [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput)) + +#### See + +FlexRenderOptions#inputs + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`inputs`](../interfaces/FlexRenderComponent.md#inputs) + +*** + +### mirror + +```ts +readonly mirror: ComponentMirror; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:262](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L262) + +Reflected metadata about the component. + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`mirror`](../interfaces/FlexRenderComponent.md#mirror) + +*** + +### outputs? + +```ts +readonly optional outputs: Outputs; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:270](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L270) + +Component instance outputs. Subscribed via OutputEmitterRef#subscribe + +#### See + +FlexRenderOptions#outputs + +#### Implementation of + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md).[`outputs`](../interfaces/FlexRenderComponent.md#outputs) diff --git a/docs/framework/angular/reference/classes/FlexRenderDirective.md b/docs/framework/angular/reference/classes/FlexRenderDirective.md new file mode 100644 index 0000000000..9a587f4b86 --- /dev/null +++ b/docs/framework/angular/reference/classes/FlexRenderDirective.md @@ -0,0 +1,121 @@ +--- +id: FlexRenderDirective +title: FlexRenderDirective +--- + +# Class: FlexRenderDirective\ + +Defined in: [packages/angular-table/src/flexRender.ts:84](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flexRender.ts#L84) + +Use this utility directive to render headers, cells, or footers with custom markup. + +Note: If you are rendering cell, header, or footer without custom context or other props, +you can use the [FlexRenderCell](FlexRenderCell.md) directive as shorthand instead . + +## Example + +```ts +import {FlexRender} from '@tanstack/angular-table'; + +@Component({ + imports: [FlexRender], + template: ` + + {{cell}} + + + + {{header}} + + + + {{footer}} + + `, +}) +class App { +} +``` + +Can be imported through FlexRenderDirective or [FlexRender](../variables/FlexRender.md), +with the latter preferred. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TRowData + +`TRowData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### TProps + +`TProps` *extends* + \| `NonNullable`\<`unknown`\> + \| `CellContext`\<`TFeatures`, `TRowData`, `TValue`\> + \| `HeaderContext`\<`TFeatures`, `TRowData`, `TValue`\> + +## Constructors + +### Constructor + +```ts +new FlexRenderDirective(): FlexRenderDirective; +``` + +Defined in: [packages/angular-table/src/flexRender.ts:109](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flexRender.ts#L109) + +#### Returns + +`FlexRenderDirective`\<`TFeatures`, `TRowData`, `TValue`, `TProps`\> + +## Properties + +### content + +```ts +readonly content: InputSignal>; +``` + +Defined in: [packages/angular-table/src/flexRender.ts:93](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flexRender.ts#L93) + +*** + +### injector + +```ts +readonly injector: InputSignal; +``` + +Defined in: [packages/angular-table/src/flexRender.ts:102](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flexRender.ts#L102) + +*** + +### props + +```ts +readonly props: InputSignal; +``` + +Defined in: [packages/angular-table/src/flexRender.ts:98](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flexRender.ts#L98) diff --git a/docs/framework/angular/reference/classes/TanStackTable.md b/docs/framework/angular/reference/classes/TanStackTable.md new file mode 100644 index 0000000000..60c92e083d --- /dev/null +++ b/docs/framework/angular/reference/classes/TanStackTable.md @@ -0,0 +1,78 @@ +--- +id: TanStackTable +title: TanStackTable +--- + +# Class: TanStackTable\ + +Defined in: [packages/angular-table/src/helpers/table.ts:59](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/table.ts#L59) + +Provides a TanStack Table instance (`AngularTable`) in Angular DI. + +The table can be injected by: +- any descendant of an element using `[tanStackTable]="..."` +- any component instantiated by `*flexRender` when the render props contains `table` + +## Example + +```html +
+ +
+``` + +```ts +@Component({ + selector: 'app-pagination', + template: ` + + + `, +}) +export class PaginationComponent { + readonly table = injectTableContext() + + prev() { + this.table().previousPage() + } + next() { + this.table().nextPage() + } +} +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +## Constructors + +### Constructor + +```ts +new TanStackTable(): TanStackTable; +``` + +#### Returns + +`TanStackTable`\<`TFeatures`, `TData`\> + +## Properties + +### table + +```ts +readonly table: InputSignal>; +``` + +Defined in: [packages/angular-table/src/helpers/table.ts:68](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/table.ts#L68) + +The current TanStack Table instance. + +Provided as a required signal input so DI consumers always read the latest value. diff --git a/docs/framework/angular/reference/classes/TanStackTableCell.md b/docs/framework/angular/reference/classes/TanStackTableCell.md new file mode 100644 index 0000000000..918dca2ff1 --- /dev/null +++ b/docs/framework/angular/reference/classes/TanStackTableCell.md @@ -0,0 +1,92 @@ +--- +id: TanStackTableCell +title: TanStackTableCell +--- + +# Class: TanStackTableCell\ + +Defined in: [packages/angular-table/src/helpers/cell.ts:76](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/cell.ts#L76) + +Provides a TanStack Table `Cell` instance in Angular DI. + +The cell can be injected by: +- any descendant of an element using `[tanStackTableCell]="..."` +- any component instantiated by `*flexRender` when the render props contains `cell` + +## Examples + +Inject from the nearest `[tanStackTableCell]`: +```html + + + +``` + +```ts +@Component({ + selector: 'app-cell-actions', + template: `{{ cell().id }}`, +}) +export class CellActionsComponent { + readonly cell = injectTableCellContext() +} +``` + +Inject inside a component rendered via `flexRender`: +```ts +@Component({ + selector: 'app-price-cell', + template: `{{ cell().getValue() }}`, +}) +export class PriceCellComponent { + readonly cell = injectTableCellContext() +} +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +## Implements + +- [`TanStackTableCellContext`](../interfaces/TanStackTableCellContext.md)\<`TFeatures`, `TData`, `TValue`\> + +## Constructors + +### Constructor + +```ts +new TanStackTableCell(): TanStackTableCell; +``` + +#### Returns + +`TanStackTableCell`\<`TFeatures`, `TData`, `TValue`\> + +## Properties + +### cell + +```ts +readonly cell: InputSignal>; +``` + +Defined in: [packages/angular-table/src/helpers/cell.ts:86](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/cell.ts#L86) + +The current TanStack Table cell. + +Provided as a required signal input so DI consumers always read the latest value. + +#### Implementation of + +[`TanStackTableCellContext`](../interfaces/TanStackTableCellContext.md).[`cell`](../interfaces/TanStackTableCellContext.md#cell) diff --git a/docs/framework/angular/reference/classes/TanStackTableHeader.md b/docs/framework/angular/reference/classes/TanStackTableHeader.md new file mode 100644 index 0000000000..e3eb0eee33 --- /dev/null +++ b/docs/framework/angular/reference/classes/TanStackTableHeader.md @@ -0,0 +1,88 @@ +--- +id: TanStackTableHeader +title: TanStackTableHeader +--- + +# Class: TanStackTableHeader\ + +Defined in: [packages/angular-table/src/helpers/header.ts:71](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/header.ts#L71) + +Provides a TanStack Table `Header` instance in Angular DI. + +The header can be injected by: +- any descendant of an element using `[tanStackTableHeader]="..."` +- any component instantiated by `*flexRender` when the render props contains `header` + +## Example + +```html + + + +``` + +```ts +@Component({ + selector: 'app-sort-indicator', + template: ` + + `, +}) +export class SortIndicatorComponent { + readonly header = injectTableHeaderContext() + + toggle() { + this.header().column.toggleSorting() + } +} +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +## Implements + +- [`TanStackTableHeaderContext`](../interfaces/TanStackTableHeaderContext.md)\<`TFeatures`, `TData`, `TValue`\> + +## Constructors + +### Constructor + +```ts +new TanStackTableHeader(): TanStackTableHeader; +``` + +#### Returns + +`TanStackTableHeader`\<`TFeatures`, `TData`, `TValue`\> + +## Properties + +### header + +```ts +readonly header: InputSignal>; +``` + +Defined in: [packages/angular-table/src/helpers/header.ts:81](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/header.ts#L81) + +The current TanStack Table header. + +Provided as a required signal input so DI consumers always read the latest value. + +#### Implementation of + +[`TanStackTableHeaderContext`](../interfaces/TanStackTableHeaderContext.md).[`header`](../interfaces/TanStackTableHeaderContext.md#header) diff --git a/docs/framework/angular/reference/functions/createTableHook.md b/docs/framework/angular/reference/functions/createTableHook.md new file mode 100644 index 0000000000..0d00861b5a --- /dev/null +++ b/docs/framework/angular/reference/functions/createTableHook.md @@ -0,0 +1,58 @@ +--- +id: createTableHook +title: createTableHook +--- + +# Function: createTableHook() + +```ts +function createTableHook(__namedParameters): CreateTableHookResult; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:362](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L362) + +Creates app-scoped Angular table helpers with features, row models, and +renderable component maps pre-bound. + +Use this when an app or design system wants typed `injectAppTable`, +pre-bound column helpers, and typed table/cell/header context injection +helpers without repeating the same feature and component generics. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TTableComponents + +`TTableComponents` *extends* `Record`\<`string`, [`RenderableComponent`](../type-aliases/RenderableComponent.md)\> + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](../type-aliases/RenderableComponent.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](../type-aliases/RenderableComponent.md)\> + +## Parameters + +### \_\_namedParameters + +[`CreateTableContextOptions`](../type-aliases/CreateTableContextOptions.md)\<`TFeatures`, `TTableComponents`, `TCellComponents`, `THeaderComponents`\> + +## Returns + +[`CreateTableHookResult`](../type-aliases/CreateTableHookResult.md)\<`TFeatures`, `TTableComponents`, `TCellComponents`, `THeaderComponents`\> + +## Example + +```ts +const { injectAppTable, createAppColumnHelper } = createTableHook({ + features, + tableComponents: {}, + cellComponents: {}, + headerComponents: {}, +}) +``` diff --git a/docs/framework/angular/reference/functions/flexRenderComponent.md b/docs/framework/angular/reference/functions/flexRenderComponent.md new file mode 100644 index 0000000000..3daa41c307 --- /dev/null +++ b/docs/framework/angular/reference/functions/flexRenderComponent.md @@ -0,0 +1,59 @@ +--- +id: flexRenderComponent +title: flexRenderComponent +--- + +# Function: flexRenderComponent() + +```ts +function flexRenderComponent(component, options?): FlexRenderComponent; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:150](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L150) + +Helper function to create a [FlexRenderComponent](../interfaces/FlexRenderComponent.md) instance, with better type-safety. + +## Type Parameters + +### TComponent + +`TComponent` = `any` + +## Parameters + +### component + +`Type`\<`TComponent`\> + +### options? + +`FlexRenderOptions`\<`Inputs`\<`TComponent`\>, `Outputs`\<`TComponent`\>\> + +## Returns + +[`FlexRenderComponent`](../interfaces/FlexRenderComponent.md)\<`TComponent`\> + +## Example + +```ts +import {flexRenderComponent} from '@tanstack/angular-table' +import {inputBinding, outputBinding} from '@angular/core'; + +const columns = [ + { + cell: ({ row }) => { + return flexRenderComponent(MyComponent, { + inputs: { value: mySignalValue() }, + outputs: { valueChange: (val) => {} } + // or using angular native createComponent#binding api + bindings: [ + inputBinding('value', mySignalValue), + outputBinding('valueChange', value => { + console.log("my value changed to", value) + }) + ] + }) + }, + }, +] +``` diff --git a/docs/framework/angular/reference/functions/injectFlexRenderContext.md b/docs/framework/angular/reference/functions/injectFlexRenderContext.md new file mode 100644 index 0000000000..457600b7e7 --- /dev/null +++ b/docs/framework/angular/reference/functions/injectFlexRenderContext.md @@ -0,0 +1,26 @@ +--- +id: injectFlexRenderContext +title: injectFlexRenderContext +--- + +# Function: injectFlexRenderContext() + +```ts +function injectFlexRenderContext(): T; +``` + +Defined in: [packages/angular-table/src/flex-render/context.ts:12](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/context.ts#L12) + +Inject the flex render context props. + +Can be used in components rendered via FlexRender directives. + +## Type Parameters + +### T + +`T` *extends* `object` + +## Returns + +`T` diff --git a/docs/framework/angular/reference/functions/injectTable.md b/docs/framework/angular/reference/functions/injectTable.md new file mode 100644 index 0000000000..9ac0109b36 --- /dev/null +++ b/docs/framework/angular/reference/functions/injectTable.md @@ -0,0 +1,88 @@ +--- +id: injectTable +title: injectTable +--- + +# Function: injectTable() + +```ts +function injectTable(options): AngularTable; +``` + +Defined in: [packages/angular-table/src/injectTable.ts:90](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L90) + +Creates and returns an Angular-reactive table instance. + +The initializer is intentionally re-evaluated whenever any signal read inside it changes. +This is how the adapter keeps the table in sync with Angular's reactivity model. + +Because of that behavior, keep expensive/static values (for example `columns`, feature setup, row models) +as stable references outside the initializer, and only read reactive state (`data()`, pagination/filter/sorting signals, etc.) +inside it. + +The returned table is also signal-reactive: table state and table APIs are wired for Angular signals, so you can safely consume table methods inside `computed(...)` and `effect(...)`. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +## Parameters + +### options + +() => `TableOptions`\<`TFeatures`, `TData`\> + +## Returns + +[`AngularTable`](../type-aliases/AngularTable.md)\<`TFeatures`, `TData`\> + +An Angular-reactive TanStack Table instance. + +## Example + +1. Register the table features you need +```ts +// Register only the features you need +import {tableFeatures, rowPaginationFeature} from '@tanstack/angular-table'; +const features = tableFeatures({ + rowPaginationFeature, + // ...all other features you need +}) + +// Use all table core features +import {stockFeatures} from '@tanstack/angular-table'; +const features = tableFeatures(stockFeatures); +``` +2. Prepare the table columns +```ts +import {ColumnDef} from '@tanstack/angular-table'; + +type MyData = {} + +const columns: ColumnDef[] = [ + // ...column definitions +] + +// or using createColumnHelper +import {createColumnHelper} from '@tanstack/angular-table'; +const columnHelper = createColumnHelper(); +const columns = columnHelper.columns([ + columnHelper.accessor(...), + // ...other columns +]) +``` +3. Create the table instance with `injectTable` +```ts +const table = injectTable(() => { + // ...table options, + features, + columns: columns, + data: myDataSignal(), +}) +``` diff --git a/docs/framework/angular/reference/functions/injectTableCellContext.md b/docs/framework/angular/reference/functions/injectTableCellContext.md new file mode 100644 index 0000000000..cde9de15dc --- /dev/null +++ b/docs/framework/angular/reference/functions/injectTableCellContext.md @@ -0,0 +1,36 @@ +--- +id: injectTableCellContext +title: injectTableCellContext +--- + +# Function: injectTableCellContext() + +```ts +function injectTableCellContext(): Signal>; +``` + +Defined in: [packages/angular-table/src/helpers/cell.ts:98](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/cell.ts#L98) + +Injects the current TanStack Table cell signal. + +Available when: +- there is a nearest `[tanStackTableCell]` directive in the DI tree, or +- the caller is rendered via `*flexRender` with render props containing `cell` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `unknown` + +## Returns + +`Signal`\<`Cell`\<`TFeatures`, `TData`, `TValue`\>\> diff --git a/docs/framework/angular/reference/functions/injectTableContext.md b/docs/framework/angular/reference/functions/injectTableContext.md new file mode 100644 index 0000000000..d19bcaa1f2 --- /dev/null +++ b/docs/framework/angular/reference/functions/injectTableContext.md @@ -0,0 +1,32 @@ +--- +id: injectTableContext +title: injectTableContext +--- + +# Function: injectTableContext() + +```ts +function injectTableContext(): Signal>; +``` + +Defined in: [packages/angular-table/src/helpers/table.ts:80](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/table.ts#L80) + +Injects the current TanStack Table instance signal. + +Available when: +- there is a nearest `[tanStackTable]` directive in the DI tree, or +- the caller is rendered via `*flexRender` with render props containing `table` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +## Returns + +`Signal`\<[`AngularTable`](../type-aliases/AngularTable.md)\<`TFeatures`, `TData`\>\> diff --git a/docs/framework/angular/reference/functions/injectTableHeaderContext.md b/docs/framework/angular/reference/functions/injectTableHeaderContext.md new file mode 100644 index 0000000000..2413752003 --- /dev/null +++ b/docs/framework/angular/reference/functions/injectTableHeaderContext.md @@ -0,0 +1,36 @@ +--- +id: injectTableHeaderContext +title: injectTableHeaderContext +--- + +# Function: injectTableHeaderContext() + +```ts +function injectTableHeaderContext(): Signal>; +``` + +Defined in: [packages/angular-table/src/helpers/header.ts:93](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/header.ts#L93) + +Injects the current TanStack Table header signal. + +Available when: +- there is a nearest `[tanStackTableHeader]` directive in the DI tree, or +- the caller is rendered via `*flexRender` with render props containing `header` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `unknown` + +## Returns + +`Signal`\<`Header`\<`TFeatures`, `TData`, `TValue`\>\> diff --git a/docs/framework/angular/reference/functions/shallow.md b/docs/framework/angular/reference/functions/shallow.md new file mode 100644 index 0000000000..57231700ef --- /dev/null +++ b/docs/framework/angular/reference/functions/shallow.md @@ -0,0 +1,32 @@ +--- +id: shallow +title: shallow +--- + +# Function: shallow() + +```ts +function shallow(objA, objB): boolean; +``` + +Defined in: node\_modules/.pnpm/@tanstack+store@0.11.0/node\_modules/@tanstack/store/dist/shallow.d.ts:2 + +## Type Parameters + +### T + +`T` + +## Parameters + +### objA + +`T` + +### objB + +`T` + +## Returns + +`boolean` diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md new file mode 100644 index 0000000000..0166ae210a --- /dev/null +++ b/docs/framework/angular/reference/index.md @@ -0,0 +1,58 @@ +--- +id: "@tanstack/angular-table" +title: "@tanstack/angular-table" +--- + +# @tanstack/angular-table + +## Classes + +- [FlexRenderCell](classes/FlexRenderCell.md) +- [FlexRenderComponentInstance](classes/FlexRenderComponentInstance.md) +- [FlexRenderDirective](classes/FlexRenderDirective.md) +- [TanStackTable](classes/TanStackTable.md) +- [TanStackTableCell](classes/TanStackTableCell.md) +- [TanStackTableHeader](classes/TanStackTableHeader.md) + +## Interfaces + +- [FlexRenderComponent](interfaces/FlexRenderComponent.md) +- [TanStackTableCellContext](interfaces/TanStackTableCellContext.md) +- [TanStackTableHeaderContext](interfaces/TanStackTableHeaderContext.md) + +## Type Aliases + +- [AngularTable](type-aliases/AngularTable.md) +- [AppAngularTable](type-aliases/AppAngularTable.md) +- [AppCellContext](type-aliases/AppCellContext.md) +- [AppColumnDefBase](type-aliases/AppColumnDefBase.md) +- [AppColumnDefTemplate](type-aliases/AppColumnDefTemplate.md) +- [AppColumnHelper](type-aliases/AppColumnHelper.md) +- [AppDisplayColumnDef](type-aliases/AppDisplayColumnDef.md) +- [AppGroupColumnDef](type-aliases/AppGroupColumnDef.md) +- [AppHeaderContext](type-aliases/AppHeaderContext.md) +- [CreateTableContextOptions](type-aliases/CreateTableContextOptions.md) +- [CreateTableHookResult](type-aliases/CreateTableHookResult.md) +- [FlexRenderComponentProps](type-aliases/FlexRenderComponentProps.md) +- [FlexRenderContent](type-aliases/FlexRenderContent.md) +- [FlexRenderInputContent](type-aliases/FlexRenderInputContent.md) +- [RenderableComponent](type-aliases/RenderableComponent.md) +- [SubscribeSource](type-aliases/SubscribeSource.md) + +## Variables + +- [FlexRender](variables/FlexRender.md) +- [TanStackTableCellToken](variables/TanStackTableCellToken.md) +- [TanStackTableHeaderToken](variables/TanStackTableHeaderToken.md) +- [TanStackTableToken](variables/TanStackTableToken.md) + +## Functions + +- [createTableHook](functions/createTableHook.md) +- [flexRenderComponent](functions/flexRenderComponent.md) +- [injectFlexRenderContext](functions/injectFlexRenderContext.md) +- [injectTable](functions/injectTable.md) +- [injectTableCellContext](functions/injectTableCellContext.md) +- [injectTableContext](functions/injectTableContext.md) +- [injectTableHeaderContext](functions/injectTableHeaderContext.md) +- [shallow](functions/shallow.md) diff --git a/docs/framework/angular/reference/interfaces/FlexRenderComponent.md b/docs/framework/angular/reference/interfaces/FlexRenderComponent.md new file mode 100644 index 0000000000..640236f8e8 --- /dev/null +++ b/docs/framework/angular/reference/interfaces/FlexRenderComponent.md @@ -0,0 +1,179 @@ +--- +id: FlexRenderComponent +title: FlexRenderComponent +--- + +# Interface: FlexRenderComponent\ + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:205](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L205) + +Wrapper interface for a component that will be used as content for [FlexRenderDirective](../classes/FlexRenderDirective.md). +Can be created using [flexRenderComponent](../functions/flexRenderComponent.md) helper. + +## Example + +```ts +import {flexRenderComponent} from '@tanstack/angular-table' + +// Usage in cell/header/footer definition +const columns = [ + { + cell: ({ row }) => { + return flexRenderComponent(MyComponent, { + inputs: { value: mySignalValue() }, + outputs: { valueChange: (val) => {} } + // or using angular createComponent#bindings api + bindings: [ + inputBinding('value', mySignalValue), + outputBinding('valueChange', value => { + console.log("my value changed to", value) + }) + ] + }) + }, + }, +] + +import {input, output} from '@angular/core'; + +@Component({ + selector: 'my-component', +}) +class MyComponent { + readonly value = input(0); + readonly valueChange = output(); +} + +``` + +## Type Parameters + +### TComponent + +`TComponent` = `any` + +## Properties + +### allowedInputNames + +```ts +readonly allowedInputNames: string[]; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:217](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L217) + +List of allowed input names. + +*** + +### allowedOutputNames + +```ts +readonly allowedOutputNames: string[]; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:221](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L221) + +List of allowed output names. + +*** + +### bindings? + +```ts +optional bindings: Binding[]; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:245](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L245) + +Bindings to apply to the root component + +#### See + +FlexRenderOptions#bindings + +*** + +### component + +```ts +readonly component: Type; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:209](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L209) + +The component type + +*** + +### directives? + +```ts +optional directives: (Type | DirectiveWithBindings)[]; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:251](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L251) + +Directives that should be applied to the component. + +#### See + +*** + +### injector? + +```ts +readonly optional injector: Injector; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:239](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L239) + +Optional Injector that will be used when rendering the component. + +#### See + +FlexRenderOptions#injector + +*** + +### inputs? + +```ts +readonly optional inputs: Inputs; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:233](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L233) + +Component instance inputs. Set via [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput)) + +#### See + +FlexRenderOptions#inputs + +*** + +### mirror + +```ts +readonly mirror: ComponentMirror; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:213](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L213) + +Reflected metadata about the component. + +*** + +### outputs? + +```ts +readonly optional outputs: Outputs; +``` + +Defined in: [packages/angular-table/src/flex-render/flexRenderComponent.ts:227](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/flexRenderComponent.ts#L227) + +Component instance outputs. Subscribed via OutputEmitterRef#subscribe + +#### See + +FlexRenderOptions#outputs diff --git a/docs/framework/angular/reference/interfaces/TanStackTableCellContext.md b/docs/framework/angular/reference/interfaces/TanStackTableCellContext.md new file mode 100644 index 0000000000..88971085c4 --- /dev/null +++ b/docs/framework/angular/reference/interfaces/TanStackTableCellContext.md @@ -0,0 +1,39 @@ +--- +id: TanStackTableCellContext +title: TanStackTableCellContext +--- + +# Interface: TanStackTableCellContext\ + +Defined in: [packages/angular-table/src/helpers/cell.ts:11](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/cell.ts#L11) + +DI context shape for a TanStack Table cell. + +This exists to make the current `Cell` injectable by any nested component/directive +without having to pass it through inputs/props manually. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +## Properties + +### cell + +```ts +cell: Signal>; +``` + +Defined in: [packages/angular-table/src/helpers/cell.ts:17](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/cell.ts#L17) + +Signal that returns the current cell instance. diff --git a/docs/framework/angular/reference/interfaces/TanStackTableHeaderContext.md b/docs/framework/angular/reference/interfaces/TanStackTableHeaderContext.md new file mode 100644 index 0000000000..9a1a50effd --- /dev/null +++ b/docs/framework/angular/reference/interfaces/TanStackTableHeaderContext.md @@ -0,0 +1,39 @@ +--- +id: TanStackTableHeaderContext +title: TanStackTableHeaderContext +--- + +# Interface: TanStackTableHeaderContext\ + +Defined in: [packages/angular-table/src/helpers/header.ts:11](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/header.ts#L11) + +DI context shape for a TanStack Table header. + +This exists to make the current `Header` injectable by any nested component/directive +without passing it through inputs/props. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +## Properties + +### header + +```ts +header: Signal>; +``` + +Defined in: [packages/angular-table/src/helpers/header.ts:17](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/header.ts#L17) + +Signal that returns the current header instance. diff --git a/docs/framework/angular/reference/type-aliases/AngularTable.md b/docs/framework/angular/reference/type-aliases/AngularTable.md new file mode 100644 index 0000000000..586dc38fcb --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AngularTable.md @@ -0,0 +1,22 @@ +--- +id: AngularTable +title: AngularTable +--- + +# Type Alias: AngularTable\ + +```ts +type AngularTable = Table; +``` + +Defined in: [packages/angular-table/src/injectTable.ts:29](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L29) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` diff --git a/docs/framework/angular/reference/type-aliases/AppAngularTable.md b/docs/framework/angular/reference/type-aliases/AppAngularTable.md new file mode 100644 index 0000000000..dd4655cfc0 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AppAngularTable.md @@ -0,0 +1,104 @@ +--- +id: AppAngularTable +title: AppAngularTable +--- + +# Type Alias: AppAngularTable\ + +```ts +type AppAngularTable = AngularTable & NoInfer & object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:241](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L241) + +Extended table API returned by useAppTable with all App wrapper components + +## Type Declaration + +### appCell() + +```ts +appCell: (cell) => Cell & NoInfer; +``` + +#### Type Parameters + +##### TValue + +`TValue` + +#### Parameters + +##### cell + +`Cell`\<`TFeatures`, `TData`, `TValue`\> + +#### Returns + +`Cell`\<`TFeatures`, `TData`, `TValue`\> & `NoInfer`\<`TCellComponents`\> + +### appFooter() + +```ts +appFooter: (footer) => Header & NoInfer; +``` + +#### Type Parameters + +##### TValue + +`TValue` + +#### Parameters + +##### footer + +`Header`\<`TFeatures`, `TData`, `TValue`\> + +#### Returns + +`Header`\<`TFeatures`, `TData`, `TValue`\> & `NoInfer`\<`THeaderComponents`\> + +### appHeader() + +```ts +appHeader: (header) => Header & NoInfer; +``` + +#### Type Parameters + +##### TValue + +`TValue` + +#### Parameters + +##### header + +`Header`\<`TFeatures`, `TData`, `TValue`\> + +#### Returns + +`Header`\<`TFeatures`, `TData`, `TValue`\> & `NoInfer`\<`THeaderComponents`\> + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TTableComponents + +`TTableComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> diff --git a/docs/framework/angular/reference/type-aliases/AppCellContext.md b/docs/framework/angular/reference/type-aliases/AppCellContext.md new file mode 100644 index 0000000000..7064edd43f --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AppCellContext.md @@ -0,0 +1,105 @@ +--- +id: AppCellContext +title: AppCellContext +--- + +# Type Alias: AppCellContext\ + +```ts +type AppCellContext = object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:46](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L46) + +Enhanced CellContext with pre-bound cell components. +The `cell` property includes the registered cellComponents. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +## Properties + +### cell + +```ts +cell: Cell & TCellComponents & object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:52](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L52) + +#### Type Declaration + +##### FlexRender() + +```ts +FlexRender: () => unknown; +``` + +###### Returns + +`unknown` + +*** + +### column + +```ts +column: Column; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:54](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L54) + +*** + +### getValue + +```ts +getValue: CellContext["getValue"]; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:55](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L55) + +*** + +### renderValue + +```ts +renderValue: CellContext["renderValue"]; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:56](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L56) + +*** + +### row + +```ts +row: Row; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:57](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L57) + +*** + +### table + +```ts +table: Table; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:58](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L58) diff --git a/docs/framework/angular/reference/type-aliases/AppColumnDefBase.md b/docs/framework/angular/reference/type-aliases/AppColumnDefBase.md new file mode 100644 index 0000000000..5b3112c7d6 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AppColumnDefBase.md @@ -0,0 +1,56 @@ +--- +id: AppColumnDefBase +title: AppColumnDefBase +--- + +# Type Alias: AppColumnDefBase\ + +```ts +type AppColumnDefBase = Omit, "cell" | "header" | "footer"> & object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:90](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L90) + +Enhanced column definition base with pre-bound components in cell/header/footer contexts. + +## Type Declaration + +### cell? + +```ts +optional cell: AppColumnDefTemplate>; +``` + +### footer? + +```ts +optional footer: AppColumnDefTemplate>; +``` + +### header? + +```ts +optional header: AppColumnDefTemplate>; +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> diff --git a/docs/framework/angular/reference/type-aliases/AppColumnDefTemplate.md b/docs/framework/angular/reference/type-aliases/AppColumnDefTemplate.md new file mode 100644 index 0000000000..53e8892975 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AppColumnDefTemplate.md @@ -0,0 +1,20 @@ +--- +id: AppColumnDefTemplate +title: AppColumnDefTemplate +--- + +# Type Alias: AppColumnDefTemplate\ + +```ts +type AppColumnDefTemplate = string | (props) => any; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:84](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L84) + +Template type for column definitions that can be a string or a function. + +## Type Parameters + +### TProps + +`TProps` *extends* `object` diff --git a/docs/framework/angular/reference/type-aliases/AppColumnHelper.md b/docs/framework/angular/reference/type-aliases/AppColumnHelper.md new file mode 100644 index 0000000000..241e5d3f93 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AppColumnHelper.md @@ -0,0 +1,144 @@ +--- +id: AppColumnHelper +title: AppColumnHelper +--- + +# Type Alias: AppColumnHelper\ + +```ts +type AppColumnHelper = object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:166](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L166) + +Enhanced column helper with pre-bound components in cell/header/footer contexts. +This enables TypeScript to know about the registered components when defining columns. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +## Properties + +### accessor() + +```ts +accessor: (accessor, column) => TAccessor extends AccessorFn ? AccessorFnColumnDef : AccessorKeyColumnDef; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:176](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L176) + +Creates a data column definition with an accessor key or function. +The cell, header, and footer contexts include pre-bound components. + +#### Type Parameters + +##### TAccessor + +`TAccessor` *extends* `AccessorFn`\<`TData`\> \| `DeepKeys`\<`TData`\> + +##### TValue + +`TValue` *extends* `TAccessor` *extends* `AccessorFn`\<`TData`, infer TReturn\> ? `TReturn` : `TAccessor` *extends* `DeepKeys`\<`TData`\> ? `DeepValue`\<`TData`, `TAccessor`\> : `never` + +#### Parameters + +##### accessor + +`TAccessor` + +##### column + +`TAccessor` *extends* `AccessorFn`\<`TData`\> ? [`AppColumnDefBase`](AppColumnDefBase.md)\<`TFeatures`, `TData`, `TValue`, `TCellComponents`, `THeaderComponents`\> & `object` : [`AppColumnDefBase`](AppColumnDefBase.md)\<`TFeatures`, `TData`, `TValue`, `TCellComponents`, `THeaderComponents`\> + +#### Returns + +`TAccessor` *extends* `AccessorFn`\<`TData`\> ? `AccessorFnColumnDef`\<`TFeatures`, `TData`, `TValue`\> : `AccessorKeyColumnDef`\<`TFeatures`, `TData`, `TValue`\> + +*** + +### columns() + +```ts +columns: (columns) => ColumnDef[] & [...TColumns]; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:207](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L207) + +Wraps an array of column definitions to preserve each column's individual TValue type. + +#### Type Parameters + +##### TColumns + +`TColumns` *extends* `ReadonlyArray`\<`ColumnDef`\<`TFeatures`, `TData`, `any`\>\> + +#### Parameters + +##### columns + +\[`...TColumns`\] + +#### Returns + +`ColumnDef`\<`TFeatures`, `TData`, `any`\>[] & \[`...TColumns`\] + +*** + +### display() + +```ts +display: (column) => DisplayColumnDef; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:215](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L215) + +Creates a display column definition for non-data columns. +The cell, header, and footer contexts include pre-bound components. + +#### Parameters + +##### column + +[`AppDisplayColumnDef`](AppDisplayColumnDef.md)\<`TFeatures`, `TData`, `TCellComponents`, `THeaderComponents`\> + +#### Returns + +`DisplayColumnDef`\<`TFeatures`, `TData`, `unknown`\> + +*** + +### group() + +```ts +group: (column) => GroupColumnDef; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:228](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L228) + +Creates a group column definition with nested child columns. +The cell, header, and footer contexts include pre-bound components. + +#### Parameters + +##### column + +[`AppGroupColumnDef`](AppGroupColumnDef.md)\<`TFeatures`, `TData`, `TCellComponents`, `THeaderComponents`\> + +#### Returns + +`GroupColumnDef`\<`TFeatures`, `TData`, `unknown`\> diff --git a/docs/framework/angular/reference/type-aliases/AppDisplayColumnDef.md b/docs/framework/angular/reference/type-aliases/AppDisplayColumnDef.md new file mode 100644 index 0000000000..640129bdcf --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AppDisplayColumnDef.md @@ -0,0 +1,52 @@ +--- +id: AppDisplayColumnDef +title: AppDisplayColumnDef +--- + +# Type Alias: AppDisplayColumnDef\ + +```ts +type AppDisplayColumnDef = Omit, "cell" | "header" | "footer"> & object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:114](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L114) + +Enhanced display column definition with pre-bound components. + +## Type Declaration + +### cell? + +```ts +optional cell: AppColumnDefTemplate>; +``` + +### footer? + +```ts +optional footer: AppColumnDefTemplate>; +``` + +### header? + +```ts +optional header: AppColumnDefTemplate>; +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> diff --git a/docs/framework/angular/reference/type-aliases/AppGroupColumnDef.md b/docs/framework/angular/reference/type-aliases/AppGroupColumnDef.md new file mode 100644 index 0000000000..75d5164e58 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AppGroupColumnDef.md @@ -0,0 +1,58 @@ +--- +id: AppGroupColumnDef +title: AppGroupColumnDef +--- + +# Type Alias: AppGroupColumnDef\ + +```ts +type AppGroupColumnDef = Omit, "cell" | "header" | "footer" | "columns"> & object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:137](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L137) + +Enhanced group column definition with pre-bound components. + +## Type Declaration + +### cell? + +```ts +optional cell: AppColumnDefTemplate>; +``` + +### columns? + +```ts +optional columns: ReadonlyArray>; +``` + +### footer? + +```ts +optional footer: AppColumnDefTemplate>; +``` + +### header? + +```ts +optional header: AppColumnDefTemplate>; +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> diff --git a/docs/framework/angular/reference/type-aliases/AppHeaderContext.md b/docs/framework/angular/reference/type-aliases/AppHeaderContext.md new file mode 100644 index 0000000000..d9171c35db --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/AppHeaderContext.md @@ -0,0 +1,75 @@ +--- +id: AppHeaderContext +title: AppHeaderContext +--- + +# Type Alias: AppHeaderContext\ + +```ts +type AppHeaderContext = object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:65](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L65) + +Enhanced HeaderContext with pre-bound header components. +The `header` property includes the registered headerComponents. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +## Properties + +### column + +```ts +column: Column; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:71](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L71) + +*** + +### header + +```ts +header: Header & THeaderComponents & object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:72](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L72) + +#### Type Declaration + +##### FlexRender() + +```ts +FlexRender: () => unknown; +``` + +###### Returns + +`unknown` + +*** + +### table + +```ts +table: Table; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:74](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L74) diff --git a/docs/framework/angular/reference/type-aliases/CreateTableContextOptions.md b/docs/framework/angular/reference/type-aliases/CreateTableContextOptions.md new file mode 100644 index 0000000000..a8759da512 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/CreateTableContextOptions.md @@ -0,0 +1,83 @@ +--- +id: CreateTableContextOptions +title: CreateTableContextOptions +--- + +# Type Alias: CreateTableContextOptions\ + +```ts +type CreateTableContextOptions = Omit, "columns" | "data" | "store" | "state" | "initialState"> & object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:270](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L270) + +Options for creating a table hook with pre-bound components and default table options. +Extends all TableOptions except 'columns' | 'data' | 'store' | 'state' | 'initialState'. + +## Type Declaration + +### cellComponents? + +```ts +optional cellComponents: TCellComponents; +``` + +Cell-level components that need access to the cell instance. +These are available on the cell object passed to AppCell's children. +Use `useCellContext()` inside these components. + +#### Example + +```ts +{ TextCell, NumberCell, DateCell, CurrencyCell } +``` + +### headerComponents? + +```ts +optional headerComponents: THeaderComponents; +``` + +Header-level components that need access to the header instance. +These are available on the header object passed to AppHeader/AppFooter's children. +Use `useHeaderContext()` inside these components. + +#### Example + +```ts +{ SortIndicator, ColumnFilter, ResizeHandle } +``` + +### tableComponents? + +```ts +optional tableComponents: TTableComponents; +``` + +Table-level components that need access to the table instance. +These are available directly on the table object returned by useAppTable. +Use `useTableContext()` inside these components. + +#### Example + +```ts +{ PaginationControls, GlobalFilter, RowCount } +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TTableComponents + +`TTableComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> diff --git a/docs/framework/angular/reference/type-aliases/CreateTableHookResult.md b/docs/framework/angular/reference/type-aliases/CreateTableHookResult.md new file mode 100644 index 0000000000..b12d61c653 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/CreateTableHookResult.md @@ -0,0 +1,192 @@ +--- +id: CreateTableHookResult +title: CreateTableHookResult +--- + +# Type Alias: CreateTableHookResult\ + +```ts +type CreateTableHookResult = object; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:302](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L302) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TTableComponents + +`TTableComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`RenderableComponent`](RenderableComponent.md)\> + +## Properties + +### createAppColumnHelper() + +```ts +createAppColumnHelper: () => AppColumnHelper; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:308](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L308) + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +#### Returns + +[`AppColumnHelper`](AppColumnHelper.md)\<`TFeatures`, `TData`, `TCellComponents`, `THeaderComponents`\> + +*** + +### injectAppTable() + +```ts +injectAppTable: (tableOptions) => AppAngularTable; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:333](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L333) + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +#### Parameters + +##### tableOptions + +() => `Omit`\<`TableOptions`\<`TFeatures`, `TData`\>, `"features"`\> + +#### Returns + +[`AppAngularTable`](AppAngularTable.md)\<`TFeatures`, `TData`, `TTableComponents`, `TCellComponents`, `THeaderComponents`\> + +*** + +### injectFlexRenderCellContext() + +```ts +injectFlexRenderCellContext: () => CellContext; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:329](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L329) + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +##### TValue + +`TValue` *extends* `CellData` + +#### Returns + +`CellContext`\<`TFeatures`, `TData`, `TValue`\> + +*** + +### injectFlexRenderHeaderContext() + +```ts +injectFlexRenderHeaderContext: () => HeaderContext; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:325](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L325) + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +##### TValue + +`TValue` *extends* `CellData` + +#### Returns + +`HeaderContext`\<`TFeatures`, `TData`, `TValue`\> + +*** + +### injectTableCellContext() + +```ts +injectTableCellContext: () => Signal & TCellComponents>; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:321](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L321) + +#### Type Parameters + +##### TValue + +`TValue` *extends* `CellData` = `CellData` + +##### TRowData + +`TRowData` *extends* `RowData` = `RowData` + +#### Returns + +`Signal`\<`Cell`\<`TFeatures`, `TRowData`, `TValue`\> & `TCellComponents`\> + +*** + +### injectTableContext() + +```ts +injectTableContext: () => Signal & TTableComponents>; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:314](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L314) + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` = `RowData` + +#### Returns + +`Signal`\<[`AngularTable`](AngularTable.md)\<`TFeatures`, `TData`\> & `TTableComponents`\> + +*** + +### injectTableHeaderContext() + +```ts +injectTableHeaderContext: () => Signal & THeaderComponents>; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:317](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L317) + +#### Type Parameters + +##### TValue + +`TValue` *extends* `CellData` = `CellData` + +##### TRowData + +`TRowData` *extends* `RowData` = `RowData` + +#### Returns + +`Signal`\<`Header`\<`TFeatures`, `TRowData`, `TValue`\> & `THeaderComponents`\> diff --git a/docs/framework/angular/reference/type-aliases/FlexRenderComponentProps.md b/docs/framework/angular/reference/type-aliases/FlexRenderComponentProps.md new file mode 100644 index 0000000000..952c9e14f0 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/FlexRenderComponentProps.md @@ -0,0 +1,13 @@ +--- +id: FlexRenderComponentProps +title: FlexRenderComponentProps +--- + +# Type Alias: FlexRenderComponentProps + +```ts +type FlexRenderComponentProps = InjectionToken<{ +}>; +``` + +Defined in: [packages/angular-table/src/flex-render/context.ts:3](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/context.ts#L3) diff --git a/docs/framework/angular/reference/type-aliases/FlexRenderContent.md b/docs/framework/angular/reference/type-aliases/FlexRenderContent.md new file mode 100644 index 0000000000..85cb24ace4 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/FlexRenderContent.md @@ -0,0 +1,31 @@ +--- +id: FlexRenderContent +title: FlexRenderContent +--- + +# Type Alias: FlexRenderContent\ + +```ts +type FlexRenderContent = + | string + | number + | Type + | FlexRenderComponent + | TemplateRef<{ + $implicit: TProps; +}> + | null + | Record + | undefined; +``` + +Defined in: [packages/angular-table/src/flex-render/renderer.ts:44](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/renderer.ts#L44) + +Content supported by the `flexRender` directive when declaring +a table column header/cell. + +## Type Parameters + +### TProps + +`TProps` *extends* `NonNullable`\<`unknown`\> diff --git a/docs/framework/angular/reference/type-aliases/FlexRenderInputContent.md b/docs/framework/angular/reference/type-aliases/FlexRenderInputContent.md new file mode 100644 index 0000000000..a3d1862dcd --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/FlexRenderInputContent.md @@ -0,0 +1,25 @@ +--- +id: FlexRenderInputContent +title: FlexRenderInputContent +--- + +# Type Alias: FlexRenderInputContent\ + +```ts +type FlexRenderInputContent = + | number + | string + | (props) => FlexRenderContent + | null + | undefined; +``` + +Defined in: [packages/angular-table/src/flex-render/renderer.ts:57](https://github.com/TanStack/table/blob/main/packages/angular-table/src/flex-render/renderer.ts#L57) + +Input content supported by the `flexRender` directives. + +## Type Parameters + +### TProps + +`TProps` *extends* `NonNullable`\<`unknown`\> diff --git a/docs/framework/angular/reference/type-aliases/RenderableComponent.md b/docs/framework/angular/reference/type-aliases/RenderableComponent.md new file mode 100644 index 0000000000..b194166761 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/RenderableComponent.md @@ -0,0 +1,14 @@ +--- +id: RenderableComponent +title: RenderableComponent +--- + +# Type Alias: RenderableComponent + +```ts +type RenderableComponent = + | Type +| (props) => FlexRenderContent; +``` + +Defined in: [packages/angular-table/src/helpers/createTableHook.ts:34](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/createTableHook.ts#L34) diff --git a/docs/framework/angular/reference/type-aliases/SubscribeSource.md b/docs/framework/angular/reference/type-aliases/SubscribeSource.md new file mode 100644 index 0000000000..531cddf8d5 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/SubscribeSource.md @@ -0,0 +1,22 @@ +--- +id: SubscribeSource +title: SubscribeSource +--- + +# Type Alias: SubscribeSource\ + +```ts +type SubscribeSource = + | Atom + | ReadonlyAtom + | Store +| ReadonlyStore; +``` + +Defined in: [packages/angular-table/src/injectTable.ts:26](https://github.com/TanStack/table/blob/main/packages/angular-table/src/injectTable.ts#L26) + +## Type Parameters + +### TValue + +`TValue` diff --git a/docs/framework/angular/reference/variables/FlexRender.md b/docs/framework/angular/reference/variables/FlexRender.md new file mode 100644 index 0000000000..0d628161c2 --- /dev/null +++ b/docs/framework/angular/reference/variables/FlexRender.md @@ -0,0 +1,21 @@ +--- +id: FlexRender +title: FlexRender +--- + +# Variable: FlexRender + +```ts +const FlexRender: readonly [typeof FlexRenderDirective, typeof FlexRenderCell]; +``` + +Defined in: [packages/angular-table/src/index.ts:24](https://github.com/TanStack/table/blob/main/packages/angular-table/src/index.ts#L24) + +Constant helper to import FlexRender directives. + +You should prefer to use this constant over importing the directives separately, +as it ensures you always have the correct set of directives over library updates. + +## See + +[FlexRenderDirective](../classes/FlexRenderDirective.md) and [FlexRenderCell](../classes/FlexRenderCell.md) for more details on the directives included in this export. diff --git a/docs/framework/angular/reference/variables/TanStackTableCellToken.md b/docs/framework/angular/reference/variables/TanStackTableCellToken.md new file mode 100644 index 0000000000..dd4895fc2d --- /dev/null +++ b/docs/framework/angular/reference/variables/TanStackTableCellToken.md @@ -0,0 +1,16 @@ +--- +id: TanStackTableCellToken +title: TanStackTableCellToken +--- + +# Variable: TanStackTableCellToken + +```ts +const TanStackTableCellToken: InjectionToken>>; +``` + +Defined in: [packages/angular-table/src/helpers/cell.ts:25](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/cell.ts#L25) + +Injection token that provides access to the current cell. + +This token is provided by the [TanStackTableCell](../classes/TanStackTableCell.md) directive. diff --git a/docs/framework/angular/reference/variables/TanStackTableHeaderToken.md b/docs/framework/angular/reference/variables/TanStackTableHeaderToken.md new file mode 100644 index 0000000000..4d607a3a2c --- /dev/null +++ b/docs/framework/angular/reference/variables/TanStackTableHeaderToken.md @@ -0,0 +1,16 @@ +--- +id: TanStackTableHeaderToken +title: TanStackTableHeaderToken +--- + +# Variable: TanStackTableHeaderToken + +```ts +const TanStackTableHeaderToken: InjectionToken>>; +``` + +Defined in: [packages/angular-table/src/helpers/header.ts:25](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/header.ts#L25) + +Injection token that provides access to the current header. + +This token is provided by the [TanStackTableHeader](../classes/TanStackTableHeader.md) directive. diff --git a/docs/framework/angular/reference/variables/TanStackTableToken.md b/docs/framework/angular/reference/variables/TanStackTableToken.md new file mode 100644 index 0000000000..93a8358263 --- /dev/null +++ b/docs/framework/angular/reference/variables/TanStackTableToken.md @@ -0,0 +1,16 @@ +--- +id: TanStackTableToken +title: TanStackTableToken +--- + +# Variable: TanStackTableToken + +```ts +const TanStackTableToken: InjectionToken>>; +``` + +Defined in: [packages/angular-table/src/helpers/table.ts:11](https://github.com/TanStack/table/blob/main/packages/angular-table/src/helpers/table.ts#L11) + +Injection token that provides access to the current [AngularTable](../type-aliases/AngularTable.md) instance. + +This token is provided by the [TanStackTable](../classes/TanStackTable.md) directive. diff --git a/docs/framework/ember/guide/aggregation.md b/docs/framework/ember/guide/aggregation.md new file mode 100644 index 0000000000..7772dd4e91 --- /dev/null +++ b/docs/framework/ember/guide/aggregation.md @@ -0,0 +1,269 @@ +--- +title: Aggregation (Ember) Guide +--- + +## Examples + +- [Aggregation](../examples/aggregation) +- [Grouped Aggregation](../examples/grouped-aggregation) + +Aggregation is independent from column grouping. Register `rowAggregationFeature` +whenever columns calculate totals or aggregated values. Add +`columnGroupingFeature` separately only when the table also groups rows. + +## Aggregation Setup + +Register only the built-in functions referenced by name. Passing a definition +directly to a column does not require a registry entry. + +```ts +import { + rowAggregationFeature, + aggregationFn_count, + aggregationFn_extent, + aggregationFn_mean, + aggregationFn_sum, + tableFeatures, + useTable, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + rowAggregationFeature, + aggregationFns: { + count: aggregationFn_count, + extent: aggregationFn_extent, + mean: aggregationFn_mean, + sum: aggregationFn_sum, + }, +}) + +const table = useTable(() => ({ + features, + columns, + data, +})) +``` + +The aggregation feature does not require a grouped row model. This makes grand +totals and custom row-subset totals available in otherwise ordinary tables. + +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + +## Column Aggregations + +A column accepts one aggregation or an array. A single entry returns a scalar; +multiple entries return an object keyed by the aggregation name or descriptor +`id`. + +```ts +columnHelper.accessor('amount', { + aggregationFn: 'sum', +}) + +columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], +}) +``` + +String values remain backward-compatible. Use descriptors when a result needs +a stable custom key or options. + +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + +## Grand Totals and Row Subsets + +Call `column.getAggregationValue()` without arguments to aggregate the default +pre-grouped row model. Filtering is included; grouping, sorting, expansion, and +pagination do not change that default total. + +```ts +footer: ({ column }) => column.getAggregationValue().toLocaleString() +``` + +Pass one options object with rows from any row model to choose a different set: + +```ts +column.getAggregationValue({ rows: table.getCoreRowModel().rows }) +column.getAggregationValue({ rows: table.getRowModel().rows }) +column.getAggregationValue({ rows: table.getFilteredSelectedRowModel().rows }) +column.getAggregationValue({ rows: table.getCoreRowModel().rows.slice(0, 3) }) +column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) +``` + +Depth is relative to the supplied row array. `0` selects those roots, `1` +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` + +## Grouped Aggregation + +Grouped aggregation composes two independent features. Register both, add the +grouped row-model slot, and configure aggregation functions on the columns that +should produce grouped values. + +```ts +const features = tableFeatures({ + rowAggregationFeature, + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), + aggregationFns: { sum: aggregationFn_sum }, +}) + +columnHelper.accessor('visits', { + aggregationFn: 'sum', + aggregatedCell: ({ getValue }) => getValue().toLocaleString(), + footer: ({ column }) => column.getAggregationValue().toLocaleString(), +}) +``` + +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. + +## Custom Aggregation Definitions + +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers + +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/ember/guide/cell-selection.md b/docs/framework/ember/guide/cell-selection.md new file mode 100644 index 0000000000..1115a7992f --- /dev/null +++ b/docs/framework/ember/guide/cell-selection.md @@ -0,0 +1,360 @@ +--- +title: Cell Selection (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Cell Selection](../examples/cell-selection) + +### Cell Selection Setup + +Here's how you set up your table to use cell selection features. Adding the cell selection feature enables the related APIs. + +```ts +import { + useTable, + tableFeatures, + cellSelectionFeature, +} from '@tanstack/ember-table' + +const features = tableFeatures({ cellSelectionFeature }) + +export default class MyTable extends Component { + @tracked data = defaultData + + table = useTable(() => ({ + features, + columns, + data: this.data, + })) +} +``` + +## Cell Selection (Ember) Guide + +The cell selection feature keeps track of spreadsheet-style rectangular selections. A user can click a cell, drag across a block of cells, Shift-click to extend, and Ctrl/Cmd-drag to add or subtract a rectangle based on whether the starting cell is selected. Let's take a look at some common use cases. + +### Access Cell Selection State + +The table instance already manages the cell selection state for you. You can access the selection or values derived from it through a few APIs. + +- `table.atoms.cellSelection.get()` - returns the current cell selection (a tracked read, so it invalidates templates and getters automatically) +- `getSelectedCellCount()` - returns how many cells are selected +- `getSelectedCellIds()` - returns the ids of every selected cell +- `getCellSelectionRowIds()` / `getCellSelectionColumnIds()` - returns the rows and columns the selection touches +- `getSelectedCellRangesData()` - returns each final positive selection region's values as a row-major grid + +```ts +console.log(table.atoms.cellSelection.get()) //get the cell selection state +console.log(table.getSelectedCellCount()) //3 +console.log(table.getSelectedCellIds()) //['0_firstName', '0_lastName', '1_firstName'] +console.log(table.getSelectedCellRangesData()) //[[['Tanner', 'Linsley'], ['Kevin', 'Vandy']]] +``` + +Reads of `table.atoms.cellSelection.get()` are tracked inside Ember reactive contexts, so they stay fresh automatically. Outside one, the same call is a plain snapshot. + +The expansion APIs (`getSelectedCellIds`, `getSelectedCellRangesData`) are memoized and pull-based. They cost nothing unless you actually call them, so a table that only highlights cells never pays to enumerate a large selection. + +### Cell Selection State Shape + +`CellSelectionState` is an ordered array of range operations, each stored as its two defining corners: + +```ts +type CellSelectionRange = { + anchorRowId: string + anchorColumnId: string + focusRowId: string + focusColumnId: string + operation?: 'include' | 'exclude' +} + +type CellSelectionState = Array +``` + +The `anchor` corner is where the selection started and stays put. The `focus` corner is the one that moves while dragging or Shift-extending. Storing both corners, rather than a normalized min/max rectangle, is what makes Shift-extend and "collapse back to the active cell" possible. + +Ranges are applied in order. An omitted `operation` is an inclusion for backward compatibility; an `exclude` range subtracts its rectangle from the selection produced so far. This compact operation log means a "select all except these cells" interaction does not build a map with one entry per selected cell. + +### Manage Cell Selection State + +If you need access to the selection elsewhere in your application, you can own the state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. + +```ts +import { createAtom } from '@tanstack/ember-table' +import { + useTable, + tableFeatures, + cellSelectionFeature, + type CellSelectionState, +} from '@tanstack/ember-table' + +const features = tableFeatures({ cellSelectionFeature }) +const cellSelectionAtom = createAtom([]) + +table = useTable(() => ({ + features, + columns, + data: this.data, + atoms: { cellSelection: cellSelectionAtom }, +})) +``` + +The classic controlled-state pattern also works: + +```ts +@tracked cellSelection: CellSelectionState = [] + +table = useTable(() => ({ + features, + columns, + data: this.data, + state: { cellSelection: this.cellSelection }, + onCellSelectionChange: (updater) => { + this.cellSelection = + typeof updater === 'function' ? updater(this.cellSelection) : updater + }, +})) +``` + +> [!NOTE] +> a drag emits one change per cell boundary the pointer crosses, so `onCellSelectionChange` fires repeatedly during a drag. If you are syncing selection to a server or a URL, debounce it or commit on `mouseup`. + +### Useful Row Ids + +Cell selection is keyed by row id and column id, so a meaningful row id matters here for the same reason it does with row selection. Use the `getRowId` table option to key selection by something stable from your data. + +```ts +table = useTable(() => ({ + features, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +})) +``` + +### Enable Cell Selection Conditionally + +Cell selection is enabled by default for every cell. Use the `enableCellSelection` table option to turn it off entirely, or pass a function for per-cell control. + +```ts +table = useTable(() => ({ + features, + //... + enableCellSelection: (cell) => cell.row.original.age > 18, //only adults' cells are selectable +})) +``` + +A column def can also opt out, which is the common case for checkbox or action columns. A column-level `false` wins over the table option. + +```ts +columnHelper.accessor('actions', { + enableCellSelection: false, //this column can never be selected +}) +``` + +A cell that cannot be selected is skipped even when a rectangle is drawn straight through it, and `moveCellSelection` steps over its column rather than landing on it. Use `cell.getCanSelect()` to decide whether to attach selection handlers in your UI. + +### Mouse Interactions + +Two cell handlers drive every mouse interaction: + +- `cell.getSelectionStartHandler()` - bind to `onMouseDown` +- `cell.getSelectionExtendHandler()` - bind to `onMouseEnter` + +```gts +{{! Ember templates extract function references without binding, so each + handler goes through a helper that calls it on the right cell }} + + + +``` + +You do not need to handle `mouseup` yourself. The start handler attaches its own document-level `mouseup` listener and removes it when the drag ends, so releasing the pointer outside the table still finishes the drag correctly. If your table renders into another document, such as an iframe or a popout window, pass that document in: `cell.getSelectionStartHandler(myDocument)`. + +#### Drag Selection + +Pressing down on a cell starts a new single-cell range, and every cell the pointer then enters moves that range's focus corner. Set `enableCellSelectionDrag: false` to require explicit clicks instead. + +#### Shift Range Selection + +Shift-clicking moves the active range's focus corner to the clicked cell, keeping its anchor fixed. The active cell therefore stays where the selection started, matching spreadsheet behavior. + +The handler recognizes Shift when the event exposes either `event.shiftKey` or `event.nativeEvent.shiftKey`. You can disable range behavior or replace the detection: + +```ts +table = useTable(() => ({ + features, + //... + enableCellRangeSelection: false, + + // For example, use the platform modifier instead of Shift: + // isCellRangeSelectionEvent: event => Boolean(event.metaKey), +})) +``` + +#### Multiple Ranges + +Ctrl-clicking or Cmd-clicking an unselected cell adds a new inclusive rectangle. Starting the same modified interaction on a selected cell adds an exclusion instead, so clicking removes that cell and dragging subtracts the whole rectangle. Whether the drag includes or excludes is fixed when it starts; shrinking an exclusion drag restores cells that leave its rectangle. Set `enableMultiCellRangeSelection: false` to disable both behaviors, or override `isMultiCellRangeSelectionEvent` to change the modifier. + +#### Programmatic Range Operations + +`table.selectCellRange(range)` replaces the current selection. Pass `{ mode: 'include' }` to append an inclusion or `{ mode: 'exclude' }` to append an exclusion. The older `{ additive: true }` option remains as a deprecated alias for include mode; `mode` wins if both options are supplied. `table.getCellSelectionBounds()` resolves the operation log into deterministic, disjoint positive rectangles. + +### Render Cell Selection UI + +TanStack Table does not dictate how you render selected cells. These cell APIs give you everything you need: + +- `cell.getIsSelected()` - whether this cell falls inside any range +- `cell.getIsFocused()` - whether this is the active cell (an excluded anchor can be focused without being selected) +- `cell.getSelectionEdges()` - which sides sit on the selection boundary +- `cell.getTabIndex()` - `0` for the focused cell and `-1` otherwise, for roving tabindex + +`getSelectionEdges()` returns `{ top, right, bottom, left }`, where a side is `true` when the neighboring cell in that direction is not itself selected. That is what lets you draw a single continuous outline around a selection, including around a union of separate rectangles, without every cell inspecting its neighbors. + +```tsx +function getCellClassName(cell) { + // most cells are unselected, so bail before asking for edges + if (!cell.getIsSelected()) { + return cell.getIsFocused() ? 'cell cell-focused' : 'cell' + } + + const edges = cell.getSelectionEdges() + + return [ + 'cell', + 'cell-selected', + cell.getIsFocused() && 'cell-focused', + edges.top && 'cell-edge-top', + edges.right && 'cell-edge-right', + edges.bottom && 'cell-edge-bottom', + edges.left && 'cell-edge-left', + ] + .filter(Boolean) + .join(' ') +} +``` + +> [!TIP] +> draw the outline with `box-shadow: inset ...` rather than `border`. On a `border-collapse` table a thicker border widens the shared grid line, which makes rows change height as cells become selected. A box-shadow never affects layout. + +### Keyboard Navigation + +Cell selection ships no keyboard handling of its own. Instead it exposes imperative APIs so a dedicated library, such as [TanStack Hotkeys](https://tanstack.com/hotkeys), can drive it: + +- `table.moveCellSelection(direction)` - collapse the selection to a single cell one step away +- `table.extendCellSelection(direction)` - move the active range's focus corner, keeping its anchor +- `table.setFocusedCell(rowId, columnId)` - collapse the selection to one specific cell +- `table.selectAllCells()` - select every selectable cell +- `table.resetCellSelection(true)` - clear the selection + +`direction` is `'up'`, `'down'`, `'left'`, or `'right'`. + +```ts +import { createMultiHotkeyHandler } from '@tanstack/hotkeys' + +// Ember has no hotkeys adapter, so the framework-agnostic core handler is used +onGridKeyDown = createMultiHotkeyHandler({ + ArrowUp: () => this.table.moveCellSelection('up'), + ArrowDown: () => this.table.moveCellSelection('down'), + 'Shift+ArrowDown': () => this.table.extendCellSelection('down'), + 'Mod+A': () => this.table.selectAllCells(), + Escape: () => this.table.resetCellSelection(true), +}) + +// then, in the template: +//
...
+``` + +Scope the hotkeys to the grid element rather than the document, or arrow keys and Escape will hijack inputs elsewhere on the page. + +### Copying a Selection + +`getSelectedCellRangesData()` returns raw values indexed as `[regionIndex][rowIndex][columnIndex]`. A region is one of the final disjoint positive rectangles after all include and exclude operations are applied, so it does not necessarily correspond one-to-one with stored state. Turning that into clipboard text is left to your application, because the delimiter, the representation of `null`, and any quoting rules are decisions only you can make. + +```ts +function escapeTsvValue(value: unknown) { + const text = value == null ? '' : String(value) + const safeText = + typeof value === 'string' && /^[\t\r ]*[=+@-]/.test(value) + ? `'${text}` + : text + // spreadsheets expect a quoted field once it contains a delimiter, a newline, + // or a quote, with inner quotes doubled + return /["\t\n\r]/.test(safeText) + ? `"${safeText.replace(/"/g, '""')}"` + : safeText +} + +function toTsv(ranges: Array>>) { + return ranges + .map((grid) => + grid.map((row) => row.map(escapeTsvValue).join('\t')).join('\n'), + ) + .join('\n\n') +} + +navigator.clipboard.writeText(toTsv(table.getSelectedCellRangesData())) +``` + +### How Ranges Survive Table Changes + +Ranges store row and column ids, not positions, so they follow their corner cells rather than screen coordinates. + +- **Sorting, filtering, and column reordering** keep the corners pinned and recompute what sits between them. A range from "row A to row B" still runs from A to B after a sort, even though different rows now fall in between. +- **Column pinning** is accounted for in render order, so a rectangle stays visually contiguous when a column is pinned. +- **Hiding a column** that a corner sits on makes the range inert. Nothing renders as selected, but the range stays in state and comes back when the column is shown again. +- **Pagination** resolves against the pre-pagination order, so a range can span pages and lights up correctly on whichever page you are viewing. + +Because a reorder can widen a selection onto columns the user never picked, some applications prefer to clear the selection whenever the column layout changes. That is a userland decision. Invoke a callback like this from a grid element modifier or resource that tracks the layout atoms: + +```ts +private lastLayoutKey: string | undefined + +onLayoutChange() { + const layoutKey = JSON.stringify([ + this.table.atoms.columnOrder.get(), + this.table.atoms.columnPinning.get(), + this.table.atoms.columnVisibility.get(), + ]) + + if (this.lastLayoutKey === undefined) { + this.lastLayoutKey = layoutKey + } else if (layoutKey !== this.lastLayoutKey) { + this.lastLayoutKey = layoutKey + queueMicrotask(() => this.table.resetCellSelection(true)) + } +} +``` + +### Resetting Cell Selection + +`table.resetCellSelection()` restores `initialState.cellSelection`. Pass `true` to ignore initial state and clear the selection entirely. + +The selection also resets automatically whenever `data` changes, because new data can invalidate the row ids a range points at, or silently re-select cells if the new data happens to reuse ids. Turn that off with `autoResetCellSelection: false`, and note that `autoResetAll` overrides it. + +```ts +table = useTable(() => ({ + features, + //... + autoResetCellSelection: false, //keep ranges across data changes +})) +``` + +### Performance + +Ember's tracked signals invalidate only the getters and template sections that +actually read the selection, so the example renders its cells plainly. There is +no equivalent of React's per-row `Subscribe` to reach for here. + +Measured on a table with a thousand rows and twelve columns, a drag updates in +roughly 19ms per move with plain reads. + +The per-cell reads are cheap by design. `cell.getIsSelected()` resolves the +cell's row and column index and compares them against a memoized cache of the +selection bounds, which is a handful of integer comparisons. If a very large +table does become a bottleneck, paginate or virtualize so fewer rows exist in the +DOM, rather than reaching for a subscription pattern. diff --git a/docs/framework/ember/guide/cell-spanning.md b/docs/framework/ember/guide/cell-spanning.md new file mode 100644 index 0000000000..d1b7a23312 --- /dev/null +++ b/docs/framework/ember/guide/cell-spanning.md @@ -0,0 +1,150 @@ +--- +title: Cell Spanning (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Cell Spanning](../examples/cell-spanning) + +### Cell Spanning Setup + +Here's how you set up your table to use cell spanning features. Adding the cell spanning feature enables the related APIs. + +```ts +import { + useTable, + tableFeatures, + cellSpanningFeature, +} from '@tanstack/ember-table' + +const features = tableFeatures({ cellSpanningFeature }) + +export default class MyTable extends Component { + @tracked data = defaultData + + table = useTable(() => ({ + features, + columns, + data: this.data, + })) +} +``` + +## Cell Spanning (Ember) Guide + +The cell spanning feature merges adjacent body cells into one rendered cell, the way `rowspan` and `colspan` merge cells in a plain HTML table or a spreadsheet. Row spans are derived from the data: adjacent rows that share a value in an opted-in column merge into one vertically spanning cell. Column spans are declared per row for things like full-width summary rows. + +The feature is stateless. Spans are always recomputed from the rows that are actually rendered, so sorting, filtering, pagination, and row pinning simply change which rows are adjacent and the spans follow. There is nothing to persist and nothing to reset. + +### Enable Row Spanning per Column + +Opt a column into value-based row spanning with `spanRows` on its column def: + +```ts +const columns = [ + columnHelper.accessor('region', { + spanRows: true, // adjacent rows with equal region values merge + }), +] +``` + +`spanRows: true` merges adjacent rows whose values are the same value, compared with `Object.is`. Nullish values never merge under the default comparison, since a merged block of blanks reads as a rendering bug and joins semantically unrelated rows. + +Pass a predicate to control run boundaries yourself. The run is anchored: every candidate row is tested against the run's first row, which keeps runs transitive by construction. + +```ts +columnHelper.accessor('createdAt', { + spanRows: ({ anchorValue, value }) => + sameMonth(anchorValue as Date, value as Date), +}) +``` + +### Rendering Spanned Cells + +A covered cell reports a span of `0`, and the renderer skips it. This is the same convention as [`header.rowSpan`](../../../guide/headers#header-row-spanning). Because v9 methods need explicit `this` binding, read the spans through small template helpers. + +```gts +// template helpers (v9 methods need explicit `this` binding) +const getVisibleCells = ( + row: Row, +): Array> => row.getVisibleCells() +const getRowSpan = (cell: Cell): number => + cell.getRowSpan() +const getColSpan = (cell: Cell): number => + cell.getColSpan() +const getIsCovered = (cell: Cell): boolean => + cell.getIsCovered() + + +``` + +`cell.getIsCovered()` is a convenience for checking that `cell.getRowSpan()` and `cell.getColSpan()` are both non-zero, so read the span numbers directly when you need them separately. + +### Column Spanning and Summary Rows + +Declare horizontal spans with `spanColumns` on the column that should carry the merged content. The count is resolved per row and measured in the order columns actually render, so hidden columns are not counted and column reordering is handled for you. + +```ts +columnHelper.accessor('label', { + spanColumns: ({ row }) => (row.original.isSummary ? Infinity : 1), +}) +``` + +Values larger than the available room are clamped to the end of the cell's pinned region, so `Infinity` means "the rest of my region". A column span can never cross the boundary between start-pinned, center, and end-pinned columns. + +When a cell spans rows and columns at once, the merged block is a rectangle: the anchor cell reports both spans and every other cell in the rectangle reports `0` on at least one axis. Cells only join a vertical run when their column spans match, so a full-width summary row never merges into the data run above it. + +### Spanning and Sorting, Filtering, and Pagination + +Spans are derived from the final row model, never stored, so every row model change recomputes them: + +- Sorting changes adjacency. Sorting by the spanned column clusters equal values and produces the largest runs; sorting by an unrelated column usually shatters them. +- Filtering removes rows. When a filter removes the middle of a run, the remaining neighbors become adjacent and merge. +- Pagination clips runs. A run never crosses a page boundary; the next page opens a fresh cell even when the value continues. +- Pinned rows render in separate sections, so a run never crosses a pinned section boundary either. + +### Disable Cell Spanning + +```ts +table = useTable(() => ({ + features, + columns, + data: this.data, + enableCellSpanning: false, // document-wide kill switch +})) + +columnHelper.accessor('status', { + enableCellSpanning: false, // per-column opt out +}) +``` + +### Selecting Merged Cells + +`cellSelectionFeature` composes with cell spanning. When both features are registered, a selection rectangle expands to fully enclose every merged cell it touches, so a merge is always entirely selected or entirely unselected. This applies to subtractions too: excluding any part of a merge deselects the whole merge. Arrow-key navigation treats a merge as a single stop, `getSelectedCellCount()` counts a merge once, and `getSelectedCellIds()` returns only the cells that render. `getSelectedCellRangesData()` still returns the full row-major lattice grid, since covered cells carry real underlying values. + +The expansion happens when the selection bounds are derived, not when the selection is stored. Stored corners stay stable while sorting, paging, or toggling `enableCellSpanning` changes which cells merge; the derived selection follows the current spans. + +### Known Limitations + +- Row virtualization needs extra care: if a run's anchor row is scrolled out of the rendered window, the covered rows render nothing. Read `table.getCellSpanIndex()` to find the anchor and render a clamped span at the top of the window. +- Grouped columns ignore `spanRows`, since grouping already collapses repeated values into group rows, and grouped rows never join a run in any column. +- Footer groups and `` rendering are unaffected by cell spanning. diff --git a/docs/framework/ember/guide/column-faceting.md b/docs/framework/ember/guide/column-faceting.md new file mode 100644 index 0000000000..b3b74db8c5 --- /dev/null +++ b/docs/framework/ember/guide/column-faceting.md @@ -0,0 +1,366 @@ +--- +title: Faceting (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Faceted Filters](../examples/filters-faceted) +- [Bucketed Faceted Filters](../examples/filters-faceted-bucketed) + +### Faceting Setup + +Here's how you set up your table to use faceting features. Adding the faceting feature enables the related APIs. If you use client-side faceting, also set up `filteredRowModel` and `facetedRowModel` after their features, since row model slots are type-checked. + +```ts +import { + useTable, + tableFeatures, + columnFacetingFeature, + columnFilteringFeature, + createFacetedRowModel, + createFacetedUniqueValues, + createFacetedMinMaxValues, + createFilteredRowModel, + filterFns, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + facetedRowModel: createFacetedRowModel(), // if using client-side faceting + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + filterFns, +}) + +// inside your Glimmer component +table = useTable(() => ({ + features, + columns, + data: this.data, +})) +``` + +## Faceting (Ember) Guide + +### What is Faceting? + +Faceting derives information that can be used to build filtering interfaces. For a given column, faceting can answer questions such as: + +- Which values are available? +- How often does each value occur? +- What is the minimum and maximum value among the available rows? +- Which rows should be used for a custom facet calculation? + +For example, an application could use faceting to render a plan filter like this: + +```text +Plan +☐ Free 128 +☐ Pro 47 +☐ Enterprise 9 +``` + +The plan names and counts are derived from the table's faceted row model. If a filter on another column changes, such as `Region = Europe`, the plan counts update to describe only the rows in that region. + +Faceting does not apply filters to the table. It provides values, counts, ranges, or rows that you can use to build a filter UI. The column filtering feature owns the filter state and determines which rows match the selected filter values. + +#### Faceting vs Row Aggregation + +Faceting and row aggregation both summarize data, but they serve different purposes. Faceting produces metadata for filter controls, such as available values, occurrence counts, or a numeric range. Row aggregation computes result values over a set of rows, such as a sum, average, or total, for display in footers or grouped rows. + +Faceted counts do not create aggregate rows or use a column's `aggregationFn`. A useful way to distinguish the features is: + +- Filtering answers: Which rows remain? +- Faceting answers: Which filtering choices remain? +- Row aggregation answers: What summary value can be calculated from these rows? + +### How Column Faceting Responds to Filters + +A column's faceted row model includes rows that pass every applicable filter except that column's own filter. This lets a facet continue to show alternative choices while the user edits it. + +Consider a table with `Region` and `Plan` filters: + +1. The user selects `Region = Europe`. +2. The `Plan` facet applies the region filter and recalculates its plan counts. +3. The user selects `Plan = Pro`. +4. The table displays only European Pro rows. +5. The `Plan` facet still calculates its choices from all European rows because it excludes its own `Plan` filter. + +Other facets do apply the selected plan filter. For example, a `Status` facet would now describe only European Pro rows. This is what allows multiple facets to narrow each other. + +Client-side faceting needs both `filteredRowModel` and `facetedRowModel` to provide this behavior. Without a filtered row model, the faceted row model falls back to the pre-filtered rows, so its values will not react to other column filters. + +### Faceting APIs + +Use the faceting API that matches the filter interface you are building: + +| API | Result | Common uses | +| --------------------------------- | --------------------------------------- | ------------------------------------------------------ | +| `column.getFacetedRowModel()` | Rows that pass the other active filters | Custom facet calculations | +| `column.getFacetedUniqueValues()` | A `Map` of values to occurrence counts | Checkboxes, select menus, and autocomplete suggestions | +| `column.getFacetedMinMaxValues()` | A `[min, max]` tuple or `undefined` | Number inputs and range sliders | + +The row model factories registered in `tableFeatures` enable these APIs: + +- `createFacetedRowModel()` is required for client-side faceting. +- `createFacetedUniqueValues()` is required for unique values and counts. +- `createFacetedMinMaxValues()` is required for numeric minimum and maximum values. + +Register only the factories your table uses. The complete setup near the top of this guide registers all three. + +### Unique Values and Counts + +`column.getFacetedUniqueValues()` returns a `Map` whose keys are facet values and whose values are occurrence counts. You can turn that map into a sorted list for an autocomplete or select control: + +```ts +const suggestions = Array.from(column.getFacetedUniqueValues().entries()) + .sort(([valueA], [valueB]) => String(valueA).localeCompare(String(valueB))) + .slice(0, 5_000) +``` + +Each entry contains both the value and its count: + +```gts +get suggestions() { + return Array.from(this.args.column.getFacetedUniqueValues().entries()).map( + ([value, count]) => ({ value: String(value), count }), + ) +} + + +``` + +For a scalar column, each row normally contributes one value, so the occurrence count is also a row count. A row can contribute more than one facet value by defining the column's `getUniqueValues` option. In that case, the counts describe occurrences and their total can be greater than the number of rows. + +```ts +columnHelper.accessor('tags', { + header: 'Tags', + getUniqueValues: (row) => row.tags, +}) +``` + +If you want each count to represent rows, make sure `getUniqueValues` returns each value no more than once per row. + +### Reactive Facet Controls in Ember + +Read facet APIs from getters on the Glimmer component that renders the controls. The getters run again when the table's tracked filter state changes. + +```gts +class FacetOptions extends Component { + get values() { + const selected = (this.args.column.getFilterValue() ?? []) as Array + + return Array.from(this.args.column.getFacetedUniqueValues().entries()).map( + ([value, count]) => ({ + value, + label: String(value), + count, + checked: selected.includes(value), + toggle: () => this.toggleValue(value), + }), + ) + } + + toggleValue = (value: unknown) => { + const selected = (this.args.column.getFilterValue() ?? []) as Array + + this.args.column.setFilterValue( + selected.includes(value) + ? selected.filter((selectedValue) => selectedValue !== value) + : [...selected, value], + ) + } + + +} +``` + +The filter function for the column still determines how the selected values match rows. See the [Column Filtering Guide](./column-filtering) for filter functions and filter state, or the [Faceted Filters example](../examples/filters-faceted) for a complete implementation. + +### Minimum and Maximum Values + +`column.getFacetedMinMaxValues()` returns the numeric range available after applying the other active filters. It returns `undefined` when there are no numeric values. + +```gts +get range(): [number, number] { + return this.args.column.getFacetedMinMaxValues() ?? [0, 1] +} + +get min(): number { + return this.range[0] +} + +get max(): number { + return this.range[1] +} + +get currentValue(): string { + return String(this.args.column.getFilterValue() ?? '') +} + +changeValue = (event: Event) => { + this.args.column.setFilterValue( + Number((event.target as HTMLInputElement).value), + ) +} + + +``` + +The minimum and maximum describe the values that are available to the filter UI. Your column's filter function determines how a selected value or range filters rows. + +### Bucketed Faceting for Continuous Values + +Raw unique values are not always useful. Dates, file sizes, durations, prices, and measurements can produce hundreds or thousands of distinct values. These columns are often easier to filter when their values are placed into meaningful buckets: + +```text +Last login +☐ Today +☐ Yesterday +☐ This week +☐ This month +☐ Older +``` + +You can use the column's `getUniqueValues` option to return a bucket key for faceting while keeping the original accessor value for rendering and other table features. + +```ts +type StorageBucket = + 'under-1-gb' | '1-to-10-gb' | '10-to-100-gb' | '100-gb-plus' + +const GB = 1024 ** 3 + +function getStorageBucket(value: number): StorageBucket { + if (value < GB) return 'under-1-gb' + if (value < 10 * GB) return '1-to-10-gb' + if (value < 100 * GB) return '10-to-100-gb' + return '100-gb-plus' +} + +const storageBucketFilter = constructFilterFn({ + resolveDataValue: (value) => getStorageBucket(value as number), + filter: (bucket, selected: Array) => selected.includes(bucket), + autoRemove: (selected: Array) => selected.length === 0, +}) + +columnHelper.accessor('storageBytes', { + header: 'Storage', + getUniqueValues: (row) => [getStorageBucket(row.storageBytes)], + filterFn: storageBucketFilter, +}) +``` + +Faceting and filtering should use the same bucket definitions so the displayed counts match the rows selected by each bucket. The column keeps its raw numeric value, so there is no need to create a hidden derived column only for faceting. See the [Bucketed Faceted Filters example](../examples/filters-faceted-bucketed) for complete date and storage bucket filters. + +### Client-Side Faceting and Performance + +The built-in client-side faceting row models are memoized. They recalculate when their input rows or relevant filter state changes. The cost still depends on the number of rows, columns, and unique values in the table. + +For columns with many unique values, consider these options: + +- Render only the first or most relevant values instead of every map entry. +- Let users search the available values before rendering a long list. +- Bucket continuous or high-cardinality values into useful ranges. +- Move faceting to the server when the complete dataset is not available in the browser. + +Avoid sorting or converting a large facet map repeatedly in unrelated components. Derive and render facet options close to the component that subscribes to the relevant filter state. + +### Custom Server-Side Faceting + +When filtering is performed on the server, the rows loaded into the browser may not contain enough information to calculate complete facet values or counts. In that case, calculate the facets on the server and provide custom `facetedUniqueValues` and `facetedMinMaxValues` factories. + +Each factory receives the table and a column ID, then returns a function that resolves the faceted result. The regular column APIs will return the server-provided values. + +Factories are resolved once per table and column, but the function each factory returns runs on every read; the table does not cache its result. Read live values inside that returned function (from a signal, store, or `table.options.meta`) so updated server facets show up immediately, and memoize inside the factory if the calculation is expensive. + +```ts +// `this.serverFacets` is a @tracked field on your component, set when +// the facet request resolves +const features = tableFeatures({ + columnFacetingFeature, + // The returned functions run on every read and table.options stays in + // sync with the latest render, so read live data through options.meta + facetedUniqueValues: (table, columnId) => () => { + const serverFacets = table.options.meta?.serverFacets + return new Map(serverFacets?.uniqueValues[columnId] ?? []) + }, + facetedMinMaxValues: (table, columnId) => () => { + return table.options.meta?.serverFacets?.minMaxValues[columnId] + }, +}) + +// Inside your Glimmer component: +table = useTable(() => ({ + features, + columns, + meta: { serverFacets: this.serverFacets }, + data: this.data, +})) +``` + +To match the built-in column faceting behavior, a server query for one column should apply the other active filters but exclude that column's own filter. This keeps alternative choices available in the current facet while allowing facets to narrow each other. + +You can also fetch facet values and pass them directly to your filter components without using the TanStack Table faceting APIs. + +### Global Faceting + +Global faceting derives values across every leaf column that can participate in global filtering. It is useful for autocomplete suggestions or other metadata associated with a global filter. The global faceted row model applies active column filters and excludes the global filter itself. + +If the table uses global filtering, register `globalFilteringFeature` so the row filtering pipeline evaluates the global filter. The same faceting factories used by column faceting also power these table APIs: + +```ts +const globalFacetedRows = table.getGlobalFacetedRowModel().flatRows + +const suggestions = Array.from(table.getGlobalFacetedUniqueValues().entries()) + +const [min, max] = table.getGlobalFacetedMinMaxValues() ?? [0, 1] +``` + +Custom faceting factories receive the internal `__global__` column ID for global requests. You can branch on that ID when the server returns separate column and global facet results: + +```ts +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + if (columnId === '__global__') { + return new Map(globalFacets.uniqueValues) + } + + return new Map(columnFacets[columnId]?.uniqueValues) + }, +}) +``` diff --git a/docs/framework/ember/guide/column-filtering.md b/docs/framework/ember/guide/column-filtering.md new file mode 100644 index 0000000000..7212fad938 --- /dev/null +++ b/docs/framework/ember/guide/column-filtering.md @@ -0,0 +1,467 @@ +--- +title: Column Filtering (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Bucketed Faceted Filters](../examples/filters-faceted-bucketed) +- [Fuzzy Search](../examples/filters-fuzzy) + +### Column Filtering Setup + +Here's how you set up your table to use column filtering features. Adding the column filtering feature enables the related APIs. If you use client-side filtering, also set up `filteredRowModel` after its feature, since row model slots are type-checked. + +```gts +import { + useTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFn_includesString, + filterFn_inNumberRange, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + // manualFiltering: true, // if using manual server-side filtering + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +// inside your component class +table = useTable(() => ({ + features, + columns, + data: this.data, +})) +``` + +> [!NOTE] +> The `filterFns` registry above lists only the built-in filter functions this table uses. Spreading the entire built-in `filterFns` registry (`filterFns: { ...filterFns }`) still works, but it puts every built-in filter function in your bundle. Register just the functions you use, or pass a function directly to the `filterFn` column option with no registration at all. + +## Column Filtering (Ember) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on column filtering, which is a filter that is applied to a single column's accessor value. + +TanStack table supports both client-side and manual server-side filtering. This guide will go over how to implement and customize both, and help you decide which one is best for your use-case. + +### Client-Side vs Server-Side Filtering + +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. + +The client-side filtered row model also invokes the page-index auto-reset hook when column filtering inputs change. Whether the page index resets depends on the `autoResetPageIndex`, `autoResetAll`, and `manualPagination` options. If filtering is manual and this row model is omitted or bypassed, a column filter state change does not invoke that hook, so reset server-side pagination in the filter change handler when needed. + +### Manual Server-Side Filtering + +If you have decided that you need to implement server-side filtering instead of using the built-in client-side filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to the features object, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```gts +const features = tableFeatures({ columnFilteringFeature }) + +table = useTable(() => ({ + features, + data: this.data, + columns, + manualFiltering: true, +})) +``` + +> [!NOTE] +> When using manual filtering, many of the options that are discussed in the rest of this guide will have no effect. When `manualFiltering` is set to `true`, the table instance will not apply any filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the `data` that you pass to it as-is. + +### Client-Side Filtering + +If you are using the built-in client-side filtering features, add the `columnFilteringFeature` and the `filteredRowModel` factory to your features. Import `createFilteredRowModel` and the filter functions you need from TanStack Table: + +```gts +import { + useTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFn_includesString, + filterFn_inNumberRange, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +table = useTable(() => ({ + features, + data: this.data, + columns, +})) +``` + +### Column Filter State + +Whether or not you use client-side or server-side filtering, you can take advantage of the built-in column filter state management that TanStack Table provides. There are many table and column APIs to mutate and interact with the filter state and retrieve the column filter state. + +The column filtering state is defined as an array of objects with the following shape: + +```ts +interface ColumnFilter { + id: string + value: unknown +} +type ColumnFiltersState = ColumnFilter[] +``` + +Since the column filter state is an array of objects, you can have multiple column filters applied at once. + +#### Accessing Column Filter State + +For reactive reads that should re-render your UI, read `table.store.state.columnFilters` from a getter or directly in a template; Glimmer tracks the read and re-renders when the slice changes. In event handlers or other non-render code, you can read the current snapshot with `table.atoms.columnFilters.get()`, but this read does not subscribe the component to future changes. + +```gts +table = useTable(() => ({ + features, + columns, + data: this.data, + //... +})) + +// reactive read in a getter (re-renders when the filters change) +get columnFilters() { + return this.table.store.state.columnFilters +} + +// this.table.atoms.columnFilters.get() // snapshot read in event handlers +``` + +However, if you need access to the column filter state outside of the table, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state in other parts of your application, you can own the column filter state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the filter values can be used elsewhere (such as in a query key for server-side filtering) without forcing the component that owns the table to re-render. + +```gts +import { createAtom } from '@tanstack/ember-table' + +const columnFiltersAtom = createAtom([]) // can set initial column filter state here + +// read the atom wherever you need the value (e.g. for a query key) +// columnFiltersAtom.get() + +table = useTable(() => ({ + features, + columns, + data: this.data, + //... + atoms: { + columnFilters: columnFiltersAtom, // table filter APIs now update columnFiltersAtom + }, +})) +``` + +Alternatively, the v8-style `state.columnFilters` plus `onColumnFiltersChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```gts +@tracked columnFilters: ColumnFiltersState = [] + +table = useTable(() => ({ + features, + columns, + data: this.data, + //... + state: { + columnFilters: this.columnFilters, + }, + onColumnFiltersChange: (updater) => { + this.columnFilters = + typeof updater === 'function' ? updater(this.columnFilters) : updater + }, +})) +``` + +#### Initial Column Filter State + +If you do not need to control the column filter state in your own state management or scope, but you still want to set an initial column filter state, you can use the `initialState` table option instead of `state`. + +```gts +table = useTable(() => ({ + features, + columns, + data: this.data, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +})) +``` + +> [!NOTE] +> Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 18 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `startsWith` - Case-insensitive string prefix match +- `endsWith` - Case-insensitive string suffix match +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `empty` - The row's value is nullish or whitespace-only (the filter value is an on/off flag) +- `notEmpty` - The row's value is not nullish or whitespace-only (the filter value is an on/off flag) +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `inDateRange` - Inclusive `[min, max]` date range accepting `Date` objects, timestamps, or date strings (blank endpoints are open-ended) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom filter functions, either inline as the `filterFn` column option, or by name in the `filterFns` registry slot on `tableFeatures`. + +#### Custom Filter Functions + +> [!NOTE] +> These filter functions only run during client-side filtering. + +Whether you register a custom filter function in the `filterFns` slot on `tableFeatures` or pass it directly as a `filterFn` column option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = ( + row, // Row + columnId: string, + filterValue: any, + addMeta?: (meta: FilterMeta) => void, +): boolean => ... +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```ts +const myCustomFilterFn: FilterFn = ( + row, + columnId, + filterValue, +) => { + return // true or false based on your custom logic +} + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + myCustomFilterFn, + startsWith: startsWithFilterFn, // defined elsewhere + }, +}) + +const columns = columnHelper.columns([ + columnHelper.accessor('name', { + header: () => 'Name', + filterFn: 'includesString', // use built-in filter function + }), + columnHelper.accessor('age', { + header: () => 'Age', + filterFn: 'inNumberRange', + }), + columnHelper.accessor('birthday', { + header: () => 'Birthday', + filterFn: 'myCustomFilterFn', // reference a custom filter function registered in features + }), + columnHelper.accessor('profile', { + header: () => 'Profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + }), +]) +//... +table = useTable(() => ({ + features, + columns, + data: this.data, +})) +``` + +> **TypeScript Note:** For `filterFn: 'myCustomFilterFn'` string references to typecheck, register the function in the `filterFns` slot on `tableFeatures` (as shown above). Alternatively, skip the registry entirely by passing the function directly to the `filterFn` column option. See the [Fuzzy Search example](../examples/filters-fuzzy) for a complete registration example. + +##### Customize Filter Function Behavior + +You can attach a few other properties to filter functions to customize their behavior: + +- `filterFn.resolveFilterValue` - This optional "hanging" method on any given `filterFn` allows the filter function to transform/sanitize/format the filter value before it is passed to the filter function. The table applies it once per filter (not once per row), so it is also the right place for expensive preparation work. + +- `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. + +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. + +The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: + +```ts +const startsWithFilterFn = constructFilterFn({ + // compare the (resolved) row value against the (resolved) filter value + filter: (dataValue, filterValue) => + Boolean(dataValue?.startsWith(filterValue)), + // normalize the filter value once, before any rows are tested + resolveFilterValue: (value) => String(value).toLowerCase().trim(), + // normalize each row's value before it reaches the comparator + resolveDataValue: (value) => String(value ?? '').toLowerCase(), + // remove the filter value from filter state if it is falsy (empty string in this case) + autoRemove: (value) => !value, +}) +``` + +Keeping the comparison in `filter` and the normalization in the resolvers pays off when you need a variant of an existing filter function. The definition is attached to the returned function, so you can spread any filter function built with `constructFilterFn` and override only what differs. For example, a version of `includesString` that also ignores diacritics (so a search for "eric" matches "Éric"): + +```ts +const normalize = (value: unknown) => + String(value ?? '') + .toLowerCase() + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + +const includesStringIgnoreDiacritics = constructFilterFn({ + ...filterFn_includesString, // reuse the comparator and autoRemove behavior + resolveFilterValue: normalize, + resolveDataValue: normalize, +}) +``` + +Register the variant by name in the `filterFns` registry or pass it directly to the `filterFn` column option, just like any other custom filter function. + +> [!NOTE] +> The table applies `resolveFilterValue` once per filter before any rows are tested. If you ever call a filter function directly (outside of a table), resolve the filter value yourself: `myFilterFn(row, columnId, myFilterFn.resolveFilterValue?.(rawValue) ?? rawValue)`. + +### Customize Column Filtering + +There are a lot of table and column options that you can use to further customize the column filtering behavior. + +#### Disable Column Filtering + +By default, column filtering is enabled for all columns. You can disable the column filtering for all columns or for specific columns by using the `enableColumnFilters` table option or the `enableColumnFilter` column option. You can also turn off both column and global filtering by setting the `enableFilters` table option to `false`. + +Disabling column filtering for a column will cause the `column.getCanFilter` API to return `false` for that column. + +```ts +const columns = columnHelper.columns([ + columnHelper.accessor('id', { + header: () => 'Id', + enableColumnFilter: false, // disable column filtering for this column + }), + //... +]) +//... +table = useTable(() => ({ + features, + columns, + data: this.data, + enableColumnFilters: false, // disable column filtering for all columns +})) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```gts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +table = useTable(() => ({ + features, + columns, + data: this.data, + filterFromLeafRows: true, // filter and search through sub-rows +})) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```gts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +table = useTable(() => ({ + features, + columns, + data: this.data, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +})) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `onChange` or `onBlur` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/ember/guide/column-ordering.md b/docs/framework/ember/guide/column-ordering.md new file mode 100644 index 0000000000..2fed31a5fe --- /dev/null +++ b/docs/framework/ember/guide/column-ordering.md @@ -0,0 +1,186 @@ +--- +title: Column Ordering (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Column Ordering](../examples/column-ordering) + +### Column Ordering Setup + +Here's how you set up your table to use column ordering features. Adding the column ordering feature enables the related APIs. + +```ts +import { + useTable, + tableFeatures, + columnOrderingFeature, +} from '@tanstack/ember-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const table = useTable(() => ({ + features, + columns, + data, +})) +``` + +## Column Ordering (Ember) Guide + +By default, columns are ordered in the order they are defined in the `columns` array. However, you can manually specify the column order using the `columnOrder` state. Other features like column pinning and grouping can also affect the column order. + +### What Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual **Column Ordering** - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +> [!NOTE] +> `columnOrder` state will only affect unpinned columns if used in conjunction with column pinning. + +### Column Order State + +If you don't provide a `columnOrder` state, TanStack Table will just use the order of the columns in the `columns` array. However, you can provide an array of string column ids to the `columnOrder` state to specify the order of the columns. + +#### Default Column Order + +If all you need to do is specify the initial column order, you can just specify the `columnOrder` state in the `initialState` table option. + +```ts +const features = tableFeatures({ columnOrderingFeature }) + +const table = useTable(() => ({ + features, + //... + initialState: { + columnOrder: ['columnId1', 'columnId2', 'columnId3'], + }, + //... +})) +``` + +> [!NOTE] +> If you are using the `state` table option to also specify the `columnOrder` state, the `initialState` will have no effect. Only specify particular states in either `initialState` or `state`, not both. + +#### Managing Column Order State + +If you need to dynamically change the column order, or set the column order after the table has been initialized, you can manage the `columnOrder` state just like any other table state. + +In v9, the recommended way to own a state slice is with an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the column order without re-rendering the component that owns the table. + +```ts +import { + useTable, + tableFeatures, + columnOrderingFeature, + createAtom, + type ColumnOrderState, +} from '@tanstack/ember-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const columnOrderAtom = createAtom([ + 'columnId1', + 'columnId2', + 'columnId3', +]) + +const columnOrder = columnOrderAtom.get() // read the atom wherever it is needed + +const table = useTable(() => ({ + features, + //... + atoms: { + columnOrder: columnOrderAtom, + }, + //... +})) +``` + +Alternatively, the v8-style `state.columnOrder` plus `onColumnOrderChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnOrderingFeature }) + +// inside your Glimmer component class +@tracked columnOrder: ColumnOrderState = ['columnId1', 'columnId2', 'columnId3'] + +table = useTable(() => ({ + features, + //... + state: { + columnOrder: this.columnOrder, + //... + }, + onColumnOrderChange: (updater) => { + this.columnOrder = + typeof updater === 'function' ? updater(this.columnOrder) : updater + }, +})) +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, hook the drop event of your drag-and-drop solution up to `table.setColumnOrder`. Here is a splice-based reorder helper you can call from a drop handler: + +```ts +import type { Table } from '@tanstack/ember-table' + +// reorder columns after a drag and drop interaction +const handleColumnDrop = ( + table: Table, + activeId: string, + overId: string, +) => { + if (activeId !== overId) { + table.setColumnOrder((prevColumnOrder) => { + const columnOrder = [...prevColumnOrder] + const oldIndex = columnOrder.indexOf(activeId) + const newIndex = columnOrder.indexOf(overId) + columnOrder.splice(newIndex, 0, columnOrder.splice(oldIndex, 1)[0]!) + return columnOrder // splice util + }) + } +} +``` + +`table.setColumnOrder` works the same whether the table manages the `columnOrder` state internally, you control it with `state` + `onColumnOrderChange`, or you own it with an external atom. + +### Column Ordering APIs + +Use `table.setColumnOrder` to update the column order state directly. Use `table.resetColumnOrder` to reset the order to `initialState.columnOrder`, or pass `true` to clear the order state. + +```ts +table.setColumnOrder(['lastName', 'firstName', 'age']) +table.resetColumnOrder() +table.resetColumnOrder(true) +``` + +Columns expose helpers for reading their current position after column pinning, manual ordering, and grouping have been applied. + +```ts +column.getIndex() +column.getIndex('start') +column.getIndex('center') +column.getIndex('end') + +column.getIsFirstColumn() +column.getIsLastColumn() +``` + +These helpers are useful for styling column boundaries or building drag-and-drop targets that need to know the current rendered order. + +#### Drag and Drop Column Reordering Suggestions + +TanStack Table is not opinionated about which drag-and-drop solution you use. Here are a few suggestions: + +1. Native browser drag events (`dragstart`, `dragover`, `drop`) wired up with the `{{on}}` modifier and your own `@tracked` state are the lightest option and need no dependencies. This is the approach the [Row DnD](../examples/row-dnd) example uses. It is very lightweight, but you will need to do extra work for proper touch support on mobile. + +2. Use an Ember drag-and-drop addon if you want a library to handle pointer and touch input, autoscrolling, and accessibility for you. Whichever you choose, hook its drop event up to `table.setColumnOrder` as shown above. + +3. If you evaluate a DnD library, check its maintenance status, Ember and Glint compatibility, bundle size, and how well it handles semantic `` markup before committing. diff --git a/docs/framework/ember/guide/column-pinning.md b/docs/framework/ember/guide/column-pinning.md new file mode 100644 index 0000000000..75313e4660 --- /dev/null +++ b/docs/framework/ember/guide/column-pinning.md @@ -0,0 +1,357 @@ +--- +title: Column Pinning (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) + +### Column Pinning Setup + +Here's how you set up your table to use column pinning features. Adding the column pinning feature enables the related APIs. + +```ts +import { + useTable, + tableFeatures, + columnPinningFeature, +} from '@tanstack/ember-table' + +const features = tableFeatures({ columnPinningFeature }) + +const table = useTable(() => ({ + features, + columns, + data, +})) +``` + +## Column Pinning (Ember) Guide + +TanStack Table offers state and APIs helpful for implementing column pinning features in your table UI. You can implement column pinning in multiple ways. You can either split pinned columns into their own separate tables, or you can keep all columns in the same table, but use the pinning state to order the columns correctly and use sticky CSS to pin the columns to the start or end. + +`start` and `end` are logical pinning regions. In LTR languages/layouts, `start` usually corresponds to left and `end` to right. In RTL languages/layouts, `start` usually corresponds to right and `end` to left. + +### How Column Pinning Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. **Column Pinning** - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +The only way to change the order of the pinned columns is in the `columnPinning.start` and `columnPinning.end` state itself. `columnOrder` state will only affect the order of the unpinned ("center") columns. + +### Column Pinning State + +Managing the `columnPinning` state is optional, and usually not necessary unless you are adding persistent state features. TanStack Table will already keep track of the column pinning state for you. Manage the `columnPinning` state just like any other table state if you need to. + +In v9, the recommended way to own a state slice is with an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without re-rendering the component that owns the table. + +```gts +import { + useTable, + createAtom, + tableFeatures, + columnPinningFeature, + type ColumnPinningState, +} from '@tanstack/ember-table' + +const features = tableFeatures({ columnPinningFeature }) + +export default class MyTable extends Component { + columnPinningAtom = createAtom({ + start: [], + end: [], + }) + + table = useTable(() => ({ + features, + //... + atoms: { + columnPinning: this.columnPinningAtom, + }, + //... + })) + + // read the atom wherever it is needed + get columnPinning() { + return this.columnPinningAtom.get() + } +} +``` + +Alternatively, the v8-style `state.columnPinning` plus `onColumnPinningChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```gts +export default class MyTable extends Component { + @tracked columnPinning: ColumnPinningState = { + start: [], + end: [], + } + + table = useTable(() => ({ + features, + //... + state: { + columnPinning: this.columnPinning, + //... + }, + onColumnPinningChange: (updater) => { + this.columnPinning = + typeof updater === 'function' ? updater(this.columnPinning) : updater + }, + //... + })) +} +``` + +### Pin Columns by Default + +A very common use case is to pin some columns by default. You can do this by either initializing the `columnPinning` state with the pinned columnIds, or by using the `initialState` table option: + +```ts +const table = useTable(() => ({ + features, + //... + initialState: { + columnPinning: { + start: ['expand-column'], + end: ['actions-column'], + }, + //... + }, + //... +})) +``` + +### Useful Column Pinning APIs + +> [!NOTE] +> These APIs are available when using `columnPinningFeature`. + +There are a handful of useful Column API methods to help you implement column pinning features: + +- `column.getCanPin`: Use to determine if a column can be pinned. +- `column.pin`: Use to pin a column to the start or end. Or use to unpin a column. +- `column.getIsPinned`: Use to determine where a column is pinned. +- `column.getPinnedIndex`: Use to read the column's index within its pinned column group. +- `column.getStart`: Use to provide the correct `start` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `end` CSS value for a pinned column. +- `column.getIsLastColumn`: Use to determine if a column is the last column in its pinned group. Useful for adding a box-shadow. +- `column.getIsFirstColumn`: Use to determine if a column is the first column in its pinned group. Useful for adding a box-shadow. + +Use `table.setColumnPinning` to update the pinning state directly. Use `table.resetColumnPinning` to reset to `initialState.columnPinning`, or pass `true` to clear both pinned column arrays. + +```ts +table.setColumnPinning({ + start: ['firstName'], + end: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```ts +table.getStartLeafColumns() +table.getCenterLeafColumns() +table.getEndLeafColumns() + +table.getStartVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getEndVisibleLeafColumns() + +table.getStartHeaderGroups() +table.getCenterHeaderGroups() +table.getEndHeaderGroups() + +table.getStartFooterGroups() +table.getCenterFooterGroups() +table.getEndFooterGroups() + +table.getStartFlatHeaders() +table.getCenterFlatHeaders() +table.getEndFlatHeaders() + +table.getStartLeafHeaders() +table.getCenterLeafHeaders() +table.getEndLeafHeaders() +``` + +You can also request pinned leaf columns by region with `table.getPinnedLeafColumns(position)` and visible pinned leaf columns with `table.getPinnedVisibleLeafColumns(position)`. + +```ts +table.getPinnedLeafColumns('start') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('end') + +table.getPinnedVisibleLeafColumns('start') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('end') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'start'` or `'end'` to check one pinned side. + +Because Ember templates extract method references without binding them, wrap these Column and Table method calls in small module-scope helper functions (or getters), then call the helpers from your markup: + +```ts +import { type Column, type ColumnPinningPosition } from '@tanstack/ember-table' + +const getCanPin = (column: Column): boolean => + column.getCanPin() + +const getIsPinned = ( + column: Column, +): ColumnPinningPosition => column.getIsPinned() + +const isPinnedStart = (column: Column): boolean => + column.getIsPinned() === 'start' + +const isPinnedEnd = (column: Column): boolean => + column.getIsPinned() === 'end' + +const pin = ( + column: Column, + side: ColumnPinningPosition, +) => { + return () => column.pin(side) +} +``` + +```hbs +{{#if (getCanPin header.column)}} +
+ {{#unless (isPinnedStart header.column)}} + + {{/unless}} + {{#if (getIsPinned header.column)}} + + {{/if}} + {{#unless (isPinnedEnd header.column)}} + + {{/unless}} +
+{{/if}} +``` + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getStartHeaderGroups`, `table.getCenterHeaderGroups`, `table.getEndHeaderGroups`, `row.getStartVisibleCells`, `row.getCenterVisibleCells`, and `row.getEndVisibleCells` methods to only render the columns that are relevant to the current table. + +```ts +const getStartVisibleCells = ( + row: Row, +): Array> => row.getStartVisibleCells() + +const getCenterVisibleCells = ( + row: Row, +): Array> => row.getCenterVisibleCells() + +const getEndVisibleCells = ( + row: Row, +): Array> => row.getEndVisibleCells() +``` + +```gts +export default class MyTable extends Component { + table = useTable(() => ({ + features, + columns, + data: this.data, + initialState: { + columnPinning: { start: ['firstName'], end: ['progress'] }, + }, + })) + + get leftHeaderGroups() { + return this.table.getStartHeaderGroups() + } + + get centerHeaderGroups() { + return this.table.getCenterHeaderGroups() + } + + get rightHeaderGroups() { + return this.table.getEndHeaderGroups() + } + + get rows() { + return this.table.getRowModel().rows + } + + +} +``` + +For sticky CSS pinning, use `column.getStart('start')` and `column.getAfter('end')` to compute the correct offsets, then apply them as `left`/`right` values in your style string: + +```ts +import { htmlSafe, type SafeString } from '@ember/template' + +const getStart = (column: Column): number => + column.getStart('start') + +const getAfter = (column: Column): number => + column.getAfter('end') + +const pinningStyle = (column: Column): SafeString => { + const isPinned = column.getIsPinned() + const parts: Array = [`width:${column.getSize()}px`] + + if (isPinned === 'start') { + parts.push('position:sticky', `left:${getStart(column)}px`, 'z-index:1') + } else if (isPinned === 'end') { + parts.push('position:sticky', `right:${getAfter(column)}px`, 'z-index:1') + } else { + parts.push('position:relative') + } + + return htmlSafe(parts.join(';')) +} +``` diff --git a/docs/framework/ember/guide/column-resizing.md b/docs/framework/ember/guide/column-resizing.md new file mode 100644 index 0000000000..e026c6c400 --- /dev/null +++ b/docs/framework/ember/guide/column-resizing.md @@ -0,0 +1,320 @@ +--- +title: Column Resizing (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) + +### Column Resizing Setup + +Here's how you set up your table to use column resizing features. Column resizing depends on column sizing, so add `columnSizingFeature` before `columnResizingFeature`. Adding the column resizing feature enables the related APIs. + +```ts +import { + useTable, + tableFeatures, + columnSizingFeature, + columnResizingFeature, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const table = useTable(() => ({ + features, + columns, + data, +})) +``` + +## Column Resizing (Ember) Guide + +TanStack Table provides built-in column resizing state and APIs for implementing column resizing in your table UI with a variety of options for UX and performance. + +Column resizing builds on column sizing. If you only need to define starting, minimum, or maximum widths, see the [Column Sizing Guide](./column-sizing). + +### Enable Column Resizing + +To use column resizing, add `columnSizingFeature` and then `columnResizingFeature` to your features. The `column.getCanResize()` API will return `true` by default for all columns, but you can either disable column resizing for all columns with the `enableColumnResizing` table option, or disable column resizing on a per-column basis with the `enableResizing` column option. + +```ts +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + useTable, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns = [ + { + accessorKey: 'id', + enableResizing: false, // disable resizing for just this column + size: 200, // starting column size + }, + //... +] + +const table = useTable(() => ({ + features, + columns, + data, +})) +``` + +### Column Resize Mode + +By default, the column resize mode is set to `"onEnd"`. This means that the `column.getSize()` API will not return the new column size until the user has finished resizing (dragging) the column. Usually a small UI indicator will be displayed while the user is resizing the column. + +In the Ember TanStack Table adapter, where achieving 60 fps column resizing renders can be difficult depending on the complexity of your table or web page, the `"onEnd"` column resize mode can be a good default option to avoid stuttering or lagging while the user resizes columns. That is not to say that you cannot achieve 60 fps column resizing renders while using TanStack Ember Table, but you may have to do some extra memoization or other performance optimizations to achieve this. + +> Advanced column resizing performance tips will be discussed [down below](#advanced-column-resizing-performance). + +If you want to change the column resize mode to `"onChange"` for immediate column resizing renders, you can do so with the `columnResizeMode` table option. + +```ts +const table = useTable(() => ({ + //... + columnResizeMode: 'onChange', // change column resize mode to "onChange" +})) +``` + +### Column Resize Direction + +By default, TanStack Table assumes that the table markup is laid out in a left-to-right direction. For right-to-left layouts, you may need to change the column resize direction to `"rtl"`. + +```ts +const table = useTable(() => ({ + //... + columnResizeDirection: 'rtl', // change column resize direction to "rtl" for certain locales +})) +``` + +### Connect Column Resizing APIs to UI + +There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI. + +#### Column Size APIs + +To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs: + +```ts +header.getSize() +column.getSize() +cell.column.getSize() +``` + +Because Ember templates extract method references without binding them, wrap these size reads in small module-scope helper functions, then call the helpers from your markup: + +```ts +const getHeaderSize = (header: Header): number => + header.getSize() + +const getCellColumnSize = (cell: Cell): number => + cell.column.getSize() +``` + +How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes. + +```hbs +
+``` + +Though, as discussed in the [advanced column resizing performance section](#advanced-column-resizing-performance), you may want to consider using CSS variables to apply column sizes to your markup. + +#### Column Resize APIs + +TanStack Table provides a pre-built event handler to make your drag interactions easy to implement. These event handlers are just convenience functions that call other internal APIs to update the column sizing state and re-render the table. Use `header.getResizeHandler()` to connect to your column resize drag interactions, for both mouse and touch events. + +```ts +const getResizeHandler = (header: Header) => { + return (event: Event) => header.getResizeHandler()?.(event) +} +``` + +```hbs +
+``` + +#### Column Resize Indicator with Column Resizing State + +TanStack Table keeps track of a `columnResizing` state object that you can use to render a column resize indicator UI. Use `column.getIsResizing()` to know when to show it, and read the transient drag offset from `table.store.state.columnResizing`. + +```ts +const getIsResizing = (column: Column): boolean => + column.getIsResizing() +``` + +```hbs +
+``` + +The `columnResizing` state stores transient drag information: + +```ts +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +You rarely need to manage this transient drag state yourself, but if you do, the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can observe the resize state without re-rendering the component that owns the table. + +```gts +import { useTable, createAtom } from '@tanstack/ember-table' +import type { columnResizingState } from '@tanstack/ember-table' + +export default class MyTable extends Component { + columnResizingAtom = createAtom({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, + }) + + table = useTable(() => ({ + features, + columns, + data: this.data, + atoms: { + columnResizing: this.columnResizingAtom, + }, + })) + + // read the atom wherever it is needed + get columnResizing() { + return this.columnResizingAtom.get() + } +} +``` + +Alternatively, the v8-style `state.columnResizing` plus `onColumnResizingChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```gts +export default class MyTable extends Component { + @tracked columnResizing: columnResizingState = { + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, + } + + table = useTable(() => ({ + features, + columns, + data: this.data, + state: { + columnResizing: this.columnResizing, + }, + onColumnResizingChange: (updater) => { + this.columnResizing = + typeof updater === 'function' ? updater(this.columnResizing) : updater + }, + })) +} +``` + +### Column Resizing APIs + +Use `header.getResizeHandler()` to connect mouse or touch events to the resizing logic. Use `column.getCanResize()` to decide whether to render a resize handle, and `column.getIsResizing()` to render active resizing UI. + +```ts +header.getResizeHandler() +column.getCanResize() +column.getIsResizing() +``` + +The table instance exposes APIs for the transient resize state through `table.setColumnResizing`. + +```ts +table.setColumnResizing((old) => ({ + ...old, + deltaOffset: 12, +})) + +table.resetHeaderSizeInfo() +table.resetHeaderSizeInfo(true) +``` + +### Advanced Column Resizing Performance + +If you are creating large or complex tables with Ember, an `"onChange"` resize can re-render the whole table on every drag frame, which degrades performance. The [performant column resizing example](../examples/column-resizing-performant) shows how to keep a drag off Ember's render path entirely, so even a table with expensive cells stays smooth. + +The idea is to stop reading a per-cell `getSize()` on every render during a drag: + +1. **Publish all column widths once as CSS variables on the table wrapper.** A single `@cached` getter reads `table.store.state.columnSizing` to establish reactivity, then iterates `table.getFlatHeaders()` to build a style string of `--header--size` and `--col--size` variables. +2. **Have each header and cell read its width from the matching variable by id.** Cells reference them with `width: calc(var(--col-firstName-size) * 1px)`, so the browser applies new widths with the variables already computed. (The core resize handler already coalesces pointer events to one update per animation frame.) +3. **Keep the resize indicator reading the narrow slice it needs.** Only the active resizer's highlight reads `column.getIsResizing()`, so a drag updates only those small pieces, not the whole table body. + +```gts +import { cached } from '@glimmer/tracking' + +const headerWidthStyle = (header: Header): string => + `width: calc(var(--header-${header.id}-size) * 1px)` + +const colWidthStyle = (cell: Cell): string => + `width: calc(var(--col-${cell.column.id}-size) * 1px)` + +export default class MyTable extends Component { + table = useTable(() => ({ + features, + columns, + data: this.data, + columnResizeMode: 'onChange' as const, + defaultColumn: { minSize: 60, maxSize: 800 }, + })) + + get totalSize() { + return this.table.getTotalSize() + } + + // Calculate all column sizes at once at the root table level and expose them + // as a CSS-variable style string applied to the table wrapper. Reading + // store.state.columnSizing establishes reactivity so the vars recompute on + // resize, while the individual header/cell widths simply read the variables. + @cached + get columnSizeVars(): string { + void this.table.store.state.columnSizing + const headers = this.table.getFlatHeaders() + const parts: Array = [] + let i = headers.length + while (--i >= 0) { + const header = headers[i]! + parts.push(`--header-${header.id}-size: ${header.getSize()}`) + parts.push(`--col-${header.column.id}-size: ${header.column.getSize()}`) + } + return parts.join('; ') + } + + get tableStyle(): string { + return `${this.columnSizeVars}; width: ${this.totalSize}px` + } +} +``` + +This replaces the older "memoize the table body while resizing" approach. Because the body reads widths from CSS variables rather than subscribing to resize state, it does not need to be memoized; it only re-renders when your data changes. diff --git a/docs/framework/ember/guide/column-sizing.md b/docs/framework/ember/guide/column-sizing.md new file mode 100644 index 0000000000..06ecd98246 --- /dev/null +++ b/docs/framework/ember/guide/column-sizing.md @@ -0,0 +1,177 @@ +--- +title: Column Sizing (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Column Sizing](../examples/column-sizing) + +### Column Sizing Setup + +Here's how you set up your table to use column sizing features. Adding the column sizing feature enables the related APIs. + +```ts +import { + useTable, + tableFeatures, + columnSizingFeature, +} from '@tanstack/ember-table' + +const features = tableFeatures({ columnSizingFeature }) + +const table = useTable(() => ({ + features, + columns, + data, +})) +``` + +## Column Sizing (Ember) Guide + +The column sizing feature lets you optionally set the width of each column including min and max widths. + +If you want users to dynamically change column widths by dragging column headers, see the [Column Resizing Guide](./column-resizing). + +### Column Widths + +Columns by default are given the following measurement options: + +```ts +export const defaultColumnSizing = { + size: 150, + minSize: 20, + maxSize: Number.MAX_SAFE_INTEGER, +} +``` + +These defaults can be overridden by both `tableOptions.defaultColumn` and individual column defs, in that order. + +```ts +const features = tableFeatures({ columnSizingFeature }) + +const columns = columnHelper.columns([ + columnHelper.accessor('col1', { + size: 270, // set column size for this column + }), + //... +]) + +const table = useTable(() => ({ + features, + defaultColumn: { + size: 200, // starting column size + minSize: 50, // enforced during column resizing + maxSize: 500, // enforced during column resizing + }, + //... +})) +``` + +The column "sizes" are stored in the table state as numbers, and are usually interpreted as pixel unit values, but you can hook up these column sizing values to your css styles however you see fit. + +As a headless utility, table logic for column sizing is really only a collection of states that you can apply to your own layouts how you see fit (our example above implements 2 styles of this logic). You can apply these width measurements in a variety of ways: + +- semantic `table` elements or any elements being displayed in a table css mode +- `div/span` elements or any elements being displayed in a non-table css mode + - Block level elements with strict widths + - Absolutely positioned elements with strict widths + - Flexbox positioned elements with loose widths + - Grid positioned elements with loose widths +- Really any layout mechanism that can interpolate cell widths into a table structure. + +Each of these approaches has its own tradeoffs and limitations which are usually opinions held by a UI/component library or design system, luckily not you 😉. + +### Column Sizing APIs + +Use the column and header APIs to read the calculated size and offsets for rendering. These values come from the `columnSizing` state and the column definition defaults. + +```ts +column.getSize() +header.getSize() + +column.getStart() // start offset in the current column flow +column.getStart('start') +column.getStart('center') +column.getStart('end') + +column.getAfter() // end offset in the current column flow +column.getAfter('start') +column.getAfter('center') +column.getAfter('end') + +column.resetSize() +``` + +The table instance also exposes total size helpers. These are useful when building scroll containers, split pinned-column tables, or CSS variables for column widths. + +```ts +table.getTotalSize() +table.getStartTotalSize() +table.getCenterTotalSize() +table.getEndTotalSize() +``` + +If you need to update sizing state directly, use `table.setColumnSizing`. Use `table.resetColumnSizing` to reset to `initialState.columnSizing`, or pass `true` to reset to the feature default. + +```ts +table.setColumnSizing({ + firstName: 180, + age: 80, +}) + +table.resetColumnSizing() +table.resetColumnSizing(true) +``` + +### Managing Column Sizing State + +If you need to own the `columnSizing` state yourself (for example, to persist user-set column widths), the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the sizing state without re-rendering the component that owns the table. + +```ts +import { + useTable, + tableFeatures, + columnSizingFeature, + createAtom, + type ColumnSizingState, +} from '@tanstack/ember-table' + +const features = tableFeatures({ columnSizingFeature }) + +const columnSizingAtom = createAtom({}) + +const columnSizing = columnSizingAtom.get() // read the atom wherever it is needed + +const table = useTable(() => ({ + features, + columns, + data, + atoms: { + columnSizing: columnSizingAtom, + }, +})) +``` + +Alternatively, the v8-style `state.columnSizing` plus `onColumnSizingChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnSizingFeature }) + +// inside your Glimmer component class +@tracked columnSizing: ColumnSizingState = {} + +table = useTable(() => ({ + features, + columns, + data: this.data, + state: { + columnSizing: this.columnSizing, + }, + onColumnSizingChange: (updater) => { + this.columnSizing = + typeof updater === 'function' ? updater(this.columnSizing) : updater + }, +})) +``` diff --git a/docs/framework/ember/guide/column-visibility.md b/docs/framework/ember/guide/column-visibility.md new file mode 100644 index 0000000000..ecedd3bf6e --- /dev/null +++ b/docs/framework/ember/guide/column-visibility.md @@ -0,0 +1,197 @@ +--- +title: Column Visibility (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Column Visibility](../examples/column-visibility) + +### Column Visibility Setup + +Here's how you set up your table to use column visibility features. Adding the column visibility feature enables the related APIs. + +```ts +import { + useTable, + tableFeatures, + columnVisibilityFeature, +} from '@tanstack/ember-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const table = useTable(() => ({ + features, + columns, + data, +})) +``` + +## Column Visibility (Ember) Guide + +The column visibility feature allows table columns to be hidden or shown dynamically. In v9, add `columnVisibilityFeature` to your `features` to enable this. There is a dedicated `columnVisibility` state and APIs for managing column visibility dynamically. + +### Column Visibility State + +The `columnVisibility` state is a map of column IDs to boolean values. A column will be hidden if its ID is present in the map and the value is `false`. If the column ID is not present in the map, or the value is `true`, the column will be shown. + +If you need to own the `columnVisibility` state yourself (for example, to persist user preferences), the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the visibility state without re-rendering the component that owns the table. + +```ts +import { + useTable, + tableFeatures, + columnVisibilityFeature, + createAtom, + type ColumnVisibilityState, +} from '@tanstack/ember-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const columnVisibilityAtom = createAtom({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +const columnVisibility = columnVisibilityAtom.get() // read the atom wherever it is needed + +const table = useTable(() => ({ + features, + //... + atoms: { + columnVisibility: columnVisibilityAtom, + }, +})) +``` + +Alternatively, the v8-style `state.columnVisibility` plus `onColumnVisibilityChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnVisibilityFeature }) + +// inside your Glimmer component class +@tracked columnVisibility: ColumnVisibilityState = { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +} + +table = useTable(() => ({ + features, + //... + state: { + columnVisibility: this.columnVisibility, + //... + }, + onColumnVisibilityChange: (updater) => { + this.columnVisibility = + typeof updater === 'function' ? updater(this.columnVisibility) : updater + }, +})) +``` + +Alternatively, if you don't need to manage the column visibility state outside of the table, you can still set the initial default column visibility state using the `initialState` option. + +> [!NOTE] +> If `columnVisibility` is provided to both `initialState` and `state`, the `state` initialization will take precedence and `initialState` will be ignored. Do not provide `columnVisibility` to both `initialState` and `state`, only one or the other. + +```ts +const features = tableFeatures({ columnVisibilityFeature }) + +const table = useTable(() => ({ + features, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +})) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```ts +const columns = columnHelper.columns([ + columnHelper.accessor('id', { + header: 'ID', + enableHiding: false, // disable hiding for this column + }), + columnHelper.accessor('name', { + header: 'Name', // can be hidden + }), +]) +``` + +### Column Visibility Toggle APIs + +There are several column API methods that are useful for rendering column visibility toggles in the UI. + +- `column.getCanHide` - Useful for disabling the visibility toggle for a column that has `enableHiding` set to `false`. +- `column.getIsVisible` - Useful for setting the initial state of the visibility toggle. +- `column.toggleVisibility` - Useful for toggling the visibility of a column. +- `column.getToggleVisibilityHandler` - Shortcut for hooking up the `column.toggleVisibility` method to a UI event handler. + +```ts +const getIsVisible = (column: Column): boolean => + column.getIsVisible() + +const getCanHide = (column: Column): boolean => + column.getCanHide() + +const not = (value: unknown): boolean => !value + +const toggleColumnVisibility = (column: Column) => { + return (event: Event) => { + column.getToggleVisibilityHandler()(event) + } +} +``` + +```hbs +{{#each this.allColumns as |column|}} + +{{/each}} +``` + +### Column Visibility Aware Table APIs + +When you render your header, body, and footer cells, there are a lot of API options available. You may see APIs like `table.getAllLeafColumns` and `row.getAllCells`, but if you use these APIs, they will not take column visibility into account. Instead, you need to use the "visible" variants of these APIs, such as `table.getVisibleLeafColumns` and `row.getVisibleCells`. + +```hbs + + + + {{#each this.visibleLeafColumns as |column|}} + {{! takes column visibility into account }} + {{/each}} + + + + {{#each this.rows as |row|}} + + {{#each (getVisibleCells row) as |cell|}} + {{! takes column visibility into account }} + {{/each}} + + {{/each}} + +
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/ember/guide/composable-tables.md b/docs/framework/ember/guide/composable-tables.md new file mode 100644 index 0000000000..aaf574230f --- /dev/null +++ b/docs/framework/ember/guide/composable-tables.md @@ -0,0 +1,158 @@ +--- +title: Composable Tables (createTableHook) Guide +--- + +`createTableHook` creates an app-specific table factory. Use it to define shared features, row models, and default table options once, then create each Ember table with the columns and data that are unique to that table. + +> [!NOTE] +> Unlike the React, Solid, Lit, and Svelte adapters, the Ember `createTableHook` does not register reusable cell/header/table components. Ember already renders cell, header, and footer content with the `FlexRenderCell`, `FlexRenderHeader`, and `FlexRenderFooter` components, so the hook is focused on sharing features and default options. You render an app table with the same components you use for a standalone `useTable` table. + +## Examples + +- [Basic App Table](../examples/basic-app-table) - Minimal `createTableHook` setup. + +## Start With Shared Features and Options + +Create one app table hook and put the feature set, row models, and shared defaults there. This example makes sorting available to every table created by `createAppTable`. + +```ts +import { + createSortedRowModel, + createTableHook, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { createAppTable, createAppColumnHelper } = createTableHook({ + features, + debugTable: true, + enableSortingRemoval: false, +}) +``` + +Options passed to `createTableHook` become defaults for every table created by `createAppTable`. The `features` option is also bound to the returned column helper, so column definitions know that sorting APIs are available. The hook also returns `appFeatures` (the feature set you passed in) if you need to reference it elsewhere. + +## Create App Columns + +Create one column helper per row type. The helper is already bound to your app's feature set, so each table does not need to thread `typeof features` through its column definitions. + +```ts +type Person = { + firstName: string + lastName: string + age: number + visits: number +} + +const columnHelper = createAppColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), +]) +``` + +## Create A Table + +Create each table with `createAppTable` inside a Glimmer component. Just like `useTable`, options are a thunk so any tracked property they read (such as `this.data`) keeps the table reactive. The call site provides table-specific inputs such as `columns` and `data`; shared features and defaults come from the hook, so you do not pass `features` again. + +```gts +import Component from '@glimmer/component' +import { tracked } from '@glimmer/tracking' + +export default class PeopleTable extends Component { + @tracked data: Array = [] + + table = createAppTable(() => ({ + columns, + data: this.data, + })) + + get headerGroups() { + return this.table.getHeaderGroups() + } + + get rows() { + return this.table.getRowModel().rows + } +} +``` + +## Render With The Normal Table APIs + +You render the table with the same table instance APIs and FlexRender components used by a standalone `useTable` table. As always in Ember templates, wrap `this`-bound method calls (like a sort toggle handler) in small module-level helper functions. + +```gts +import { on } from '@ember/modifier' +import { FlexRenderHeader, FlexRenderCell } from '@tanstack/ember-table' +import type { Column, Row, Cell } from '@tanstack/ember-table' + +const toggleSort = + (column: Column) => (event: Event) => + column.getToggleSortingHandler()?.(event) + +const getAllCells = ( + row: Row, +): Array> => row.getAllCells() + +// ...inside the component's