diff --git a/.prettierignore b/.prettierignore index d49ba4fc8c..6ee0b18740 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ **/reference **/old-examples **/examples/**/*.svelte +**/test-results pnpm-lock.yaml docs/config.json diff --git a/README.md b/README.md index 689cb0a7df..38d5c7ded0 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) > - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) +> - [Ember Table](https://tanstack.com/table/alpha/docs/framework/ember/ember-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml index fd2e9e5466..88883e4f4b 100644 --- a/_artifacts/domain_map.yaml +++ b/_artifacts/domain_map.yaml @@ -31,7 +31,7 @@ scope: - 'Use docs and examples for intended workflows, and direct exact API discovery to the installed package src directory.' - 'Teach headless rendering, feature registration, row-model ownership, framework state, and v8-to-v9 changes as foundations.' included: - - 'All 16 public workspace packages.' + - 'All 17 public workspace packages.' - 'All 14 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: @@ -1271,6 +1271,62 @@ skills: - '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/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 diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md index 0ecf3d377b..d79320781d 100644 --- a/_artifacts/skill_spec.md +++ b/_artifacts/skill_spec.md @@ -9,7 +9,7 @@ This specification is the generation contract for a deliberately smaller, foot-g ## Outcome -Generate 72 short package-local skills across all 16 public packages. A loaded skill should quickly do three things: +Generate 75 short package-local skills across all 17 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. @@ -233,7 +233,13 @@ Alpine ships three: - table-state - create-table-hook -Do not add Query where no maintained adapter example exists. Do not add an Alpine migration skill because there is no v8 Alpine adapter journey to teach. Do not invent Preact virtualization examples; its Virtual skill should rely on the maintained adapter guide and installed APIs. +Ember ships three: + +- getting-started +- table-state +- create-table-hook + +Do not add Query where no maintained adapter example exists. Do not add Alpine or Ember migration skills because neither has a v8 adapter journey to teach. Do not add Ember 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) @@ -265,6 +271,7 @@ All Devtools skills must emphasize the required non-empty table options.key, lif | @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 | @@ -272,7 +279,7 @@ All Devtools skills must emphasize the required non-empty table options.key, lif | @tanstack/vue-table-devtools | 1 | | @tanstack/angular-table-devtools | 1 | | @tanstack/match-sorter-utils | 1 | -| Total | 72 | +| Total | 75 | ## Framework distinctions that must survive generation @@ -323,6 +330,14 @@ All Devtools skills must emphasize the required non-empty table options.key, lif - 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; selectors in table-state; CSS variables in resizing; measurement/overscan in Virtual; row ownership in client-vs-server. diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml index 50181df818..30d3785fcf 100644 --- a/_artifacts/skill_tree.yaml +++ b/_artifacts/skill_tree.yaml @@ -938,6 +938,51 @@ skills: - 'TanStack/table:examples/lit/virtualized-columns' - 'TanStack/table:examples/lit/virtualized-infinite-scrolling' + - 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 diff --git a/docs/config.json b/docs/config.json index 28afd10657..0b7a987f1e 100644 --- a/docs/config.json +++ b/docs/config.json @@ -28,6 +28,12 @@ { "label": "Rendering components", "to": "framework/angular/guide/rendering" } ] }, + { + "label": "ember", + "children": [ + { "label": "Quick Start", "to": "framework/ember/quick-start" } + ] + }, { "label": "lit", "children": [ @@ -121,6 +127,14 @@ { "label": "Custom Plugins", "to": "framework/angular/guide/custom-features" } ] }, + { + "label": "ember", + "children": [ + { "label": "Table State", "to": "framework/ember/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/ember/guide/composable-tables" }, + { "label": "Custom Plugins", "to": "framework/ember/guide/custom-features" } + ] + }, { "label": "lit", "children": [ @@ -224,6 +238,26 @@ { "label": "Virtualization", "to": "framework/angular/guide/virtualization" } ] }, + { + "label": "ember", + "children": [ + { "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": "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": "lit", "children": [ @@ -370,6 +404,12 @@ { "label": "Angular API Reference", "to": "framework/angular/reference/index" } ] }, + { + "label": "ember", + "children": [ + { "label": "Ember API Reference", "to": "framework/ember/reference/index" } + ] + }, { "label": "react", "children": [ @@ -473,6 +513,25 @@ { "label": "injectTableContext", "to": "framework/angular/reference/functions/injectTableContext" } ] }, + { + "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": "preact", "children": [ @@ -580,6 +639,12 @@ { "label": "AppColumnHelper", "to": "framework/angular/reference/type-aliases/AppColumnHelper" } ] }, + { + "label": "ember", + "children": [ + { "label": "AppColumnHelper", "to": "framework/ember/reference/type-aliases/AppColumnHelper" } + ] + }, { "label": "preact", "children": [ @@ -875,6 +940,16 @@ { "label": "Header Groups", "to": "framework/angular/examples/column-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/column-groups" } + ] + }, { "label": "lit", "children": [ @@ -1005,6 +1080,30 @@ { "label": "Sorting", "to": "framework/angular/examples/sorting" } ] }, + { + "label": "ember", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/ember/examples/kitchen-sink" }, + { "label": "Column Filters", "to": "framework/ember/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/ember/examples/filters-faceted" }, + { "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": "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": [ @@ -1188,6 +1287,15 @@ { "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": [ diff --git a/docs/framework/ember/guide/column-faceting.md b/docs/framework/ember/guide/column-faceting.md new file mode 100644 index 0000000000..ba3bafe3ef --- /dev/null +++ b/docs/framework/ember/guide/column-faceting.md @@ -0,0 +1,168 @@ +--- +title: Faceting (Ember) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Ember examples: + +- [Faceted Filters](../examples/filters-faceted) + +### Faceting Setup + +Here's how you set up your table to use faceting features. Adding the faceting feature enables the related APIs. Additionally, if using client-side faceting, you also need to set up `filteredRowModel` and `facetedRowModel` after their associated features because 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 + 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 + +Faceting is a feature that generates lists of values from your table's data, either for a single column (column faceting) or across the entire table (global faceting). For example, a list of unique values can be used as search suggestions in an autocomplete filter component, or a tuple of minimum and maximum values from a column of numbers can drive a range slider filter component. The same row models power both the per-column and table-wide APIs. + +### Column Faceting Row Models + +In order to use any of the column faceting features, add the `columnFacetingFeature` and the appropriate faceted row model factories to your features. Faceting exists to power filter UIs, so in practice you will also register the `columnFilteringFeature` and a `filteredRowModel`. Without a filtered row model, the faceted row models fall back to the pre-filtered rows and the facet values will not react to other columns' filters. + +```ts +import { + useTable, + tableFeatures, + columnFacetingFeature, + columnFilteringFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, + createFilteredRowModel, + filterFns, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // facet values react to other columns' filters + facetedRowModel: createFacetedRowModel(), // required for faceting (other faceted row models depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + filterFns, +}) + +// inside your Glimmer component +table = useTable(() => ({ + features, + columns, + data: this.data, +})) +``` + +First, you must include the `facetedRowModel`. This row model will generate a list of values for a given column. If you need a list of unique values, include the `facetedUniqueValues` row model. If you need a tuple of minimum and maximum values, include the `facetedMinMaxValues` row model. + +### Use Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting column instance APIs to access the lists of values generated by the faceted row models. + +```ts +// list of unique values for autocomplete filter +const autoCompleteSuggestions = Array.from( + column.getFacetedUniqueValues().keys(), +) + .sort() + .slice(0, 5000) +``` + +```ts +// tuple of min and max values for range filter +const [min, max] = column.getFacetedMinMaxValues() ?? [0, 1] +``` + +### Global Faceting + +The same `columnFacetingFeature` and faceted row models also power table-wide (global) faceting. Where column faceting derives values from a single column, global faceting derives values across all columns, which is useful for populating a global filter's autocomplete suggestions or a global range slider. If your table also uses global filtering, register the `globalFilteringFeature` so global facet values react to the active global filter. + +Use the global faceting table instance APIs to read the values: + +```ts +const globalFacetedRows = table.getGlobalFacetedRowModel().flatRows +``` + +```ts +// list of unique values for autocomplete filter +const autoCompleteSuggestions = Array.from( + table.getGlobalFacetedUniqueValues().keys(), +) + .sort() + .slice(0, 5000) +``` + +```ts +// tuple of min and max values for range filter +const [min, max] = table.getGlobalFacetedMinMaxValues() ?? [0, 1] +``` + +### Custom (Server-Side) Faceting + +Instead of using the built-in client-side faceting features, you can implement your own faceting logic on the server-side and pass the faceted values to the client-side. Supply custom `facetedUniqueValues` and `facetedMinMaxValues` factories in the features object. Each factory receives the table and a column ID and returns a thunk that resolves the faceted values. The column instance APIs (`column.getFacetedUniqueValues()` and `column.getFacetedMinMaxValues()`) will then return your server-provided values. + +```ts +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + const uniqueValueMap = new Map() + //... populate from your server data for this columnId + return uniqueValueMap + }, + facetedMinMaxValues: (_table, columnId) => () => { + //... read from your server data for this columnId + return [min, max] + }, +}) + +// inside your Glimmer component +table = useTable(() => ({ + features, + columns, + data: this.data, + //... +})) +``` + +The same factories also serve global faceting. Global faceting requests values with the internal `__global__` column ID, so you can branch on it inside the same `facetedUniqueValues` and `facetedMinMaxValues` factories on the features object to return table-wide facet values: + +```ts +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() // per-column facets + return new Map(globalFacets.uniqueValues) // global facets + }, +}) +``` + +Alternatively, you don't have to put any of your faceting logic through the TanStack Table APIs at all. Just fetch your lists and pass them to your filter components directly. diff --git a/docs/framework/ember/guide/column-filtering.md b/docs/framework/ember/guide/column-filtering.md new file mode 100644 index 0000000000..83d62558de --- /dev/null +++ b/docs/framework/ember/guide/column-filtering.md @@ -0,0 +1,431 @@ +--- +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) +- [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. Additionally, if using client-side filtering, you also need to set up `filteredRowModel` after its associated feature because row model slots are type-checked. + +```gts +import { + useTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + filterFns, +}) + +// inside your component class +table = useTable(() => ({ + features, + columns, + data: this.data, +})) +``` + +## 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 + +If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. + +However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. + +> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. + +Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: + +1. Can your server query all of the data in a reasonable amount of time (and cost)? +2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) +3. Is the client's browser using too much memory if all of the data is loaded at once? + +If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. + +### 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 `filterFns` from TanStack Table: + +```gts +import { + useTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/ember-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns, +}) + +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 retrieving 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 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 filter functions, either inline as the `filterFn` column option, or by name in the filter function registry that you pass to `createFilteredRowModel`. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +Whether you register a custom filter function in the registry passed to `createFilteredRowModel` 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: { + ...filterFns, + 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. + +- `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`. + +```ts +const startsWithFilterFn = < + TFeatures extends TableFeatures, + TData extends RowData, +>( + row: Row, + columnId: string, + filterValue: string, // resolveFilterValue below transforms the raw value to a string +) => + row + .getValue(columnId) + .toString() + .toLowerCase() + .trim() + .startsWith(filterValue) // toString, toLowerCase, and trim the filter value in `resolveFilterValue` + +// remove the filter value from filter state if it is falsy (empty string in this case) +startsWithFilterFn.autoRemove = (val: any) => !val + +// transform/sanitize/format the filter value before it is passed to the filter function +startsWithFilterFn.resolveFilterValue = (val: any) => + val.toString().toLowerCase().trim() +``` + +### 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, +}) + +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, +}) + +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..480c129034 --- /dev/null +++ b/docs/framework/ember/guide/column-ordering.md @@ -0,0 +1,184 @@ +--- +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..ef8775eac4 --- /dev/null +++ b/docs/framework/ember/guide/column-pinning.md @@ -0,0 +1,356 @@ +--- +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..27638ac42a --- /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 that allow you to easily implement 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 in order 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..96e60f029d --- /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 allows you to optionally specify 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..81920f8c9c --- /dev/null +++ b/docs/framework/ember/guide/column-visibility.md @@ -0,0 +1,196 @@ +--- +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