diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml index ae8e41d760..5a03c3e7d3 100644 --- a/_artifacts/domain_map.yaml +++ b/_artifacts/domain_map.yaml @@ -1,12 +1,13 @@ library: name: '@tanstack/table' - version: '9.0.0-beta.53' + version: '9.0.0-beta.55' 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/match-sorter-utils': '9.0.0-beta.53' meta: generated_by: '@tanstack/intent scaffold domain discovery' diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml index fe6aad1a91..9c4a4cabf0 100644 --- a/_artifacts/skill_tree.yaml +++ b/_artifacts/skill_tree.yaml @@ -1,9 +1,10 @@ library: name: '@tanstack/table' - version: '9.0.0-beta.53' + version: '9.0.0-beta.55' 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/match-sorter-utils': '9.0.0-beta.53' generated_from: domain_map: '_artifacts/domain_map.yaml' skill_spec: '_artifacts/skill_spec.md' diff --git a/docs/config.json b/docs/config.json index 653264a6fc..c447345253 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1062,6 +1062,7 @@ "children": [ { "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)" }, @@ -1088,6 +1089,7 @@ { "label": "Kitchen Sink (All Features)", "to": "framework/angular/examples/kitchen-sink" }, { "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" }, @@ -1114,6 +1116,7 @@ { "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": "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" }, @@ -1140,6 +1143,7 @@ { "label": "Kitchen Sink (All Features)", "to": "framework/lit/examples/kitchen-sink" }, { "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" }, @@ -1167,6 +1171,7 @@ { "label": "Kitchen Sink (All Features)", "to": "framework/react/examples/kitchen-sink" }, { "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" }, @@ -1195,6 +1200,7 @@ { "label": "Kitchen Sink (All Features)", "to": "framework/solid/examples/kitchen-sink" }, { "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" }, @@ -1221,6 +1227,7 @@ { "label": "Kitchen Sink (All Features)", "to": "framework/svelte/examples/kitchen-sink" }, { "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" }, @@ -1247,6 +1254,7 @@ { "label": "Kitchen Sink (All Features)", "to": "framework/vue/examples/kitchen-sink" }, { "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" }, @@ -1273,6 +1281,7 @@ { "label": "Kitchen Sink (All Features)", "to": "framework/preact/examples/kitchen-sink" }, { "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" }, diff --git a/docs/framework/alpine/guide/column-faceting.md b/docs/framework/alpine/guide/column-faceting.md index 5d945102c9..fc727e251b 100644 --- a/docs/framework/alpine/guide/column-faceting.md +++ b/docs/framework/alpine/guide/column-faceting.md @@ -7,6 +7,7 @@ title: Faceting (Alpine) Guide 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. @@ -48,176 +49,255 @@ const table = createTable({ ## Faceting (Alpine) 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. +### What is Faceting? -### Column Faceting Row Models +Faceting derives information that can be used to build filtering interfaces. For a given column, faceting can answer questions such as: -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. +- 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? -```ts -import { - columnFacetingFeature, - columnFilteringFeature, - createFacetedMinMaxValues, - createFacetedRowModel, - createFacetedUniqueValues, - createFilteredRowModel, - createTable, - filterFns, - tableFeatures, -} from '@tanstack/alpine-table' +For example, an application could use faceting to render a plan filter like this: -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, -}) - -const table = createTable({ - features, - columns, - get data() { - return local.data - }, -}) +```text +Plan +☐ Free 128 +☐ Pro 47 +☐ Enterprise 9 ``` -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. +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. -### Use Faceted Row Models +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. -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. `column.getFacetedUniqueValues()` returns a `Map` of value to count, so read its size with `.size` and its values with `.keys()`. +#### 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 -// list of unique values for autocomplete filter -const autoCompleteSuggestions = Array.from( - column.getFacetedUniqueValues().keys(), -) - .sort() - .slice(0, 5000) +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 -// tuple of min and max values for range filter -const [min, max] = column.getFacetedMinMaxValues() ?? [0, 1] +columnHelper.accessor('tags', { + header: 'Tags', + getUniqueValues: (row) => row.tags, +}) ``` -These reads are reactive inside Alpine bindings, so expose them as methods on your `Alpine.data` object and call them from the template. For example, a text filter can offer a faceted `` of unique values, while a numeric column can seed the `min`/`max` of a range input. +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', () => { - // ...createTable as above + const table = createTable({ + // ... + }) + return { table, - FlexRender, - isNumberColumn(column) { - const firstValue = table - .getPreFilteredRowModel() - .flatRows[0]?.getValue(column.id) - return typeof firstValue === 'number' - }, - uniqueCount(column) { - return column.getFacetedUniqueValues().size - }, - uniqueValues(column) { - return Array.from(column.getFacetedUniqueValues().keys()) - .sort() - .slice(0, 5000) + facetValues(column) { + return Array.from(column.getFacetedUniqueValues().entries()) }, - facetMin(column) { - return column.getFacetedMinMaxValues()?.[0] ?? '' + facetSelected(column, value) { + return (column.getFilterValue() ?? []).includes(value) }, - facetMax(column) { - return column.getFacetedMinMaxValues()?.[1] ?? '' + toggleFacet(column, value) { + const selected = column.getFilterValue() ?? [] + column.setFilterValue( + selected.includes(value) + ? selected.filter((item) => item !== value) + : [...selected, value], + ) }, } }) ``` ```html - -