From 29bea700a0346e66ead755f259169937e155a479 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Tue, 9 Jun 2026 10:18:53 -0500 Subject: [PATCH 1/2] docs: re-organize feature guides to be framework specific --- docs/config.json | 189 ++++++- .../angular/guide/column-faceting.md | 116 +++++ .../angular/guide/column-filtering.md | 376 ++++++++++++++ .../angular/guide/column-ordering.md | 156 ++++++ .../framework/angular/guide/column-pinning.md | 169 ++++++ .../angular/guide/column-resizing.md | 224 ++++++++ docs/framework/angular/guide/column-sizing.md | 125 +++++ .../angular/guide/column-visibility.md | 150 ++++++ .../angular/guide/custom-features.md | 293 +++++++++++ docs/framework/angular/guide/expanding.md | 283 ++++++++++ .../angular/guide/fuzzy-filtering.md | 156 ++++++ .../angular/guide/global-faceting.md | 112 ++++ .../angular/guide/global-filtering.md | 244 +++++++++ docs/framework/angular/guide/grouping.md | 232 +++++++++ docs/framework/angular/guide/pagination.md | 247 +++++++++ docs/framework/angular/guide/row-pinning.md | 219 ++++++++ docs/framework/angular/guide/row-selection.md | 201 ++++++++ docs/framework/angular/guide/sorting.md | 473 +++++++++++++++++ .../framework/angular/guide/virtualization.md | 233 +++++++++ docs/framework/lit/guide/column-faceting.md | 125 +++++ docs/framework/lit/guide/column-filtering.md | 385 ++++++++++++++ docs/framework/lit/guide/column-ordering.md | 168 ++++++ docs/framework/lit/guide/column-pinning.md | 178 +++++++ docs/framework/lit/guide/column-resizing.md | 237 +++++++++ docs/framework/lit/guide/column-sizing.md | 134 +++++ docs/framework/lit/guide/column-visibility.md | 161 ++++++ docs/framework/lit/guide/custom-features.md | 290 +++++++++++ docs/framework/lit/guide/expanding.md | 293 +++++++++++ docs/framework/lit/guide/fuzzy-filtering.md | 165 ++++++ docs/framework/lit/guide/global-faceting.md | 121 +++++ docs/framework/lit/guide/global-filtering.md | 256 ++++++++++ docs/framework/lit/guide/grouping.md | 241 +++++++++ docs/framework/lit/guide/pagination.md | 251 +++++++++ docs/framework/lit/guide/row-pinning.md | 220 ++++++++ docs/framework/lit/guide/row-selection.md | 212 ++++++++ docs/framework/lit/guide/sorting.md | 482 ++++++++++++++++++ docs/framework/lit/guide/virtualization.md | 229 +++++++++ .../framework/preact/guide/column-faceting.md | 111 ++++ .../preact/guide/column-filtering.md | 368 +++++++++++++ .../framework/preact/guide/column-ordering.md | 148 ++++++ docs/framework/preact/guide/column-pinning.md | 161 ++++++ .../framework/preact/guide/column-resizing.md | 216 ++++++++ docs/framework/preact/guide/column-sizing.md | 120 +++++ .../preact/guide/column-visibility.md | 140 +++++ .../framework/preact/guide/custom-features.md | 318 ++++++++++++ docs/framework/preact/guide/expanding.md | 289 +++++++++++ .../framework/preact/guide/fuzzy-filtering.md | 151 ++++++ .../framework/preact/guide/global-faceting.md | 107 ++++ .../preact/guide/global-filtering.md | 239 +++++++++ docs/framework/preact/guide/grouping.md | 224 ++++++++ docs/framework/preact/guide/pagination.md | 258 ++++++++++ docs/framework/preact/guide/row-pinning.md | 219 ++++++++ docs/framework/preact/guide/row-selection.md | 200 ++++++++ docs/framework/preact/guide/sorting.md | 462 +++++++++++++++++ docs/framework/preact/guide/virtualization.md | 225 ++++++++ .../react}/guide/column-faceting.md | 50 +- .../react}/guide/column-filtering.md | 80 +-- .../react}/guide/column-ordering.md | 78 +-- .../react}/guide/column-pinning.md | 121 +++-- .../react}/guide/column-resizing.md | 125 +++-- .../react}/guide/column-sizing.md | 88 ++-- .../react}/guide/column-visibility.md | 56 +- .../react}/guide/custom-features.md | 27 +- docs/{ => framework/react}/guide/expanding.md | 94 ++-- .../react}/guide/fuzzy-filtering.md | 52 +- docs/framework/react/guide/global-faceting.md | 107 ++++ .../react}/guide/global-filtering.md | 81 +-- docs/{ => framework/react}/guide/grouping.md | 87 ++-- .../{ => framework/react}/guide/pagination.md | 74 +-- docs/framework/react/guide/row-pinning.md | 219 ++++++++ .../react}/guide/row-selection.md | 47 +- docs/{ => framework/react}/guide/sorting.md | 77 ++- docs/framework/react/guide/virtualization.md | 302 +++++++++++ docs/framework/solid/guide/column-faceting.md | 115 +++++ .../framework/solid/guide/column-filtering.md | 372 ++++++++++++++ docs/framework/solid/guide/column-ordering.md | 152 ++++++ docs/framework/solid/guide/column-pinning.md | 165 ++++++ docs/framework/solid/guide/column-resizing.md | 220 ++++++++ docs/framework/solid/guide/column-sizing.md | 124 +++++ .../solid/guide/column-visibility.md | 152 ++++++ docs/framework/solid/guide/custom-features.md | 315 ++++++++++++ docs/framework/solid/guide/expanding.md | 279 ++++++++++ docs/framework/solid/guide/fuzzy-filtering.md | 155 ++++++ docs/framework/solid/guide/global-faceting.md | 111 ++++ .../framework/solid/guide/global-filtering.md | 243 +++++++++ docs/framework/solid/guide/grouping.md | 228 +++++++++ docs/framework/solid/guide/pagination.md | 260 ++++++++++ docs/framework/solid/guide/row-pinning.md | 223 ++++++++ docs/framework/solid/guide/row-selection.md | 201 ++++++++ docs/framework/solid/guide/sorting.md | 466 +++++++++++++++++ docs/framework/solid/guide/virtualization.md | 239 +++++++++ .../framework/svelte/guide/column-faceting.md | 115 +++++ .../svelte/guide/column-filtering.md | 377 ++++++++++++++ .../framework/svelte/guide/column-ordering.md | 160 ++++++ docs/framework/svelte/guide/column-pinning.md | 171 +++++++ .../framework/svelte/guide/column-resizing.md | 223 ++++++++ docs/framework/svelte/guide/column-sizing.md | 124 +++++ .../svelte/guide/column-visibility.md | 151 ++++++ .../framework/svelte/guide/custom-features.md | 290 +++++++++++ docs/framework/svelte/guide/expanding.md | 281 ++++++++++ .../framework/svelte/guide/fuzzy-filtering.md | 155 ++++++ .../framework/svelte/guide/global-faceting.md | 111 ++++ .../svelte/guide/global-filtering.md | 247 +++++++++ docs/framework/svelte/guide/grouping.md | 232 +++++++++ docs/framework/svelte/guide/pagination.md | 241 +++++++++ docs/framework/svelte/guide/row-pinning.md | 213 ++++++++ docs/framework/svelte/guide/row-selection.md | 202 ++++++++ docs/framework/svelte/guide/sorting.md | 470 +++++++++++++++++ docs/framework/svelte/guide/virtualization.md | 239 +++++++++ docs/framework/vanilla/guide/pagination.md | 262 ++++++++++ docs/framework/vanilla/guide/sorting.md | 471 +++++++++++++++++ docs/framework/vue/guide/column-faceting.md | 113 ++++ docs/framework/vue/guide/column-filtering.md | 376 ++++++++++++++ docs/framework/vue/guide/column-ordering.md | 156 ++++++ docs/framework/vue/guide/column-pinning.md | 169 ++++++ docs/framework/vue/guide/column-resizing.md | 224 ++++++++ docs/framework/vue/guide/column-sizing.md | 122 +++++ docs/framework/vue/guide/column-visibility.md | 144 ++++++ docs/framework/vue/guide/custom-features.md | 295 +++++++++++ docs/framework/vue/guide/expanding.md | 278 ++++++++++ docs/framework/vue/guide/fuzzy-filtering.md | 153 ++++++ docs/framework/vue/guide/global-faceting.md | 109 ++++ docs/framework/vue/guide/global-filtering.md | 248 +++++++++ docs/framework/vue/guide/grouping.md | 232 +++++++++ docs/framework/vue/guide/pagination.md | 247 +++++++++ docs/framework/vue/guide/row-pinning.md | 211 ++++++++ docs/framework/vue/guide/row-selection.md | 199 ++++++++ docs/framework/vue/guide/sorting.md | 476 +++++++++++++++++ docs/framework/vue/guide/virtualization.md | 250 +++++++++ docs/guide/column-defs.md | 2 +- docs/guide/data.md | 4 +- docs/guide/features.md | 170 +++++- docs/guide/filters.md | 63 ++- docs/guide/global-faceting.md | 104 ---- docs/guide/headers.md | 2 +- docs/guide/pinning.md | 39 +- docs/guide/row-pinning.md | 46 -- docs/guide/rows.md | 2 +- docs/guide/virtualization.md | 53 -- docs/overview.md | 156 +++++- 140 files changed, 26613 insertions(+), 852 deletions(-) create mode 100644 docs/framework/angular/guide/column-faceting.md create mode 100644 docs/framework/angular/guide/column-filtering.md create mode 100644 docs/framework/angular/guide/column-ordering.md create mode 100644 docs/framework/angular/guide/column-pinning.md create mode 100644 docs/framework/angular/guide/column-resizing.md create mode 100644 docs/framework/angular/guide/column-sizing.md create mode 100644 docs/framework/angular/guide/column-visibility.md create mode 100644 docs/framework/angular/guide/custom-features.md create mode 100644 docs/framework/angular/guide/expanding.md create mode 100644 docs/framework/angular/guide/fuzzy-filtering.md create mode 100644 docs/framework/angular/guide/global-faceting.md create mode 100644 docs/framework/angular/guide/global-filtering.md create mode 100644 docs/framework/angular/guide/grouping.md create mode 100644 docs/framework/angular/guide/pagination.md create mode 100644 docs/framework/angular/guide/row-pinning.md create mode 100644 docs/framework/angular/guide/row-selection.md create mode 100644 docs/framework/angular/guide/sorting.md create mode 100644 docs/framework/angular/guide/virtualization.md create mode 100644 docs/framework/lit/guide/column-faceting.md create mode 100644 docs/framework/lit/guide/column-filtering.md create mode 100644 docs/framework/lit/guide/column-ordering.md create mode 100644 docs/framework/lit/guide/column-pinning.md create mode 100644 docs/framework/lit/guide/column-resizing.md create mode 100644 docs/framework/lit/guide/column-sizing.md create mode 100644 docs/framework/lit/guide/column-visibility.md create mode 100644 docs/framework/lit/guide/custom-features.md create mode 100644 docs/framework/lit/guide/expanding.md create mode 100644 docs/framework/lit/guide/fuzzy-filtering.md create mode 100644 docs/framework/lit/guide/global-faceting.md create mode 100644 docs/framework/lit/guide/global-filtering.md create mode 100644 docs/framework/lit/guide/grouping.md create mode 100644 docs/framework/lit/guide/pagination.md create mode 100644 docs/framework/lit/guide/row-pinning.md create mode 100644 docs/framework/lit/guide/row-selection.md create mode 100644 docs/framework/lit/guide/sorting.md create mode 100644 docs/framework/lit/guide/virtualization.md create mode 100644 docs/framework/preact/guide/column-faceting.md create mode 100644 docs/framework/preact/guide/column-filtering.md create mode 100644 docs/framework/preact/guide/column-ordering.md create mode 100644 docs/framework/preact/guide/column-pinning.md create mode 100644 docs/framework/preact/guide/column-resizing.md create mode 100644 docs/framework/preact/guide/column-sizing.md create mode 100644 docs/framework/preact/guide/column-visibility.md create mode 100644 docs/framework/preact/guide/custom-features.md create mode 100644 docs/framework/preact/guide/expanding.md create mode 100644 docs/framework/preact/guide/fuzzy-filtering.md create mode 100644 docs/framework/preact/guide/global-faceting.md create mode 100644 docs/framework/preact/guide/global-filtering.md create mode 100644 docs/framework/preact/guide/grouping.md create mode 100644 docs/framework/preact/guide/pagination.md create mode 100644 docs/framework/preact/guide/row-pinning.md create mode 100644 docs/framework/preact/guide/row-selection.md create mode 100644 docs/framework/preact/guide/sorting.md create mode 100644 docs/framework/preact/guide/virtualization.md rename docs/{ => framework/react}/guide/column-faceting.md (81%) rename docs/{ => framework/react}/guide/column-filtering.md (90%) rename docs/{ => framework/react}/guide/column-ordering.md (78%) rename docs/{ => framework/react}/guide/column-pinning.md (63%) rename docs/{ => framework/react}/guide/column-resizing.md (63%) rename docs/{ => framework/react}/guide/column-sizing.md (57%) rename docs/{ => framework/react}/guide/column-visibility.md (83%) rename docs/{ => framework/react}/guide/custom-features.md (95%) rename docs/{ => framework/react}/guide/expanding.md (85%) rename docs/{ => framework/react}/guide/fuzzy-filtering.md (86%) create mode 100644 docs/framework/react/guide/global-faceting.md rename docs/{ => framework/react}/guide/global-filtering.md (84%) rename docs/{ => framework/react}/guide/grouping.md (83%) rename docs/{ => framework/react}/guide/pagination.md (86%) create mode 100644 docs/framework/react/guide/row-pinning.md rename docs/{ => framework/react}/guide/row-selection.md (90%) rename docs/{ => framework/react}/guide/sorting.md (96%) create mode 100644 docs/framework/react/guide/virtualization.md create mode 100644 docs/framework/solid/guide/column-faceting.md create mode 100644 docs/framework/solid/guide/column-filtering.md create mode 100644 docs/framework/solid/guide/column-ordering.md create mode 100644 docs/framework/solid/guide/column-pinning.md create mode 100644 docs/framework/solid/guide/column-resizing.md create mode 100644 docs/framework/solid/guide/column-sizing.md create mode 100644 docs/framework/solid/guide/column-visibility.md create mode 100644 docs/framework/solid/guide/custom-features.md create mode 100644 docs/framework/solid/guide/expanding.md create mode 100644 docs/framework/solid/guide/fuzzy-filtering.md create mode 100644 docs/framework/solid/guide/global-faceting.md create mode 100644 docs/framework/solid/guide/global-filtering.md create mode 100644 docs/framework/solid/guide/grouping.md create mode 100644 docs/framework/solid/guide/pagination.md create mode 100644 docs/framework/solid/guide/row-pinning.md create mode 100644 docs/framework/solid/guide/row-selection.md create mode 100644 docs/framework/solid/guide/sorting.md create mode 100644 docs/framework/solid/guide/virtualization.md create mode 100644 docs/framework/svelte/guide/column-faceting.md create mode 100644 docs/framework/svelte/guide/column-filtering.md create mode 100644 docs/framework/svelte/guide/column-ordering.md create mode 100644 docs/framework/svelte/guide/column-pinning.md create mode 100644 docs/framework/svelte/guide/column-resizing.md create mode 100644 docs/framework/svelte/guide/column-sizing.md create mode 100644 docs/framework/svelte/guide/column-visibility.md create mode 100644 docs/framework/svelte/guide/custom-features.md create mode 100644 docs/framework/svelte/guide/expanding.md create mode 100644 docs/framework/svelte/guide/fuzzy-filtering.md create mode 100644 docs/framework/svelte/guide/global-faceting.md create mode 100644 docs/framework/svelte/guide/global-filtering.md create mode 100644 docs/framework/svelte/guide/grouping.md create mode 100644 docs/framework/svelte/guide/pagination.md create mode 100644 docs/framework/svelte/guide/row-pinning.md create mode 100644 docs/framework/svelte/guide/row-selection.md create mode 100644 docs/framework/svelte/guide/sorting.md create mode 100644 docs/framework/svelte/guide/virtualization.md create mode 100644 docs/framework/vanilla/guide/pagination.md create mode 100644 docs/framework/vanilla/guide/sorting.md create mode 100644 docs/framework/vue/guide/column-faceting.md create mode 100644 docs/framework/vue/guide/column-filtering.md create mode 100644 docs/framework/vue/guide/column-ordering.md create mode 100644 docs/framework/vue/guide/column-pinning.md create mode 100644 docs/framework/vue/guide/column-resizing.md create mode 100644 docs/framework/vue/guide/column-sizing.md create mode 100644 docs/framework/vue/guide/column-visibility.md create mode 100644 docs/framework/vue/guide/custom-features.md create mode 100644 docs/framework/vue/guide/expanding.md create mode 100644 docs/framework/vue/guide/fuzzy-filtering.md create mode 100644 docs/framework/vue/guide/global-faceting.md create mode 100644 docs/framework/vue/guide/global-filtering.md create mode 100644 docs/framework/vue/guide/grouping.md create mode 100644 docs/framework/vue/guide/pagination.md create mode 100644 docs/framework/vue/guide/row-pinning.md create mode 100644 docs/framework/vue/guide/row-selection.md create mode 100644 docs/framework/vue/guide/sorting.md create mode 100644 docs/framework/vue/guide/virtualization.md delete mode 100644 docs/guide/global-faceting.md delete mode 100644 docs/guide/row-pinning.md delete mode 100644 docs/guide/virtualization.md diff --git a/docs/config.json b/docs/config.json index ab3493dece..d9965d0e3f 100644 --- a/docs/config.json +++ b/docs/config.json @@ -143,25 +143,176 @@ }, { "label": "Feature Guides", - "children": [ - { "label": "Column Ordering", "to": "guide/column-ordering" }, - { "label": "Column Pinning", "to": "guide/column-pinning" }, - { "label": "Column Sizing", "to": "guide/column-sizing" }, - { "label": "Column Resizing", "to": "guide/column-resizing" }, - { "label": "Column Visibility", "to": "guide/column-visibility" }, - { "label": "Column Filtering", "to": "guide/column-filtering" }, - { "label": "Global Filtering", "to": "guide/global-filtering" }, - { "label": "Fuzzy Filtering", "to": "guide/fuzzy-filtering" }, - { "label": "Column Faceting", "to": "guide/column-faceting" }, - { "label": "Global Faceting", "to": "guide/global-faceting" }, - { "label": "Grouping", "to": "guide/grouping" }, - { "label": "Expanding", "to": "guide/expanding" }, - { "label": "Pagination", "to": "guide/pagination" }, - { "label": "Row Pinning", "to": "guide/row-pinning" }, - { "label": "Row Selection", "to": "guide/row-selection" }, - { "label": "Sorting", "to": "guide/sorting" }, - { "label": "Virtualization", "to": "guide/virtualization" }, - { "label": "Custom Features", "to": "guide/custom-features" } + "children": [], + "frameworks": [ + { + "label": "angular", + "children": [ + { "label": "Column Ordering", "to": "framework/angular/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/angular/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/angular/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/angular/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/angular/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/angular/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/angular/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/angular/guide/fuzzy-filtering" }, + { "label": "Column Faceting", "to": "framework/angular/guide/column-faceting" }, + { "label": "Global Faceting", "to": "framework/angular/guide/global-faceting" }, + { "label": "Grouping", "to": "framework/angular/guide/grouping" }, + { "label": "Expanding", "to": "framework/angular/guide/expanding" }, + { "label": "Pagination", "to": "framework/angular/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/angular/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/angular/guide/row-selection" }, + { "label": "Sorting", "to": "framework/angular/guide/sorting" }, + { "label": "Virtualization", "to": "framework/angular/guide/virtualization" }, + { "label": "Custom Features", "to": "framework/angular/guide/custom-features" } + ] + }, + { + "label": "lit", + "children": [ + { "label": "Column Ordering", "to": "framework/lit/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/lit/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/lit/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/lit/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/lit/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/lit/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/lit/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/lit/guide/fuzzy-filtering" }, + { "label": "Column Faceting", "to": "framework/lit/guide/column-faceting" }, + { "label": "Global Faceting", "to": "framework/lit/guide/global-faceting" }, + { "label": "Grouping", "to": "framework/lit/guide/grouping" }, + { "label": "Expanding", "to": "framework/lit/guide/expanding" }, + { "label": "Pagination", "to": "framework/lit/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/lit/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/lit/guide/row-selection" }, + { "label": "Sorting", "to": "framework/lit/guide/sorting" }, + { "label": "Virtualization", "to": "framework/lit/guide/virtualization" }, + { "label": "Custom Features", "to": "framework/lit/guide/custom-features" } + ] + }, + { + "label": "react", + "children": [ + { "label": "Column Ordering", "to": "framework/react/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/react/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/react/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/react/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/react/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/react/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/react/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/react/guide/fuzzy-filtering" }, + { "label": "Column Faceting", "to": "framework/react/guide/column-faceting" }, + { "label": "Global Faceting", "to": "framework/react/guide/global-faceting" }, + { "label": "Grouping", "to": "framework/react/guide/grouping" }, + { "label": "Expanding", "to": "framework/react/guide/expanding" }, + { "label": "Pagination", "to": "framework/react/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/react/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/react/guide/row-selection" }, + { "label": "Sorting", "to": "framework/react/guide/sorting" }, + { "label": "Virtualization", "to": "framework/react/guide/virtualization" }, + { "label": "Custom Features", "to": "framework/react/guide/custom-features" } + ] + }, + { + "label": "preact", + "children": [ + { "label": "Column Ordering", "to": "framework/preact/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/preact/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/preact/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/preact/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/preact/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/preact/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/preact/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/preact/guide/fuzzy-filtering" }, + { "label": "Column Faceting", "to": "framework/preact/guide/column-faceting" }, + { "label": "Global Faceting", "to": "framework/preact/guide/global-faceting" }, + { "label": "Grouping", "to": "framework/preact/guide/grouping" }, + { "label": "Expanding", "to": "framework/preact/guide/expanding" }, + { "label": "Pagination", "to": "framework/preact/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/preact/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/preact/guide/row-selection" }, + { "label": "Sorting", "to": "framework/preact/guide/sorting" }, + { "label": "Virtualization", "to": "framework/preact/guide/virtualization" }, + { "label": "Custom Features", "to": "framework/preact/guide/custom-features" } + ] + }, + { + "label": "solid", + "children": [ + { "label": "Column Ordering", "to": "framework/solid/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/solid/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/solid/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/solid/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/solid/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/solid/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/solid/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/solid/guide/fuzzy-filtering" }, + { "label": "Column Faceting", "to": "framework/solid/guide/column-faceting" }, + { "label": "Global Faceting", "to": "framework/solid/guide/global-faceting" }, + { "label": "Grouping", "to": "framework/solid/guide/grouping" }, + { "label": "Expanding", "to": "framework/solid/guide/expanding" }, + { "label": "Pagination", "to": "framework/solid/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/solid/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/solid/guide/row-selection" }, + { "label": "Sorting", "to": "framework/solid/guide/sorting" }, + { "label": "Virtualization", "to": "framework/solid/guide/virtualization" }, + { "label": "Custom Features", "to": "framework/solid/guide/custom-features" } + ] + }, + { + "label": "svelte", + "children": [ + { "label": "Column Ordering", "to": "framework/svelte/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/svelte/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/svelte/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/svelte/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/svelte/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/svelte/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/svelte/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/svelte/guide/fuzzy-filtering" }, + { "label": "Column Faceting", "to": "framework/svelte/guide/column-faceting" }, + { "label": "Global Faceting", "to": "framework/svelte/guide/global-faceting" }, + { "label": "Grouping", "to": "framework/svelte/guide/grouping" }, + { "label": "Expanding", "to": "framework/svelte/guide/expanding" }, + { "label": "Pagination", "to": "framework/svelte/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/svelte/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/svelte/guide/row-selection" }, + { "label": "Sorting", "to": "framework/svelte/guide/sorting" }, + { "label": "Virtualization", "to": "framework/svelte/guide/virtualization" }, + { "label": "Custom Features", "to": "framework/svelte/guide/custom-features" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "Column Ordering", "to": "framework/vue/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/vue/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/vue/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/vue/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/vue/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/vue/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/vue/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/vue/guide/fuzzy-filtering" }, + { "label": "Column Faceting", "to": "framework/vue/guide/column-faceting" }, + { "label": "Global Faceting", "to": "framework/vue/guide/global-faceting" }, + { "label": "Grouping", "to": "framework/vue/guide/grouping" }, + { "label": "Expanding", "to": "framework/vue/guide/expanding" }, + { "label": "Pagination", "to": "framework/vue/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/vue/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/vue/guide/row-selection" }, + { "label": "Sorting", "to": "framework/vue/guide/sorting" }, + { "label": "Virtualization", "to": "framework/vue/guide/virtualization" }, + { "label": "Custom Features", "to": "framework/vue/guide/custom-features" } + ] + }, + { + "label": "vanilla", + "children": [ + { "label": "Pagination", "to": "framework/vanilla/guide/pagination" }, + { "label": "Sorting", "to": "framework/vanilla/guide/sorting" } + ] + } ] }, { diff --git a/docs/framework/angular/guide/column-faceting.md b/docs/framework/angular/guide/column-faceting.md new file mode 100644 index 0000000000..72ace92ee1 --- /dev/null +++ b/docs/framework/angular/guide/column-faceting.md @@ -0,0 +1,116 @@ +--- +title: Column Faceting (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Faceted Filters](../examples/filters-faceted) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/angular-table' + +const features = tableFeatures({ columnFacetingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data: this.data(), + })) +} +``` + +## Column Faceting (Angular) Guide + +Column Faceting is a feature that allows you to generate lists of values for a given column from that column's data. For example, a list of unique values in a column can be generated from all rows in that column to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a column of numbers to be used as a range for a range slider filter component. + +### Column Faceting Row Models + +In order to use any of the column faceting features, add the `columnFacetingFeature` to your features and the appropriate faceted row models to `rowModels`. + +```ts +import { + injectTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnFacetingFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + 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 + }, + columns, + 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]; +``` + +### Custom (Server-Side) Faceting + +If 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. You can use the `getFacetedUniqueValues` and `getFacetedMinMaxValues` table options to resolve the faceted values from the server-side. + +```ts +const facetingQuery = useQuery( + //... +) + +readonly table = injectTable(() => ({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, + getFacetedUniqueValues: (table, columnId) => { + const uniqueValueMap = new Map() + //... + return uniqueValueMap + }, + getFacetedMinMaxValues: (table, columnId) => { + //... + return [min, max] + }, + //... +}) +``` + +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/angular/guide/column-filtering.md b/docs/framework/angular/guide/column-filtering.md new file mode 100644 index 0000000000..7d107f3fa1 --- /dev/null +++ b/docs/framework/angular/guide/column-filtering.md @@ -0,0 +1,376 @@ +--- +title: Column Filtering (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Fuzzy Search](../examples/filters-fuzzy) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/angular-table' + +const features = tableFeatures({ columnFilteringFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data: this.data(), + })) +} +``` + +## Column Filtering (Angular) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on column filtering, which is a filter that is applied to a single column's accessor value. + +TanStack table supports both client-side and manual server-side filtering. This guide will go over how to implement and customize both, and help you decide which one is best for your use-case. + +### Client-Side vs Server-Side Filtering + +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 `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +readonly table = injectTable(() => ({ + features: tableFeatures({ columnFilteringFeature }), + rowModels: {}, // no filteredRowModel needed for manual server-side filtering + 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` to your features and the `filteredRowModel` to your row models. Import `createFilteredRowModel` and `filterFns` from TanStack Table: + +```ts +import { + injectTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnFilteringFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + 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 + +You can access the column filter state from the table instance with `table.atoms.columnFilters.get()` or from the current `table.store.state` snapshot. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.columnFilters.get()) // access the current column filters state +``` + +However, if you need to access the column filter state before the table is initialized, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state, you can control/manage the column filter state in your own state management with the `state.columnFilters` and `onColumnFiltersChange` table options. + +```ts +readonly columnFilters = signal([]) // can set initial column filter state here +//... +readonly table = injectTable(() => ({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + state: { + columnFilters: this.columnFilters(), + }, + onColumnFiltersChange: (updater) => + typeof updater === 'function' + ? this.columnFilters.update(updater) + : this.columnFilters.set(updater), +}) +``` + +#### Initial Column Filter State + +If you do not need to control the column filter state in your own state management or scope, but you still want to set an initial column filter state, you can use the `initialState` table option instead of `state`. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 10 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `arrIncludes` - Item inclusion within an array +- `arrIncludesAll` - All items included in an array +- `arrIncludesSome` - Some items included in an array +- `equals` - Object/referential equality `Object.is`/`===` +- `weakEquals` - Weak object/referential equality `==` +- `inNumberRange` - Number range inclusion + +You can also define your own custom filter functions either as the `filterFn` column option, or as a global filter function using the `filterFns` table option. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +When defining a custom filter function in either the `filterFn` column option or the `filterFns` table option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = (row: Row, columnId: string, filterValue: any, addMeta: (meta: any) => void) => boolean +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // use custom global filter function + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + } +] +//... +readonly table = injectTable(() => ({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }), + }, + columns, + data, +}) +``` + +##### 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 = ( + row: Row, + columnId: string, + filterValue: number | string, //resolveFilterValue will transform this 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 = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + enableColumnFilters: false, // disable column filtering for all columns +}) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```ts +readonly table = injectTable(() => ({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + filterFromLeafRows: true, // filter and search through sub-rows +}) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```ts +readonly table = injectTable(() => ({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +}) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `onChange` or `onBlur` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/angular/guide/column-ordering.md b/docs/framework/angular/guide/column-ordering.md new file mode 100644 index 0000000000..7c5abee23b --- /dev/null +++ b/docs/framework/angular/guide/column-ordering.md @@ -0,0 +1,156 @@ +--- +title: Column Ordering (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Ordering](../examples/column-ordering) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnOrderingFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ columnOrderingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data: this.data(), + })) +} +``` + +## Column Ordering (Angular) Guide + +By default, columns are ordered in the order they are defined in the `columns` array. However, you can manually specify the column order using the `columnOrder` state. Other features like column pinning and grouping can also affect the column order. + +### What Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into left, center (unpinned), and right 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 +readonly table = injectTable(() => ({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + 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. + +```ts +readonly columnOrder = signal(['columnId1', 'columnId2', 'columnId3']) +//... +readonly table = injectTable(() => ({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + state: { + columnOrder: this.columnOrder(), + //... + } + onColumnOrderChange: (updater) => + typeof updater === 'function' + ? this.columnOrder.update(updater) + : this.columnOrder.set(updater), + //... +}); +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, you can set up the logic something like this: + +```ts +readonly columnOrder = signal(columns.map(c => c.id)); + +//depending on your dnd solution of choice, you may or may not need state like this +readonly movingColumnId = signal(null); +readonly targetColumnId = signal(null); + +//util function to splice and reorder the columnOrder array +const reorderColumn = ( + movingColumnId: Column, + targetColumnId: Column, +): string[] => { + const newColumnOrder = [...columnOrder]; + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + newColumnOrder.splice(newColumnOrder.indexOf(movingColumnId), 1)[0], + ); + setColumnOrder(newColumnOrder); +}; + +const handleDragEnd = (e: DragEvent) => { + if(!movingColumnId || !targetColumnId) return; + setColumnOrder(reorderColumn(movingColumnId, targetColumnId)); +}; + +//use your dnd solution of choice +``` + +### 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('left') +column.getIndex('center') +column.getIndex('right') + +column.getIsFirstColumn() +column.getIsLastColumn() +``` + +These helpers are useful for styling column boundaries or building drag-and-drop targets that need to know the current rendered order. + +#### Drag and Drop Column Reordering Suggestions (Angular) + +There are undoubtedly many ways to implement drag and drop features along-side TanStack Table. Here are a few suggestions in order for you to not have a bad time: + +1. Do NOT try to use [`"react-dnd"`](https://react-dnd.github.io/react-dnd/docs/overview) _if you are using Angular or newer_. framework-specific DnD was an important library for its time, but it now does not get updated very often, and it has incompatibilities with Angular, especially in Angular development mode. It is still possible to get it to work, but there are newer alternatives that have better compatibility and are more actively maintained. framework-specific DnD's Provider may also interfere and conflict with any other DnD solutions you may want to try in your app. + +2. Use [`"@dnd-kit/core"`](https://dndkit.com/). DnD Kit is a modern, modular and lightweight drag and drop library that is highly compatible with the modern Angular ecosystem, and it works well with semantic `` markup. The official framework-specific DnD examples, Column DnD and [Row DnD](../examples/row-dnd), use DnD Kit. + +3. Consider other DnD libraries like [`"react-beautiful-dnd"`](https://github.com/atlassian/react-beautiful-dnd), but be aware of their potentially large bundle sizes, maintenance status, and compatibility with `
` markup. + +4. Consider using native browser events and state management to implement lightweight drag and drop features. However, be aware that this approach may not be best for mobile users if you do not go the extra mile to implement proper touch events. [Material React Table V2](https://www.material-react-table.com/docs/examples/column-ordering) is an example of a library that implements TanStack Table with only browser drag and drop events such as `onDragStart`, `onDragEnd`, `onDragEnter` and no other dependencies. Browse its source code to see how it is done. diff --git a/docs/framework/angular/guide/column-pinning.md b/docs/framework/angular/guide/column-pinning.md new file mode 100644 index 0000000000..02ac8bba61 --- /dev/null +++ b/docs/framework/angular/guide/column-pinning.md @@ -0,0 +1,169 @@ +--- +title: Column Pinning (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnPinningFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ columnPinningFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data: this.data(), + })) +} +``` + +## Column Pinning (Angular) Guide + +TanStack Table offers state and APIs helpful for implementing column pinning features in your table UI. You can implement column pinning in multiple ways. You can either split pinned columns into their own separate tables, or you can keep all columns in the same table, but use the pinning state to order the columns correctly and use sticky CSS to pin the columns to the left or right. + +### 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 left, center (unpinned), and right 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.left` and `columnPinning.right` 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. + +```ts +import { injectTable, tableFeatures, columnPinningFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ columnPinningFeature }) + +readonly columnPinning = signal({ + left: [], + right: [], +}) + +readonly table = injectTable(() => ({ + features, + rowModels: {}, + //... + state: { + columnPinning: this.columnPinning(), + //... + }, + onColumnPinningChange: (updater) => + typeof updater === 'function' + ? this.columnPinning.update(updater) + : this.columnPinning.set(updater), + //... +}) +``` + +### Pin Columns by Default + +A very common use case is to pin some columns by default. You can do this by either initializing the `columnPinning` state with the pinned columnIds, or by using the `initialState` table option + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: {}, + //... + initialState: { + columnPinning: { + left: ['expand-column'], + right: ['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 left or right. 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 `left` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `right` 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({ + left: ['firstName'], + right: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```ts +table.getLeftLeafColumns() +table.getCenterLeafColumns() +table.getRightLeafColumns() + +table.getLeftVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getRightVisibleLeafColumns() + +table.getLeftHeaderGroups() +table.getCenterHeaderGroups() +table.getRightHeaderGroups() + +table.getLeftFooterGroups() +table.getCenterFooterGroups() +table.getRightFooterGroups() + +table.getLeftFlatHeaders() +table.getCenterFlatHeaders() +table.getRightFlatHeaders() + +table.getLeftLeafHeaders() +table.getCenterLeafHeaders() +table.getRightLeafHeaders() +``` + +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('left') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('right') + +table.getPinnedVisibleLeafColumns('left') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('right') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'left'` or `'right'` to check one pinned side. + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getLeftHeaderGroups`, `table.getCenterHeaderGroups`, `table.getRightHeaderGroups`, `row.getLeftVisibleCells`, `row.getCenterVisibleCells`, and `row.getRightVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/angular/guide/column-resizing.md b/docs/framework/angular/guide/column-resizing.md new file mode 100644 index 0000000000..e4a915160d --- /dev/null +++ b/docs/framework/angular/guide/column-resizing.md @@ -0,0 +1,224 @@ +--- +title: Column Resizing (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnResizingFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ columnResizingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data: this.data(), + })) +} +``` + +## Column Resizing (Angular) 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 `columnResizingFeature` to your features. The `column.getCanResize()` API will return `true` by default for all columns, but you can either disable column resizing for all columns with the `enableColumnResizing` table option, or disable column resizing on a per-column basis with the `enableResizing` column option. + +```ts +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + injectTable, +} from '@tanstack/angular-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns = [ + { + accessorKey: 'id', + enableResizing: false, // disable resizing for just this column + size: 200, // starting column size + }, + //... +] + +readonly table = injectTable(() => ({ + features, + rowModels: {}, + 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 React 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 React 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 +readonly table = injectTable(() => ({ + //... + columnResizeMode: 'onChange', // change column resize mode to "onChange" +}) +``` + +### Column Resize Direction + +By default, TanStack Table assumes that the table markup is laid out in a left-to-right direction. For right-to-left layouts, you may need to change the column resize direction to `"rtl"`. + +```ts +readonly table = injectTable(() => ({ + //... + columnResizeDirection: 'rtl', // change column resize direction to "rtl" for certain locales +}) +``` + +### Connect Column Resizing APIs to UI + +There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI. + +#### Column Size APIs + +To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs: + +```ts +header.getSize() +column.getSize() +cell.column.getSize() +``` + +How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes. + +```html + +``` + +Though, as discussed in the [advanced column resizing performance section](#advanced-column-resizing-performance), you may want to consider using CSS variables to apply column sizes to your markup. + +#### Column Resize APIs + +TanStack Table provides a pre-built event handler to make your drag interactions easy to implement. These event handlers are just convenience functions that call other internal APIs to update the column sizing state and re-render the table. Use `header.getResizeHandler()` to connect to your column resize drag interactions, for both mouse and touch events. + +```html +
+``` + +#### Column Resize Indicator with Column Resizing State + +TanStack Table keeps track of a `columnResizing` state object that you can use to render a column resize indicator UI. + +```html +
+``` + +The `columnResizing` state stores transient drag information: + +```ts +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +Use `onColumnResizingChange` with `state.columnResizing` if you need to manage this state externally. + +```ts +readonly columnResizing = signal({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data, + state: { + columnResizing: this.columnResizing(), + }, + onColumnResizingChange: (updater) => + typeof updater === 'function' + ? this.columnResizing.update(updater) + : this.columnResizing.set(updater), +}) +``` + +### Column Resizing APIs + +Use `header.getResizeHandler()` to connect mouse or touch events to the resizing logic. Use `column.getCanResize()` to decide whether to render a resize handle, and `column.getIsResizing()` to render active resizing UI. + +```ts +header.getResizeHandler() +column.getCanResize() +column.getIsResizing() +``` + +The table instance exposes APIs for the transient resize state. The current generated v9 API spelling is `table.setcolumnResizing` with a lowercase `c` in `column`; use that exact name. + +```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 Angular, you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns. + +We have created a [performant column resizing example](../examples/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: + +1. Don't use `column.getSize()` on every header and every data cell. Instead, calculate all column widths once upfront, **memoized**! +2. Memoize your Table Body while resizing is in progress. +3. Use CSS variables to communicate column widths to your table cells. + +If you follow these steps, you should see significant performance improvements while resizing columns. + +If you are not using React, and are using the Svelte, Vue, or Solid adapters instead, you may not need to worry about this as much, but similar principles apply. diff --git a/docs/framework/angular/guide/column-sizing.md b/docs/framework/angular/guide/column-sizing.md new file mode 100644 index 0000000000..c118aaef52 --- /dev/null +++ b/docs/framework/angular/guide/column-sizing.md @@ -0,0 +1,125 @@ +--- +title: Column Sizing (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Sizing](../examples/column-sizing) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnSizingFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ columnSizingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data: this.data(), + })) +} +``` + +## Column Sizing (Angular) 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 columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +readonly table = injectTable(() => ({ + features: tableFeatures({ columnSizingFeature }), + rowModels: {}, + 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() // left offset in the current column flow +column.getStart('left') +column.getStart('center') +column.getStart('right') + +column.getAfter() // right offset in the current column flow +column.getAfter('left') +column.getAfter('center') +column.getAfter('right') + +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.getLeftTotalSize() +table.getCenterTotalSize() +table.getRightTotalSize() +``` + +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) +``` diff --git a/docs/framework/angular/guide/column-visibility.md b/docs/framework/angular/guide/column-visibility.md new file mode 100644 index 0000000000..b33a9b4ec1 --- /dev/null +++ b/docs/framework/angular/guide/column-visibility.md @@ -0,0 +1,150 @@ +--- +title: Column Visibility (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Visibility](../examples/column-visibility) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnVisibilityFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data: this.data(), + })) +} +``` + +## Column Visibility (Angular) Guide + +The column visibility feature allows table columns to be hidden or shown dynamically. In v9, add `columnVisibilityFeature` to your `features` to enable this. There is a dedicated `columnVisibility` state and APIs for managing column visibility dynamically. + +### Column Visibility State + +The `columnVisibility` state is a map of column IDs to boolean values. A column will be hidden if its ID is present in the map and the value is `false`. If the column ID is not present in the map, or the value is `true`, the column will be shown. + +```ts +import { injectTable, tableFeatures, columnVisibilityFeature } from '@tanstack/angular-table' + +readonly columnVisibility = signal({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +readonly table = injectTable(() => ({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + state: { + columnVisibility: this.columnVisibility(), + //... + }, + onColumnVisibilityChange: (updater) => + typeof updater === 'function' + ? this.columnVisibility.update(updater) + : this.columnVisibility.set(updater), +}) +``` + +Alternatively, if you don't need to manage the column visibility state outside of the table, you can still set the initial default column visibility state using the `initialState` option. + +> **Note**: If `columnVisibility` is provided to both `initialState` and `state`, the `state` initialization will take precedence and `initialState` will be ignored. Do not provide `columnVisibility` to both `initialState` and `state`, only one or the other. + +```ts +readonly table = injectTable(() => ({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +}) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```ts +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessor: 'name', // can be hidden + }, +]; +``` + +### Column Visibility Toggle APIs + +There are several column API methods that are useful for rendering column visibility toggles in the UI. + +- `column.getCanHide` - Useful for disabling the visibility toggle for a column that has `enableHiding` set to `false`. +- `column.getIsVisible` - Useful for setting the initial state of the visibility toggle. +- `column.toggleVisibility` - Useful for toggling the visibility of a column. +- `column.getToggleVisibilityHandler` - Shortcut for hooking up the `column.toggleVisibility` method to a UI event handler. + +```html +@for (column of table.getAllLeafColumns(); track column.id) { + +} +``` + +### Column Visibility Aware Table APIs + +When you render your header, body, and footer cells, there are a lot of API options available. You may see APIs like `table.getAllLeafColumns` and `row.getAllCells`, but if you use these APIs, they will not take column visibility into account. Instead, you need to use the "visible" variants of these APIs, such as `table.getVisibleLeafColumns` and `row.getVisibleCells`. + +```html +
+ +
+ + + @for (column of table.getVisibleLeafColumns(); track column.id) { + + } + + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + } + + } + +
{{ column.id }}
+ {{ renderCell }} +
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/angular/guide/custom-features.md b/docs/framework/angular/guide/custom-features.md new file mode 100644 index 0000000000..7fc2ec39cd --- /dev/null +++ b/docs/framework/angular/guide/custom-features.md @@ -0,0 +1,293 @@ +--- +title: Custom Features (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Custom Plugin](../examples/custom-plugin) +## Custom Features (Angular) Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers where TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable`, `injectTable`, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrapper hooks around the `injectTable` hook to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking—you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods do: + +#### Default Options and Initial State + +
+ +##### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +##### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is responsible for adding methods to the `table` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. + +
+ +##### assignColumnPrototype + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, the [Column Grouping](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts) feature adds cell instance API methods such as `getIsGrouped` and `getIsAggregated`. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full [custom-plugin](../examples/custom-plugin) example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/angular-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs options, and state in your application. + +```html +@let density = table.atoms.density.get(); + + {{ renderCell }} + +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `signal`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/angular/guide/expanding.md b/docs/framework/angular/guide/expanding.md new file mode 100644 index 0000000000..6192d772af --- /dev/null +++ b/docs/framework/angular/guide/expanding.md @@ -0,0 +1,283 @@ +--- +title: Expanding (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Expanding](../examples/expanding) + +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, rowExpandingFeature, createExpandedRowModel } from '@tanstack/angular-table' + +const features = tableFeatures({ rowExpandingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + columns, + data: this.data(), + })) +} +``` + +## Expanding Feature (Angular) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` to your features and the `expandedRowModel` to your row models: + +```ts +import { + injectTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowExpandingFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are essentially child rows that inherit the same column structure as their parent rows. If your data object already includes these expanded rows data, you can utilize the `getSubRows` function to specify these child rows. However, if your data object does not contain the expanded rows data, they can be treated as custom expanded data, which is discussed in next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 } + ] + }, + { id: 3, + name: 'Doe', + age: 40, + children: [ + { id: 4, name: 'Alice', age: 10 } + ] + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +}) +``` + +> **Note:** You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +```html + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + {{ renderCell }} + + } + + @if (row.getIsExpanded()) { + + + + + + } + } + +``` + +### Expanded rows state + +If you need to control the expanded state of the rows in your table, you can do so by using the expanded state and the `onExpandedChange` option. This allows you to manage the expanded state according to your requirements. + +```ts +readonly expanded = signal({}) + +readonly table = injectTable(() => ({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + state: { + expanded: this.expanded(), + }, + onExpandedChange: (updater) => + typeof updater === 'function' + ? this.expanded.update(updater) + : this.expanded.set(updater), +}) +``` + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows with IDs present as keys in the record and have their value set to true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. For example, you can add a button UI within the columns definition. + +```ts +const columns = [ + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'age', + header: 'Age', + }, + { + header: 'Children', + cell: ({ row }) => row.getCanExpand(), + }, +] +``` + +```html +@if (row.getCanExpand()) { + +} +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```ts +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```ts +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, the filtering process starts from the parent rows and moves downwards. This means if a parent row is excluded by the filter, all its child rows will also be excluded. However, you can change this behavior by using the `filterFromLeafRows` option. When this option is enabled, the filtering process starts from the leaf (child) rows and moves upwards. This ensures that a parent row will be included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. Additionally, you can control how deep into the child hierarchy the filter process goes by using the `maxLeafRowFilterDepth` option. This option allows you to specify the maximum depth of child rows that the filter should consider. + +```ts +//... +readonly table = injectTable(() => ({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parents page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Pinning Guide](../../../guide/pinning.md) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +readonly table = injectTable(() => ({ + features: tableFeatures({ rowExpandingFeature }), + rowModels: {}, // no expandedRowModel needed for manual expanding + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/angular/guide/fuzzy-filtering.md b/docs/framework/angular/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..ca89f7c239 --- /dev/null +++ b/docs/framework/angular/guide/fuzzy-filtering.md @@ -0,0 +1,156 @@ +--- +title: Fuzzy Filtering (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Fuzzy Search](../examples/filters-fuzzy) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/angular-table' + +const features = tableFeatures({ columnFilteringFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data: this.data(), + })) +} +``` + +## Fuzzy Filtering (Angular) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement a client side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> **Note:** You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked in order to work better with TanStack Table's row by row filtering approach. + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +Here's an example of a custom fuzzy filter function: + +```typescript +import { rankItem } from '@tanstack/match-sorter-utils'; +import { FilterFn } from '@tanstack/angular-table' + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the rankItem function from the @tanstack/match-sorter-utils library to rank the item. We then store the ranking information in the meta data of the row, and return whether the item passed the ranking criteria. + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, you can specify the fuzzy filter function in the globalFilterFn option of the table instance: + +```typescript +import { + injectTable, + tableFeatures, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + filterFns, + sortFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ globalFilteringFeature, rowSortingFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + fuzzy: fuzzyFilter, + }), + sortedRowModel: createSortedRowModel(sortFns), // needed if you want sorting with fuzzy rank + }, + columns, + data, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, pass your fuzzy filter function to `createFilteredRowModel` (merging it with the built-in `filterFns`). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```typescript +const column = [ + { + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +]; +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```typescript +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFns } from '@tanstack/angular-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId]?.itemRank!, + rowB.columnFiltersMeta[columnId]?.itemRank! + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then specify this sorting function in the sortFn option of the column definition: + +```typescript +{ + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + sortFn: 'fuzzySort', //using our custom fuzzy sort function +} +``` diff --git a/docs/framework/angular/guide/global-faceting.md b/docs/framework/angular/guide/global-faceting.md new file mode 100644 index 0000000000..9d866b18c1 --- /dev/null +++ b/docs/framework/angular/guide/global-faceting.md @@ -0,0 +1,112 @@ +--- +title: Global Faceting (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Faceted Filters](../examples/filters-faceted) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/angular-table' + +const features = tableFeatures({ columnFacetingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data: this.data(), + })) +} +``` + +## Global Faceting (Angular) Guide + +Global Faceting allows you to generate lists of values for all columns from the table's data. For example, a list of unique values in a table can be generated from all rows in all columns to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a table of numbers to be used as a range for a range slider filter component. + +### Global Faceting Row Models + +In order to use any of the global faceting features, add the appropriate faceted row models to your `rowModels`: + +```ts +import { + injectTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnFacetingFeature }) +// If the table also uses global filtering, include globalFilteringFeature too. + +readonly table = injectTable(() => ({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), // required (other faceting methods depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + }, + // other options... +}) +``` + +### Use Global Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting table instance APIs to access the lists of values generated by the faceted row models. + +Use `table.getGlobalFacetedRowModel()` if you need to inspect the row model used to derive global facet 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 Global (Server-Side) Faceting + +If 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 `rowModels.facetedUniqueValues` and `rowModels.facetedMinMaxValues` factories. Global faceting requests use the internal `__global__` column ID. + +```ts +const globalFacets = await fetch('/api/faceting').then(res => res.json()) + +readonly table = injectTable(() => ({ + features, + rowModels: { + facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() + return new Map(globalFacets.uniqueValues) + }, + facetedMinMaxValues: (_table, columnId) => () => { + if (columnId !== '__global__') return undefined + return globalFacets.minMaxValues + }, + }, + // other options... +}) +``` + +In this example, the custom factories return server-provided global facet values when TanStack Table asks for the `__global__` faceting context. Column-specific faceting can still use separate values by checking for each column ID. diff --git a/docs/framework/angular/guide/global-filtering.md b/docs/framework/angular/guide/global-filtering.md new file mode 100644 index 0000000000..3d951fe809 --- /dev/null +++ b/docs/framework/angular/guide/global-filtering.md @@ -0,0 +1,244 @@ +--- +title: Global Filtering (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Column Filters](../examples/filters) +- [Fuzzy Search](../examples/filters-fuzzy) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, globalFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/angular-table' + +const features = tableFeatures({ globalFilteringFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data: this.data(), + })) +} +``` + +## Global Filtering (Angular) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +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 Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +import { + injectTable, + tableFeatures, + globalFilteringFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ globalFilteringFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: {}, // no filteredRowModel needed for manual server-side global filtering + data, + columns, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` to your features and the `filteredRowModel` to your row models: + +```ts +import { + injectTable, + tableFeatures, + globalFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ globalFilteringFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + // other options... +}) +``` + +### Global Filter Function + +The globalFilterFn option allows you to specify the filter function that will be used for global filtering. The filter function can be a string that references a built-in filter function, a string that references a custom filter function provided via the tableOptions.filterFns option, or a custom filter function. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + data, + columns, + globalFilterFn: 'text', // built-in filter function +}) +``` + +By default there are 10 built-in filter functions to choose from: + +- includesString - Case-insensitive string inclusion +- includesStringSensitive - Case-sensitive string inclusion +- equalsString - Case-insensitive string equality +- equalsStringSensitive - Case-sensitive string equality +- arrIncludes - Item inclusion within an array +- arrIncludesAll - All items included in an array +- arrIncludesSome - Some items included in an array +- equals - Object/referential equality Object.is/=== +- weakEquals - Weak object/referential equality == +- inNumberRange - Number range inclusion + +You can also define your own custom filter functions either as the globalFilterFn table option. + +### Global Filter State + +The global filter state is stored in the table's state atoms and can be read with `table.atoms.globalFilter.get()` or from the current `table.store.state.globalFilter` snapshot. If you want to persist the global filter state outside of the table, use the `state.globalFilter` and `onGlobalFilterChange` options together. + +```ts +readonly globalFilter = signal([]) + +readonly table = injectTable(() => ({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + state: { + globalFilter: this.globalFilter(), + }, + onGlobalFilterChange: (updater) => + typeof updater === 'function' + ? this.globalFilter.update(updater) + : this.globalFilter.set(updater), +}) +``` + +The global filtering state is defined as an object with the following shape: + +```ts +interface GlobalFilter { + globalFilter: any +} +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. + +```html + +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the globalFilterFn option. + +> **Note:** It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering.md). + +```ts +const customFilterFn = (rows, columnId, filterValue) => { + // custom filter logic +} + +readonly table = injectTable(() => ({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table initialState option. + +However, you can also just specify the initial global filter state in the state.globalFilter option. + +```ts +readonly globalFilter = signal("search term") //recommended to initialize globalFilter state here + +readonly table = injectTable(() => ({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + initialState: { + globalFilter: 'search term', // if not managing globalFilter state, set initial state here + }, + state: { + globalFilter: this.globalFilter(), // pass our managed globalFilter state to the table + }, +}) +``` + +> NOTE: Do not use both initialState.globalFilter and state.globalFilter at the same time, as the initialized state in the state.globalFilter will override the initialState.globalFilter. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` diff --git a/docs/framework/angular/guide/grouping.md b/docs/framework/angular/guide/grouping.md new file mode 100644 index 0000000000..a0e2c9f67a --- /dev/null +++ b/docs/framework/angular/guide/grouping.md @@ -0,0 +1,232 @@ +--- +title: Grouping (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Grouping](../examples/grouping) + +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, columnGroupingFeature, createGroupedRowModel, aggregationFns } from '@tanstack/angular-table' + +const features = tableFeatures({ columnGroupingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + columns, + data: this.data(), + })) +} +``` + +## Grouping (Angular) Guide + +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 left, center (unpinned), and right pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +To use the grouping feature, add the `columnGroupingFeature` to your features and the `groupedRowModel` to your row models. The grouped row model is responsible for grouping the rows based on the grouping state. + +```ts +import { + injectTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, + aggregationFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ columnGroupingFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```ts +const features = tableFeatures({ columnGroupingFeature, rowExpandingFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```ts +table.setGrouping(['column1', 'column2']); +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```ts +table.resetGrouping(); +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows by columns using the aggregationFn option. This is a string that is the ID of the aggregation function. You can define the aggregation functions using the aggregationFns option. + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'sum', +}) +``` + +In the above example, the sum aggregation function will be used to aggregate the data in the grouped rows. +By default, numeric columns will use the sum aggregation function, and non-numeric columns will use the count aggregation function. You can override this behavior by specifying the aggregationFn option in the column definition. + +There are several built-in aggregation functions that you can use: + +- sum - Sums the values in the grouped rows. +- count - Counts the number of rows in the grouped rows. +- min - Finds the minimum value in the grouped rows. +- max - Finds the maximum value in the grouped rows. +- extent - Finds the extent (min and max) of the values in the grouped rows. +- mean - Finds the mean of the values in the grouped rows. +- median - Finds the median of the values in the grouped rows. +- unique - Returns an array of unique values in the grouped rows. +- uniqueCount - Counts the number of unique values in the grouped rows. + +#### Custom Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows using the aggregationFns option. This is a record where the keys are the IDs of the aggregation functions, and the values are the aggregation functions themselves. You can then reference these aggregation functions in a column's aggregationFn option. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel({ + ...aggregationFns, + myCustomAggregation: (columnId, leafRows, childRows) => { + // return the aggregated value + }, + }), + }, + // other options... +}) +``` + +In the above example, myCustomAggregation is a custom aggregation function that takes the column ID, the leaf rows, and the child rows, and returns the aggregated value. You can then use this aggregation function in a column's aggregationFn option: + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'myCustomAggregation', +}) +``` + +### Manual Grouping + +If you are doing server-side grouping and aggregation, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to manually group the rows before passing them to the table. + +```ts +readonly table = injectTable(() => ({ + features: tableFeatures({ columnGroupingFeature }), + rowModels: {}, // no groupedRowModel needed for manual grouping + // other options... + manualGrouping: true, +}) +``` + +> **Note:** There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Grouping Change Handler + +If you want to manage the grouping state yourself, you can use the onGroupingChange option. This option is a function that is called when the grouping state changes. You can pass the managed state back to the table via the tableOptions.state.grouping option. + +```ts +readonly grouping = signal([]) + +readonly table = injectTable(() => ({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + state: { + grouping: this.grouping(), + }, + onGroupingChange: (updater) => + typeof updater === 'function' + ? this.grouping.update(updater) + : this.grouping.set(updater), +}) +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```ts +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +column.getAutoAggregationFn() +column.getAggregationFn() +``` + +Rows expose grouping helpers for grouped row rendering: + +```ts +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose helpers for choosing between grouped, aggregated, placeholder, and normal cell rendering: + +```ts +cell.getIsGrouped() +cell.getIsAggregated() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```ts +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/angular/guide/pagination.md b/docs/framework/angular/guide/pagination.md new file mode 100644 index 0000000000..ddaf846dac --- /dev/null +++ b/docs/framework/angular/guide/pagination.md @@ -0,0 +1,247 @@ +--- +title: Pagination (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Pagination](../examples/pagination) + +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/angular-table' + +const features = tableFeatures({ rowPaginationFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data(), + })) +} +``` + +## Pagination (Angular) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain ***ALL*** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. + +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. + +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 pagination and then switch to server-side pagination in the future as your data grows. + +#### Should You Use Virtualization Instead? + +Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` to your features and the `paginatedRowModel` to your row models: + +```ts +import { + injectTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPaginationFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. + +```ts +import { + injectTable, + tableFeatures, + rowPaginationFeature, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPaginationFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: {}, // no paginatedRowModel needed for server-side pagination + columns, + data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +You can manage the `pagination` state just like any other state in the table instance. + +```ts +import { + injectTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPaginationFeature }) + +readonly pagination = signal({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +readonly table = injectTable(() => ({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + onPaginationChange: (updater) => + typeof updater === 'function' + ? this.pagination.update(updater) + : this.pagination.set(updater), + state: { + pagination: this.pagination(), + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT pass the `pagination` state to both the `state` and `initialState` options. `state` will overwrite `initialState`. Only use one or the other. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` when page-altering state changes occur, such as when the `data` is updated, filters change, grouping changes, etc. This behavior is automatically disabled when `manualPagination` is true but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + autoResetPageIndex: false, // turn off auto reset of pageIndex +}) +``` + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> **Note**: These pagination APIs are available when using `rowPaginationFeature`. + +```html + + + + + +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/angular/guide/row-pinning.md b/docs/framework/angular/guide/row-pinning.md new file mode 100644 index 0000000000..ea05512ddc --- /dev/null +++ b/docs/framework/angular/guide/row-pinning.md @@ -0,0 +1,219 @@ +--- +title: Row Pinning (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Row Pinning](../examples/row-pinning) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, rowPinningFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ rowPinningFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data: this.data(), + })) +} +``` + +## Row Pinning (Angular) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory, so `rowModels` can stay empty unless your table uses other row-model features. + +```ts +import { + rowPinningFeature, + tableFeatures, + injectTable, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPinningFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data, +}) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```ts +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, use `state.rowPinning` with `onRowPinningChange`. + +```ts +readonly rowPinning = signal({ + top: [], + bottom: [], +}) + +readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data, + state: { + rowPinning: this.rowPinning(), + }, + onRowPinningChange: (updater) => + typeof updater === 'function' + ? this.rowPinning.update(updater) + : this.rowPinning.set(updater), +}) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```ts +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +}) + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```ts +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls: + +```html +@if (row.getCanPin()) { +
+ + + +
+} +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```ts +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly: + +```html + + @for (row of table.getTopRows(); track row.id) { + + } + @for (row of table.getCenterRows(); track row.id) { + + } + @for (row of table.getBottomRows(); track row.id) { + + } + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```ts +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data, + enableRowPinning: row => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/angular/guide/row-selection.md b/docs/framework/angular/guide/row-selection.md new file mode 100644 index 0000000000..bdac294ece --- /dev/null +++ b/docs/framework/angular/guide/row-selection.md @@ -0,0 +1,201 @@ +--- +title: Row Selection (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Row Selection](../examples/row-selection) +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, rowSelectionFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ rowSelectionFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data: this.data(), + })) +} +``` + +## Row Selection (Angular) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you (though as seen down below, it may be more convenient to manage the row selection state in your own scope). You can access the internal row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the current row selection state +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +> Note: If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +Even though the table instance will already manage the row selection state for you, it is usually more convenient to manage the state yourself in order to have easy access to the selected row ids that you can use to make API calls or other actions. + +Use the `onRowSelectionChange` table option to hoist up the row selection state to your own scope. Then pass the row selection state back to the table instance using in the `state` table option. + +```ts +import { injectTable, tableFeatures, rowSelectionFeature } from '@tanstack/angular-table' + +const features = tableFeatures({ rowSelectionFeature }) + +readonly rowSelection = signal({}) + +readonly table = injectTable(() => ({ + features, + rowModels: {}, + //... + onRowSelectionChange: (updater) => + typeof updater === 'function' + ? this.rowSelection.update(updater) + : this.rowSelection.set(updater), + state: { + rowSelection: this.rowSelection(), + }, +}) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: {}, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +readonly table = injectTable(() => ({ + //... + enableRowSelection: row => row.original.age > 18, //only enable row selection for adults +}) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +readonly table = injectTable(() => ({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +}) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +readonly table = injectTable(() => ({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +}) +``` + +### Render Row Selection UI + +TanStack table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These function automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +```html + + + +``` + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. + +```html + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getVisibleCells(); track cell.id) { + + {{ renderCell }} + + } + + } + +``` diff --git a/docs/framework/angular/guide/sorting.md b/docs/framework/angular/guide/sorting.md new file mode 100644 index 0000000000..f987c1f602 --- /dev/null +++ b/docs/framework/angular/guide/sorting.md @@ -0,0 +1,473 @@ +--- +title: Sorting (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Sorting](../examples/sorting) + +### Angular Setup + +```ts +import { signal } from '@angular/core' +import { injectTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/angular-table' + +const features = tableFeatures({ rowSortingFeature }) + +export class App { + readonly data = signal(defaultData) + + readonly table = injectTable(() => ({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data: this.data(), + })) +} +``` + +## Sorting (Angular) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```ts +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +You can access the sorting state directly from the table instance with `table.atoms.sorting.get()` or from the current `table.store.state` snapshot. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.sorting.get()) // access the current sorting state +``` + +However, if you need to access the sorting state before the table is initialized, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state, you can control/manage the sorting state in your own state management with the `state.sorting` and `onSortingChange` table options. + +```ts +readonly sorting = signal([]) // can set initial sorting state here +//... +// use sorting state to fetch data from your server or something... +//... +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + state: { + sorting: this.sorting(), + }, + onSortingChange: (updater) => + typeof updater === 'function' + ? this.sorting.update(updater) + : this.sorting.set(updater), +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. + +```ts +readonly sorting = signal([]) +//... +readonly table = injectTable(() => ({ + features: tableFeatures({ rowSortingFeature }), // feature needed for sorting state/APIs + rowModels: {}, // no sortedRowModel needed for manual sorting + columns, + data, + manualSorting: true, // use pre-sorted row model instead of sorted row model + state: { + sorting: this.sorting(), + }, + onSortingChange: (updater) => + typeof updater === 'function' + ? this.sorting.update(updater) + : this.sorting.set(updater), +}) +``` + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` to your features and the `sortedRowModel` to your row models. Import `createSortedRowModel` and `sortFns` from TanStack Table: + +```ts +import { + injectTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFns, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowSortingFeature }) + +readonly table = injectTable(() => ({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions either as the `sortFn` column option, or as a global sorting function using the `sortFns` table option. + +#### Custom Sorting Functions + +When defining a custom sorting function in either the `sortFns` table option or as a `sortFn` column option, it should have the following signature: + +```ts +//optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = (rowA: Row, rowB: Row, columnId: string) => { + return //-1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> Note: The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // use custom global sorting function + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + } +] +//... +readonly table = injectTable(() => ({ + features, + rowModels: { + sortedRowModel: createSortedRowModel({ + ...sortFns, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }), + }, + columns, + data, +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```ts +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, //sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, //sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + sortDescFirst: true, //sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> **NOTE**: You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +In not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> NOTE: `'first'` and `'last'` options are available in v9. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this: + +`'none' -> 'desc' -> 'asc' -> 'none' -> 'desc' -> 'asc' -> ...` + +If you disable sorting removal, the behavior will be like this: + +`'none' -> 'desc' -> 'asc' -> 'desc' -> 'asc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (always none -> asc -> desc -> asc) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```ts +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/angular/guide/virtualization.md b/docs/framework/angular/guide/virtualization.md new file mode 100644 index 0000000000..89be90d571 --- /dev/null +++ b/docs/framework/angular/guide/virtualization.md @@ -0,0 +1,233 @@ +--- +title: Virtualization (Angular) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Angular examples: + +- [Virtualized Columns](../examples/virtualized-columns) +- [Virtualized Rows](../examples/virtualized-rows) +- [Virtualized Infinite Scrolling](../examples/virtualized-infinite-scrolling) + +### Angular Setup + +Install and import the Angular virtualizer adapter from `@tanstack/angular-virtual`. TanStack Table still owns rows, columns, and table state; the virtualizer owns scroll indexes and measurements. +Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/react/examples/table). + +## Virtualization (Angular) Guide + +The TanStack Table packages do not come with any virtualization APIs or features built in. Virtualization is a rendering strategy, not a table feature. You can use TanStack Table with any virtualization library, but the official examples use TanStack Virtual. + +TanStack Table and TanStack Virtual solve different parts of the problem: + +- TanStack Table builds the row models, columns, headers, cells, sizing, sorting, filtering, and other table state. +- TanStack Virtual decides which item indexes should be rendered for the current scroll position. +- Your table renderer maps those virtual indexes back to rows, headers, and cells. + +### When To Use Virtualization + +Use virtualization when your table has a very large number of rows, columns, or both. Virtualization keeps the DOM small by only rendering the items that are visible in the scroll viewport plus a small overscan buffer. + +Virtualization is not a replacement for server-side pagination, filtering, or sorting. If the data is virtualized on the client, the data still needs to exist on the client. If your dataset is too large to load into the browser, use server-side data operations or infinite scrolling. + +For small tables, normal rendering is simpler and usually preferable. + +### Install TanStack Virtual + +Install the Angular virtualizer adapter: + +```sh +npm install @tanstack/angular-virtual +``` + +The Angular examples use `injectVirtualizer` from `@tanstack/angular-virtual`. TanStack Table still owns rows, columns, headers, cells, sizing, sorting, filtering, and other table state; TanStack Virtual decides which item indexes should render for the current scroll position. +### The Basic Pattern + +Most virtualized table implementations follow the same pattern: + +1. Create a fixed-height scroll container. +2. Pass the scroll element to the virtualizer. +3. Use `table.getRowModel().rows` or `table.getVisibleLeafColumns()` as the source list. +4. Configure `count`, `estimateSize`, `overscan`, and optional `measureElement`. +5. Render only virtual items. +6. Use virtual offsets or spacer padding to preserve the full scroll geometry. + +Here is a compact row virtualization example: + +```ts +readonly rows = computed(() => this.table.getRowModel().rows) + +readonly rowVirtualizer = injectVirtualizer(() => ({ + count: this.rows().length, + scrollElement: this.scrollContainer()?.nativeElement, + estimateSize: () => 33, + overscan: 5, +})) +``` + +```html + + @for (virtualRow of rowVirtualizer.getVirtualItems(); track virtualRow.key) { + @let row = rows()[virtualRow.index]; + + @for (cell of row.getVisibleCells(); track cell.id) { + + + {{ renderCell }} + + + } + + } + +``` + +### Virtualized Rows + +The [virtualized rows examples](../examples/virtualized-rows) show how to render large row counts while keeping the DOM small. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The core idea is that sorting, filtering, grouping, and other row-model work still comes from TanStack Table. The virtualizer reads from the final table row model: + +```ts +const rows = table.getRowModel().rows +``` + +The row virtualizer is configured with `count: rows.length`, a row height estimate, the scroll container, and an overscan value. The `tbody` is given the full virtual height with `rowVirtualizer.getTotalSize()`, while each rendered row is absolutely positioned with `transform: translateY(...)`. + +The examples render cells from the current row with APIs like `row.getVisibleCells()` or `row.getAllCells()`, depending on whether the example needs visibility-aware cells or all cells. + +The official examples use large generated datasets, commonly tens or hundreds of thousands of rows. They also support dynamic row heights by using `measureElement` when possible. The examples skip dynamic row measurement in Firefox because Firefox can measure table border height differently. + +### Virtualized Columns + +The [virtualized columns examples](../examples/virtualized-columns) show how to render large row and column counts. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +Column virtualization uses the current visible column list: + +```ts +const visibleColumns = table.getVisibleLeafColumns() +``` + +The column virtualizer is configured for horizontal virtualization: + +```ts +readonly visibleColumns = computed(() => this.table.getVisibleLeafColumns()) + +readonly columnVirtualizer = injectVirtualizer(() => ({ + count: this.visibleColumns().length, + estimateSize: index => this.visibleColumns()[index].getSize(), + scrollElement: this.scrollContainer()?.nativeElement, + horizontal: true, + overscan: 3, +})) +``` + +Column virtualization uses a different rendering strategy than row virtualization. Instead of absolutely positioning columns, the examples add fake spacer cells to the left and right: + +```ts +const virtualColumns = this.columnVirtualizer.getVirtualItems() +const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 +const virtualPaddingRight = + this.columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) +``` + +Those spacer cells preserve the horizontal scroll width while the renderer only mounts the virtual columns. This approach keeps row rendering table-like and allows dynamic row height measurement to keep working. + +### Virtualized Rows And Columns Together + +The official virtualized columns examples also virtualize rows. In those examples: + +- The row virtualizer controls vertical positioning and total body height. +- The column virtualizer controls horizontal header/cell rendering and left/right spacer cells. +- `virtualRow.index` maps to `rows[virtualRow.index]`. +- `virtualColumn.index` maps to `visibleCells[virtualColumn.index]`. + +Always use virtual indexes against the same current row and column lists returned by the table. If sorting, filtering, pagination, grouping, or column visibility changes, recompute the virtualized rows and columns from the current table state. + +### Virtualized Infinite Scrolling + +The [virtualized infinite scrolling examples](../examples/virtualized-infinite-scrolling) combine row virtualization with progressive data fetching. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The common pattern is: + +1. Fetch a page of rows. +2. Flatten fetched pages into the table `data`. +3. Use row virtualization over the loaded rows. +4. Listen to scroll events on the table container. +5. Fetch the next page when the user scrolls near the bottom. + +The Angular infinite scrolling pattern can use TanStack Query or any other data-fetching layer. + +```ts +const { scrollHeight, scrollTop, clientHeight } = scrollElement + +if (scrollHeight - scrollTop - clientHeight < 500) { + fetchNextPage() +} +``` + +If sorting is handled by the server, use manual sorting so the fetched data reflects the whole backend dataset rather than only the currently loaded rows. When sorting changes and the fetched dataset is replaced, scroll back to the top with `rowVirtualizer.scrollToIndex(0)`. + +### Dynamic Row Heights + +Dynamic row heights are useful when content can wrap or expand. They are also more complex than fixed-height rows. + +Use `estimateSize` as the virtualizer's initial guess: + +```ts +estimateSize: () => 33 +``` + +Then use `measureElement` to refine the actual row height after rendering: + +```html + +``` + +Set `data-index` on each row so the virtualizer can associate measurements with the correct item. In non-React adapters, call `measureElement` through the adapter-appropriate ref, action, directive, or controller. + +Overscan helps avoid blank regions while measurements settle. If every row has a known fixed height, skip dynamic measurement and use the fixed height estimate instead. + +### Sticky Headers And Semantic Table Markup + +The examples still use semantic table tags, but they change table layout CSS to support virtual positioning and sticky headers. + +Dynamic row virtualization commonly requires: + +```css +table { + display: grid; +} + +thead { + display: grid; + position: sticky; + top: 0; +} + +tr { + display: flex; +} +``` + +Rows are absolutely positioned inside a relatively positioned `tbody`, and cells use flex sizing so they can match `column.getSize()` or `cell.column.getSize()`. This is intentional. Native table layout does not work well with dynamic-height virtual rows that are positioned independently. + +### Performance Tips + +- Keep virtualizers near the components that render the virtualized items. +- Avoid re-rendering the full table body on every scroll. +- Keep row, column, and data references stable where possible. +- Use `overscan` deliberately. More overscan reduces visible blanking, while less overscan reduces DOM nodes. +- Avoid expensive cell renderers in very large virtualized tables. +- Test production builds. Framework development builds can be slower than production builds; profile production bundles before optimizing. +- Prefer fixed row sizes when the UI allows it. +- For column virtualization, use `column.getSize()`, `header.getSize()`, and `cell.column.getSize()` consistently. diff --git a/docs/framework/lit/guide/column-faceting.md b/docs/framework/lit/guide/column-faceting.md new file mode 100644 index 0000000000..cf6fb17923 --- /dev/null +++ b/docs/framework/lit/guide/column-faceting.md @@ -0,0 +1,125 @@ +--- +title: Column Faceting (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Faceted Filters](../examples/filters-faceted) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/lit-table' + +const features = tableFeatures({ columnFacetingFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Column Faceting (Lit) Guide + +Column Faceting is a feature that allows you to generate lists of values for a given column from that column's data. For example, a list of unique values in a column can be generated from all rows in that column to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a column of numbers to be used as a range for a range slider filter component. + +### Column Faceting Row Models + +In order to use any of the column faceting features, add the `columnFacetingFeature` to your features and the appropriate faceted row models to `rowModels`. + +```ts +import { + TableController, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/lit-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + 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 + }, + 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]; +``` + +### Custom (Server-Side) Faceting + +If 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. You can use the `getFacetedUniqueValues` and `getFacetedMinMaxValues` table options to resolve the faceted values from the server-side. + +```ts +const facetingQuery = useQuery( + //... +) + +const table = this.tableController.table({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data: this.data, + getFacetedUniqueValues: (table, columnId) => { + const uniqueValueMap = new Map() + //... + return uniqueValueMap + }, + getFacetedMinMaxValues: (table, columnId) => { + //... + return [min, max] + }, + //... +}) +``` + +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/lit/guide/column-filtering.md b/docs/framework/lit/guide/column-filtering.md new file mode 100644 index 0000000000..1e8f7fb15f --- /dev/null +++ b/docs/framework/lit/guide/column-filtering.md @@ -0,0 +1,385 @@ +--- +title: Column Filtering (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Fuzzy Search](../examples/filters-fuzzy) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/lit-table' + +const features = tableFeatures({ columnFilteringFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Column Filtering (Lit) 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 `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +const table = this.tableController.table({ + features: tableFeatures({ columnFilteringFeature }), + rowModels: {}, // no filteredRowModel needed for manual server-side filtering + 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` to your features and the `filteredRowModel` to your row models. Import `createFilteredRowModel` and `filterFns` from TanStack Table: + +```ts +import { + TableController, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/lit-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + 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 + +You can access the column filter state from the table instance with `table.atoms.columnFilters.get()` or from the current `table.store.state` snapshot. + +```ts +const table = this.tableController.table({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data: this.data, + //... +}) + +console.log(table.atoms.columnFilters.get()) // access the current column filters state +``` + +However, if you need to access the column filter state before the table is initialized, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state, you can control/manage the column filter state in your own state management with the `state.columnFilters` and `onColumnFiltersChange` table options. + +```ts +@state() +private columnFilters: ColumnFiltersState = [] // can set initial column filter state here +//... +const table = this.tableController.table({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + 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`. + +```ts +const table = this.tableController.table({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + 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 10 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `arrIncludes` - Item inclusion within an array +- `arrIncludesAll` - All items included in an array +- `arrIncludesSome` - Some items included in an array +- `equals` - Object/referential equality `Object.is`/`===` +- `weakEquals` - Weak object/referential equality `==` +- `inNumberRange` - Number range inclusion + +You can also define your own custom filter functions either as the `filterFn` column option, or as a global filter function using the `filterFns` table option. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +When defining a custom filter function in either the `filterFn` column option or the `filterFns` table option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = (row: Row, columnId: string, filterValue: any, addMeta: (meta: any) => void) => boolean +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // use custom global filter function + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + } +] +//... +const table = this.tableController.table({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }), + }, + columns, + data: this.data, +}) +``` + +##### 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 = ( + row: Row, + columnId: string, + filterValue: number | string, //resolveFilterValue will transform this 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 = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +const table = this.tableController.table({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + 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. + +```ts +const table = this.tableController.table({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + 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. + +```ts +const table = this.tableController.table({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + 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/lit/guide/column-ordering.md b/docs/framework/lit/guide/column-ordering.md new file mode 100644 index 0000000000..be65d86079 --- /dev/null +++ b/docs/framework/lit/guide/column-ordering.md @@ -0,0 +1,168 @@ +--- +title: Column Ordering (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Column Ordering](../examples/column-ordering) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnOrderingFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ columnOrderingFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Column Ordering (Lit) 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 left, center (unpinned), and right 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 table = this.tableController.table({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + 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. + +```ts +@state() +private columnOrder: string[] = ['columnId1', 'columnId2', 'columnId3'] +//... +const table = this.tableController.table({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + 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, you can set up the logic something like this: + +```ts +@state() +private columnOrder: string[] = columns.map(c => c.id); + +//depending on your dnd solution of choice, you may or may not need state like this +@state() +private movingColumnId: string | null = null; +@state() +private targetColumnId: string | null = null; + +//util function to splice and reorder the columnOrder array +const reorderColumn = ( + movingColumnId: Column, + targetColumnId: Column, +): string[] => { + const newColumnOrder = [...columnOrder]; + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + newColumnOrder.splice(newColumnOrder.indexOf(movingColumnId), 1)[0], + ); + setColumnOrder(newColumnOrder); +}; + +const handleDragEnd = (e: DragEvent) => { + if(!movingColumnId || !targetColumnId) return; + setColumnOrder(reorderColumn(movingColumnId, targetColumnId)); +}; + +//use your dnd solution of choice +``` + +### 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('left') +column.getIndex('center') +column.getIndex('right') + +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 (Lit) + +There are undoubtedly many ways to implement drag and drop features along-side TanStack Table. Here are a few suggestions in order for you to not have a bad time: + +1. Do NOT try to use [`"react-dnd"`](https://react-dnd.github.io/react-dnd/docs/overview) _if you are using Lit or newer_. framework-specific DnD was an important library for its time, but it now does not get updated very often, and it has incompatibilities with Lit, especially in Lit development mode. It is still possible to get it to work, but there are newer alternatives that have better compatibility and are more actively maintained. framework-specific DnD's Provider may also interfere and conflict with any other DnD solutions you may want to try in your app. + +2. Use [`"@dnd-kit/core"`](https://dndkit.com/). DnD Kit is a modern, modular and lightweight drag and drop library that is highly compatible with the modern Lit ecosystem, and it works well with semantic `` markup. The official framework-specific DnD examples, Column DnD and Row DnD, use DnD Kit. + +3. Consider other DnD libraries like [`"react-beautiful-dnd"`](https://github.com/atlassian/react-beautiful-dnd), but be aware of their potentially large bundle sizes, maintenance status, and compatibility with `
` markup. + +4. Consider using native browser events and state management to implement lightweight drag and drop features. However, be aware that this approach may not be best for mobile users if you do not go the extra mile to implement proper touch events. [Material React Table V2](https://www.material-react-table.com/docs/examples/column-ordering) is an example of a library that implements TanStack Table with only browser drag and drop events such as `onDragStart`, `onDragEnd`, `onDragEnter` and no other dependencies. Browse its source code to see how it is done. diff --git a/docs/framework/lit/guide/column-pinning.md b/docs/framework/lit/guide/column-pinning.md new file mode 100644 index 0000000000..5363e8dbed --- /dev/null +++ b/docs/framework/lit/guide/column-pinning.md @@ -0,0 +1,178 @@ +--- +title: Column Pinning (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnPinningFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ columnPinningFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Column Pinning (Lit) 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 left or right. + +### 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 left, center (unpinned), and right 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.left` and `columnPinning.right` 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. + +```ts +import { TableController, tableFeatures, columnPinningFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ columnPinningFeature }) + +@state() +private columnPinning: ColumnPinningState = { + left: [], + right: [], +} + +const table = this.tableController.table({ + features, + rowModels: {}, + //... + 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 = this.tableController.table({ + features, + rowModels: {}, + //... + initialState: { + columnPinning: { + left: ['expand-column'], + right: ['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 left or right. 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 `left` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `right` 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({ + left: ['firstName'], + right: ['actions'], +} + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```ts +table.getLeftLeafColumns() +table.getCenterLeafColumns() +table.getRightLeafColumns() + +table.getLeftVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getRightVisibleLeafColumns() + +table.getLeftHeaderGroups() +table.getCenterHeaderGroups() +table.getRightHeaderGroups() + +table.getLeftFooterGroups() +table.getCenterFooterGroups() +table.getRightFooterGroups() + +table.getLeftFlatHeaders() +table.getCenterFlatHeaders() +table.getRightFlatHeaders() + +table.getLeftLeafHeaders() +table.getCenterLeafHeaders() +table.getRightLeafHeaders() +``` + +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('left') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('right') + +table.getPinnedVisibleLeafColumns('left') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('right') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'left'` or `'right'` to check one pinned side. + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getLeftHeaderGroups`, `table.getCenterHeaderGroups`, `table.getRightHeaderGroups`, `row.getLeftVisibleCells`, `row.getCenterVisibleCells`, and `row.getRightVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/lit/guide/column-resizing.md b/docs/framework/lit/guide/column-resizing.md new file mode 100644 index 0000000000..0540328177 --- /dev/null +++ b/docs/framework/lit/guide/column-resizing.md @@ -0,0 +1,237 @@ +--- +title: Column Resizing (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnResizingFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ columnResizingFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Column Resizing (Lit) 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 `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, + TableController, +} from '@tanstack/lit-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 = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.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 React 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 React 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 = this.tableController.table({ + //... + 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 = this.tableController.table({ + //... + columnResizeDirection: 'rtl', // change column resize direction to "rtl" for certain locales +}) +``` + +### Connect Column Resizing APIs to UI + +There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI. + +#### Column Size APIs + +To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs: + +```ts +header.getSize() +column.getSize() +cell.column.getSize() +``` + +How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes. + +```ts +html` + +` +``` + +Though, as discussed in the [advanced column resizing performance section](#advanced-column-resizing-performance), you may want to consider using CSS variables to apply column sizes to your markup. + +#### Column Resize APIs + +TanStack Table provides a pre-built event handler to make your drag interactions easy to implement. These event handlers are just convenience functions that call other internal APIs to update the column sizing state and re-render the table. Use `header.getResizeHandler()` to connect to your column resize drag interactions, for both mouse and touch events. + +```ts +html` +
+` +``` + +#### Column Resize Indicator with Column Resizing State + +TanStack Table keeps track of a `columnResizing` state object that you can use to render a column resize indicator UI. + +```ts +html` +
+` +``` + +The `columnResizing` state stores transient drag information: + +```ts +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +Use `onColumnResizingChange` with `state.columnResizing` if you need to manage this state externally. + +```ts +@state() +private columnResizing: columnResizingState = { + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +} + +const table = this.tableController.table({ + features, + rowModels: {}, + 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. The current generated v9 API spelling is `table.setcolumnResizing` with a lowercase `c` in `column`; use that exact name. + +```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 Lit, you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns. + +We have created a [performant column resizing example](../examples/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: + +1. Don't use `column.getSize()` on every header and every data cell. Instead, calculate all column widths once upfront, **memoized**! +2. Memoize your Table Body while resizing is in progress. +3. Use CSS variables to communicate column widths to your table cells. + +If you follow these steps, you should see significant performance improvements while resizing columns. + +If you are not using React, and are using the Svelte, Vue, or Solid adapters instead, you may not need to worry about this as much, but similar principles apply. diff --git a/docs/framework/lit/guide/column-sizing.md b/docs/framework/lit/guide/column-sizing.md new file mode 100644 index 0000000000..ed15c44956 --- /dev/null +++ b/docs/framework/lit/guide/column-sizing.md @@ -0,0 +1,134 @@ +--- +title: Column Sizing (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Column Sizing](../examples/column-sizing) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnSizingFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ columnSizingFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Column Sizing (Lit) 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 columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +const table = this.tableController.table({ + features: tableFeatures({ columnSizingFeature }), + rowModels: {}, + 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() // left offset in the current column flow +column.getStart('left') +column.getStart('center') +column.getStart('right') + +column.getAfter() // right offset in the current column flow +column.getAfter('left') +column.getAfter('center') +column.getAfter('right') + +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.getLeftTotalSize() +table.getCenterTotalSize() +table.getRightTotalSize() +``` + +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) +``` diff --git a/docs/framework/lit/guide/column-visibility.md b/docs/framework/lit/guide/column-visibility.md new file mode 100644 index 0000000000..692fd4eefd --- /dev/null +++ b/docs/framework/lit/guide/column-visibility.md @@ -0,0 +1,161 @@ +--- +title: Column Visibility (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Column Visibility](../examples/column-visibility) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnVisibilityFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Column Visibility (Lit) 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. + +```ts +import { TableController, tableFeatures, columnVisibilityFeature } from '@tanstack/lit-table' + +@state() +private columnVisibility: ColumnVisibilityState = { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +} + +const table = this.tableController.table({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + 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 table = this.tableController.table({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +}) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```ts +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessor: '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 +html` + ${table.getAllColumns().map( + (column) => html` + + `, + )} +` +``` + +### Column Visibility Aware Table APIs + +When you render your header, body, and footer cells, there are a lot of API options available. You may see APIs like `table.getAllLeafColumns` and `row.getAllCells`, but if you use these APIs, they will not take column visibility into account. Instead, you need to use the "visible" variants of these APIs, such as `table.getVisibleLeafColumns` and `row.getVisibleCells`. + +```ts +html` +
+ +
+ + + ${table.getVisibleLeafColumns().map((column) => html``)} + + + + ${table.getRowModel().rows.map( + (row) => html` + + ${row.getVisibleCells().map((cell) => html``)} + + `, + )} + +
${column.id}
${FlexRender({ cell })}
+` +``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/lit/guide/custom-features.md b/docs/framework/lit/guide/custom-features.md new file mode 100644 index 0000000000..84634acb21 --- /dev/null +++ b/docs/framework/lit/guide/custom-features.md @@ -0,0 +1,290 @@ +--- +title: Custom Features (Lit) Guide +--- + +## Custom Features (Lit) Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers where TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable`, `this.tableController.table`, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrapper hooks around the `this.tableController.table` hook to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking—you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods do: + +#### Default Options and Initial State + +
+ +##### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +##### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is responsible for adding methods to the `table` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. + +
+ +##### assignColumnPrototype + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, the [Column Grouping](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts) feature adds cell instance API methods such as `getIsGrouped` and `getIsAggregated`. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full custom-plugin example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/lit-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs options, and state in your application. + +```ts +const density = table.state.density + +return html` + + ${FlexRender({ cell })} + +` +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `@state`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/lit/guide/expanding.md b/docs/framework/lit/guide/expanding.md new file mode 100644 index 0000000000..003386fa14 --- /dev/null +++ b/docs/framework/lit/guide/expanding.md @@ -0,0 +1,293 @@ +--- +title: Expanding (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Expanding](../examples/expanding) + +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, rowExpandingFeature, createExpandedRowModel } from '@tanstack/lit-table' + +const features = tableFeatures({ rowExpandingFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Expanding Feature (Lit) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` to your features and the `expandedRowModel` to your row models: + +```ts +import { + TableController, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/lit-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are essentially child rows that inherit the same column structure as their parent rows. If your data object already includes these expanded rows data, you can utilize the `getSubRows` function to specify these child rows. However, if your data object does not contain the expanded rows data, they can be treated as custom expanded data, which is discussed in next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 } + ] + }, + { id: 3, + name: 'Doe', + age: 40, + children: [ + { id: 4, name: 'Alice', age: 10 } + ] + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +const table = this.tableController.table({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +}) +``` + +> **Note:** You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +```ts +html` + + ${table.getRowModel().rows.map( + (row) => html` + + ${row.getVisibleCells().map((cell) => html`${FlexRender({ cell })}`)} + + ${row.getIsExpanded() + ? html` + + + + + + ` + : null} + `, + )} + +` +``` + +### Expanded rows state + +If you need to control the expanded state of the rows in your table, you can do so by using the expanded state and the `onExpandedChange` option. This allows you to manage the expanded state according to your requirements. + +```ts +@state() +private expanded: ExpandedState = {} + +const table = this.tableController.table({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + state: { + expanded: this.expanded, + }, + onExpandedChange: (updater) => { + this.expanded = typeof updater === 'function' ? updater(this.expanded) : updater + }, +}) +``` + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows with IDs present as keys in the record and have their value set to true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. For example, you can add a button UI within the columns definition. + +```ts +const columns = [ + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'age', + header: 'Age', + }, + { + header: 'Children', + cell: ({ row }) => row.getCanExpand(), + }, +] +``` + +```ts +row.getCanExpand() + ? html`` + : null +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```ts +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```ts +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, the filtering process starts from the parent rows and moves downwards. This means if a parent row is excluded by the filter, all its child rows will also be excluded. However, you can change this behavior by using the `filterFromLeafRows` option. When this option is enabled, the filtering process starts from the leaf (child) rows and moves upwards. This ensures that a parent row will be included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. Additionally, you can control how deep into the child hierarchy the filter process goes by using the `maxLeafRowFilterDepth` option. This option allows you to specify the maximum depth of child rows that the filter should consider. + +```ts +//... +const table = this.tableController.table({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parents page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +const table = this.tableController.table({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Pinning Guide](../../../guide/pinning.md) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const table = this.tableController.table({ + features: tableFeatures({ rowExpandingFeature }), + rowModels: {}, // no expandedRowModel needed for manual expanding + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/lit/guide/fuzzy-filtering.md b/docs/framework/lit/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..23c9e9ce1e --- /dev/null +++ b/docs/framework/lit/guide/fuzzy-filtering.md @@ -0,0 +1,165 @@ +--- +title: Fuzzy Filtering (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Fuzzy Search](../examples/filters-fuzzy) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/lit-table' + +const features = tableFeatures({ columnFilteringFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Fuzzy Filtering (Lit) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement a client side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> **Note:** You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked in order to work better with TanStack Table's row by row filtering approach. + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +Here's an example of a custom fuzzy filter function: + +```typescript +import { rankItem } from '@tanstack/match-sorter-utils'; +import { FilterFn } from '@tanstack/lit-table' + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the rankItem function from the @tanstack/match-sorter-utils library to rank the item. We then store the ranking information in the meta data of the row, and return whether the item passed the ranking criteria. + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, you can specify the fuzzy filter function in the globalFilterFn option of the table instance: + +```typescript +import { + TableController, + tableFeatures, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + filterFns, + sortFns, +} from '@tanstack/lit-table' + +const features = tableFeatures({ globalFilteringFeature, rowSortingFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + fuzzy: fuzzyFilter, + }), + sortedRowModel: createSortedRowModel(sortFns), // needed if you want sorting with fuzzy rank + }, + columns, + data: this.data, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, pass your fuzzy filter function to `createFilteredRowModel` (merging it with the built-in `filterFns`). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```typescript +const column = [ + { + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +]; +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```typescript +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFns } from '@tanstack/lit-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId]?.itemRank!, + rowB.columnFiltersMeta[columnId]?.itemRank! + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then specify this sorting function in the sortFn option of the column definition: + +```typescript +{ + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + sortFn: 'fuzzySort', //using our custom fuzzy sort function +} +``` diff --git a/docs/framework/lit/guide/global-faceting.md b/docs/framework/lit/guide/global-faceting.md new file mode 100644 index 0000000000..bb3fbd4216 --- /dev/null +++ b/docs/framework/lit/guide/global-faceting.md @@ -0,0 +1,121 @@ +--- +title: Global Faceting (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Faceted Filters](../examples/filters-faceted) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/lit-table' + +const features = tableFeatures({ columnFacetingFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Global Faceting (Lit) Guide + +Global Faceting allows you to generate lists of values for all columns from the table's data. For example, a list of unique values in a table can be generated from all rows in all columns to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a table of numbers to be used as a range for a range slider filter component. + +### Global Faceting Row Models + +In order to use any of the global faceting features, add the appropriate faceted row models to your `rowModels`: + +```ts +import { + TableController, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/lit-table' + +const features = tableFeatures({ columnFacetingFeature }) +// If the table also uses global filtering, include globalFilteringFeature too. + +const table = this.tableController.table({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), // required (other faceting methods depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + }, + // other options... +}) +``` + +### Use Global Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting table instance APIs to access the lists of values generated by the faceted row models. + +Use `table.getGlobalFacetedRowModel()` if you need to inspect the row model used to derive global facet 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 Global (Server-Side) Faceting + +If 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 `rowModels.facetedUniqueValues` and `rowModels.facetedMinMaxValues` factories. Global faceting requests use the internal `__global__` column ID. + +```ts +const globalFacets = await fetch('/api/faceting').then(res => res.json()) + +const table = this.tableController.table({ + features, + rowModels: { + facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() + return new Map(globalFacets.uniqueValues) + }, + facetedMinMaxValues: (_table, columnId) => () => { + if (columnId !== '__global__') return undefined + return globalFacets.minMaxValues + }, + }, + // other options... +}) +``` + +In this example, the custom factories return server-provided global facet values when TanStack Table asks for the `__global__` faceting context. Column-specific faceting can still use separate values by checking for each column ID. diff --git a/docs/framework/lit/guide/global-filtering.md b/docs/framework/lit/guide/global-filtering.md new file mode 100644 index 0000000000..a109f0843d --- /dev/null +++ b/docs/framework/lit/guide/global-filtering.md @@ -0,0 +1,256 @@ +--- +title: Global Filtering (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Column Filters](../examples/filters) +- [Fuzzy Search](../examples/filters-fuzzy) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, globalFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/lit-table' + +const features = tableFeatures({ globalFilteringFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Global Filtering (Lit) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +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 Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +import { + TableController, + tableFeatures, + globalFilteringFeature, +} from '@tanstack/lit-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = this.tableController.table({ + features, + rowModels: {}, // no filteredRowModel needed for manual server-side global filtering + data: this.data, + columns, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` to your features and the `filteredRowModel` to your row models: + +```ts +import { + TableController, + tableFeatures, + globalFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/lit-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + // other options... +}) +``` + +### Global Filter Function + +The globalFilterFn option allows you to specify the filter function that will be used for global filtering. The filter function can be a string that references a built-in filter function, a string that references a custom filter function provided via the tableOptions.filterFns option, or a custom filter function. + +```ts +const table = this.tableController.table({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + data: this.data, + columns, + globalFilterFn: 'text', // built-in filter function +}) +``` + +By default there are 10 built-in filter functions to choose from: + +- includesString - Case-insensitive string inclusion +- includesStringSensitive - Case-sensitive string inclusion +- equalsString - Case-insensitive string equality +- equalsStringSensitive - Case-sensitive string equality +- arrIncludes - Item inclusion within an array +- arrIncludesAll - All items included in an array +- arrIncludesSome - Some items included in an array +- equals - Object/referential equality Object.is/=== +- weakEquals - Weak object/referential equality == +- inNumberRange - Number range inclusion + +You can also define your own custom filter functions either as the globalFilterFn table option. + +### Global Filter State + +The global filter state is stored in the table's state atoms and can be read with `table.atoms.globalFilter.get()` or from the current `table.store.state.globalFilter` snapshot. If you want to persist the global filter state outside of the table, use the `state.globalFilter` and `onGlobalFilterChange` options together. + +```ts +@state() +private globalFilter: any = [] + +const table = this.tableController.table({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + state: { + globalFilter: this.globalFilter, + }, + onGlobalFilterChange: (updater) => { + this.globalFilter = typeof updater === 'function' ? updater(this.globalFilter) : updater + }, +}) +``` + +The global filtering state is defined as an object with the following shape: + +```ts +interface GlobalFilter { + globalFilter: any +} +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. + +```ts +html` + table.setGlobalFilter((e.target as HTMLInputElement).value)} + placeholder="Search all columns..." + /> +` +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the globalFilterFn option. + +> **Note:** It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering.md). + +```ts +const customFilterFn = (rows, columnId, filterValue) => { + // custom filter logic +} + +const table = this.tableController.table({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table initialState option. + +However, you can also just specify the initial global filter state in the state.globalFilter option. + +```ts +@state() +private globalFilter = "search term" //recommended to initialize globalFilter state here + +const table = this.tableController.table({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + initialState: { + globalFilter: 'search term', // if not managing globalFilter state, set initial state here + }, + state: { + globalFilter: this.globalFilter, // pass our managed globalFilter state to the table + }, +}) +``` + +> NOTE: Do not use both initialState.globalFilter and state.globalFilter at the same time, as the initialized state in the state.globalFilter will override the initialState.globalFilter. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +const table = this.tableController.table({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` diff --git a/docs/framework/lit/guide/grouping.md b/docs/framework/lit/guide/grouping.md new file mode 100644 index 0000000000..c019284490 --- /dev/null +++ b/docs/framework/lit/guide/grouping.md @@ -0,0 +1,241 @@ +--- +title: Grouping (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Grouping](../examples/grouping) + +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, columnGroupingFeature, createGroupedRowModel, aggregationFns } from '@tanstack/lit-table' + +const features = tableFeatures({ columnGroupingFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Grouping (Lit) Guide + +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 left, center (unpinned), and right pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +To use the grouping feature, add the `columnGroupingFeature` to your features and the `groupedRowModel` to your row models. The grouped row model is responsible for grouping the rows based on the grouping state. + +```ts +import { + TableController, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, + aggregationFns, +} from '@tanstack/lit-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```ts +const features = tableFeatures({ columnGroupingFeature, rowExpandingFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```ts +table.setGrouping(['column1', 'column2']); +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```ts +table.resetGrouping(); +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```ts +const table = this.tableController.table({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows by columns using the aggregationFn option. This is a string that is the ID of the aggregation function. You can define the aggregation functions using the aggregationFns option. + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'sum', +}) +``` + +In the above example, the sum aggregation function will be used to aggregate the data in the grouped rows. +By default, numeric columns will use the sum aggregation function, and non-numeric columns will use the count aggregation function. You can override this behavior by specifying the aggregationFn option in the column definition. + +There are several built-in aggregation functions that you can use: + +- sum - Sums the values in the grouped rows. +- count - Counts the number of rows in the grouped rows. +- min - Finds the minimum value in the grouped rows. +- max - Finds the maximum value in the grouped rows. +- extent - Finds the extent (min and max) of the values in the grouped rows. +- mean - Finds the mean of the values in the grouped rows. +- median - Finds the median of the values in the grouped rows. +- unique - Returns an array of unique values in the grouped rows. +- uniqueCount - Counts the number of unique values in the grouped rows. + +#### Custom Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows using the aggregationFns option. This is a record where the keys are the IDs of the aggregation functions, and the values are the aggregation functions themselves. You can then reference these aggregation functions in a column's aggregationFn option. + +```ts +const table = this.tableController.table({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel({ + ...aggregationFns, + myCustomAggregation: (columnId, leafRows, childRows) => { + // return the aggregated value + }, + }), + }, + // other options... +}) +``` + +In the above example, myCustomAggregation is a custom aggregation function that takes the column ID, the leaf rows, and the child rows, and returns the aggregated value. You can then use this aggregation function in a column's aggregationFn option: + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'myCustomAggregation', +}) +``` + +### Manual Grouping + +If you are doing server-side grouping and aggregation, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to manually group the rows before passing them to the table. + +```ts +const table = this.tableController.table({ + features: tableFeatures({ columnGroupingFeature }), + rowModels: {}, // no groupedRowModel needed for manual grouping + // other options... + manualGrouping: true, +}) +``` + +> **Note:** There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Grouping Change Handler + +If you want to manage the grouping state yourself, you can use the onGroupingChange option. This option is a function that is called when the grouping state changes. You can pass the managed state back to the table via the tableOptions.state.grouping option. + +```ts +@state() +private grouping: string[] = [] + +const table = this.tableController.table({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + state: { + grouping: this.grouping, + }, + onGroupingChange: (updater) => { + this.grouping = typeof updater === 'function' ? updater(this.grouping) : updater + }, +}) +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```ts +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +column.getAutoAggregationFn() +column.getAggregationFn() +``` + +Rows expose grouping helpers for grouped row rendering: + +```ts +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose helpers for choosing between grouped, aggregated, placeholder, and normal cell rendering: + +```ts +cell.getIsGrouped() +cell.getIsAggregated() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```ts +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/lit/guide/pagination.md b/docs/framework/lit/guide/pagination.md new file mode 100644 index 0000000000..b5dd834d5f --- /dev/null +++ b/docs/framework/lit/guide/pagination.md @@ -0,0 +1,251 @@ +--- +title: Pagination (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Pagination](../examples/pagination) + +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/lit-table' + +const features = tableFeatures({ rowPaginationFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Pagination (Lit) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain ***ALL*** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. + +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. + +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 pagination and then switch to server-side pagination in the future as your data grows. + +#### Should You Use Virtualization Instead? + +Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` to your features and the `paginatedRowModel` to your row models: + +```ts +import { + TableController, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/lit-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data, +}) +``` + +> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. + +```ts +import { + TableController, + tableFeatures, + rowPaginationFeature, +} from '@tanstack/lit-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = this.tableController.table({ + features, + rowModels: {}, // no paginatedRowModel needed for server-side pagination + columns, + data: this.data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +You can manage the `pagination` state just like any other state in the table instance. + +```ts +import { + TableController, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/lit-table' + +const features = tableFeatures({ rowPaginationFeature }) + +@state() +private pagination: PaginationState = { + pageIndex: 0, // initial page index + pageSize: 10, // default page size +} + +const table = this.tableController.table({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data, + onPaginationChange: (updater) => { + this.pagination = typeof updater === 'function' ? updater(this.pagination) : updater + }, + state: { + pagination: this.pagination, + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```ts +const table = this.tableController.table({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT pass the `pagination` state to both the `state` and `initialState` options. `state` will overwrite `initialState`. Only use one or the other. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` when page-altering state changes occur, such as when the `data` is updated, filters change, grouping changes, etc. This behavior is automatically disabled when `manualPagination` is true but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. + +```ts +const table = this.tableController.table({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data: this.data, + autoResetPageIndex: false, // turn off auto reset of pageIndex +}) +``` + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> **Note**: These pagination APIs are available when using `rowPaginationFeature`. + +```ts +html` + + + + + +` +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/lit/guide/row-pinning.md b/docs/framework/lit/guide/row-pinning.md new file mode 100644 index 0000000000..8ffaab0247 --- /dev/null +++ b/docs/framework/lit/guide/row-pinning.md @@ -0,0 +1,220 @@ +--- +title: Row Pinning (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Row Pinning](../examples/row-pinning) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, rowPinningFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ rowPinningFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Row Pinning (Lit) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory, so `rowModels` can stay empty unless your table uses other row-model features. + +```ts +import { + rowPinningFeature, + tableFeatures, + TableController, +} from '@tanstack/lit-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, +}) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```ts +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```ts +const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, use `state.rowPinning` with `onRowPinningChange`. + +```ts +@state() +private rowPinning: RowPinningState = { + top: [], + bottom: [], +} + +const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + state: { + rowPinning: this.rowPinning, + }, + onRowPinningChange: (updater) => { + this.rowPinning = typeof updater === 'function' ? updater(this.rowPinning) : updater + }, +}) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```ts +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +} + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```ts +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls: + +```ts +row.getCanPin() + ? html` +
+ + + +
+ ` + : null +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```ts +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly: + +```ts +html` + + ${table.getTopRows().map((row) => html``)} + ${table.getCenterRows().map((row) => html``)} + ${table.getBottomRows().map((row) => html``)} + +` +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```ts +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```ts +const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + enableRowPinning: row => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```ts +const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/lit/guide/row-selection.md b/docs/framework/lit/guide/row-selection.md new file mode 100644 index 0000000000..9f3559a66d --- /dev/null +++ b/docs/framework/lit/guide/row-selection.md @@ -0,0 +1,212 @@ +--- +title: Row Selection (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Row Selection](../examples/row-selection) +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, rowSelectionFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ rowSelectionFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Row Selection (Lit) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you (though as seen down below, it may be more convenient to manage the row selection state in your own scope). You can access the internal row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the current row selection state +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +> Note: If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +Even though the table instance will already manage the row selection state for you, it is usually more convenient to manage the state yourself in order to have easy access to the selected row ids that you can use to make API calls or other actions. + +Use the `onRowSelectionChange` table option to hoist up the row selection state to your own scope. Then pass the row selection state back to the table instance using in the `state` table option. + +```ts +import { TableController, tableFeatures, rowSelectionFeature } from '@tanstack/lit-table' + +const features = tableFeatures({ rowSelectionFeature }) + +@state() +private rowSelection: RowSelectionState = {} + +const table = this.tableController.table({ + features, + rowModels: {}, + //... + onRowSelectionChange: (updater) => { + this.rowSelection = typeof updater === 'function' ? updater(this.rowSelection) : updater + }, + state: { + rowSelection: this.rowSelection, + }, +}) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +const table = this.tableController.table({ + features, + rowModels: {}, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +const table = this.tableController.table({ + //... + enableRowSelection: row => row.original.age > 18, //only enable row selection for adults +}) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +const table = this.tableController.table({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +}) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +const table = this.tableController.table({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +}) +``` + +### Render Row Selection UI + +TanStack table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These function automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +```ts +html` + + + +` +``` + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. + +```ts +html` + + ${table.getRowModel().rows.map( + (row) => html` + + ${row.getVisibleCells().map((cell) => html`${FlexRender({ cell })}`)} + + `, + )} + +` +``` diff --git a/docs/framework/lit/guide/sorting.md b/docs/framework/lit/guide/sorting.md new file mode 100644 index 0000000000..431fc63701 --- /dev/null +++ b/docs/framework/lit/guide/sorting.md @@ -0,0 +1,482 @@ +--- +title: Sorting (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Sorting](../examples/sorting) + +### Lit Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { TableController, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/lit-table' + +const features = tableFeatures({ rowSortingFeature }) + +@customElement('my-table') +class MyTable extends LitElement { + @state() + private data = defaultData + + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data: this.data, + }) + + return html`...` + } +} +``` + +## Sorting (Lit) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```ts +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +You can access the sorting state directly from the table instance with `table.atoms.sorting.get()` or from the current `table.store.state` snapshot. + +```ts +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + //... +}) + +console.log(table.atoms.sorting.get()) // access the current sorting state +``` + +However, if you need to access the sorting state before the table is initialized, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state, you can control/manage the sorting state in your own state management with the `state.sorting` and `onSortingChange` table options. + +```ts +@state() +private sorting: SortingState = [] // can set initial sorting state here +//... +// use sorting state to fetch data from your server or something... +//... +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + //... + state: { + sorting: this.sorting, + }, + onSortingChange: (updater) => { + this.sorting = typeof updater === 'function' ? updater(this.sorting) : updater + }, +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```ts +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + //... + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. + +```ts +@state() +private sorting: SortingState = [] +//... +const table = this.tableController.table({ + features: tableFeatures({ rowSortingFeature }), // feature needed for sorting state/APIs + rowModels: {}, // no sortedRowModel needed for manual sorting + columns, + data: this.data, + manualSorting: true, // use pre-sorted row model instead of sorted row model + state: { + sorting: this.sorting, + }, + onSortingChange: (updater) => { + this.sorting = typeof updater === 'function' ? updater(this.sorting) : updater + }, +}) +``` + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` to your features and the `sortedRowModel` to your row models. Import `createSortedRowModel` and `sortFns` from TanStack Table: + +```ts +import { + TableController, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFns, +} from '@tanstack/lit-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = this.tableController.table({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data: this.data, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions either as the `sortFn` column option, or as a global sorting function using the `sortFns` table option. + +#### Custom Sorting Functions + +When defining a custom sorting function in either the `sortFns` table option or as a `sortFn` column option, it should have the following signature: + +```ts +//optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = (rowA: Row, rowB: Row, columnId: string) => { + return //-1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> Note: The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // use custom global sorting function + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + } +] +//... +const table = this.tableController.table({ + features, + rowModels: { + sortedRowModel: createSortedRowModel({ + ...sortFns, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }), + }, + columns, + data: this.data, +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```ts +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, //sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, //sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + sortDescFirst: true, //sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> **NOTE**: You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +In not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> NOTE: `'first'` and `'last'` options are available in v9. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this: + +`'none' -> 'desc' -> 'asc' -> 'none' -> 'desc' -> 'asc' -> ...` + +If you disable sorting removal, the behavior will be like this: + +`'none' -> 'desc' -> 'asc' -> 'desc' -> 'asc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```ts +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (always none -> asc -> desc -> asc) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```ts +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```ts +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```ts +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```ts +const table = this.tableController.table({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data: this.data, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/lit/guide/virtualization.md b/docs/framework/lit/guide/virtualization.md new file mode 100644 index 0000000000..d9487facbd --- /dev/null +++ b/docs/framework/lit/guide/virtualization.md @@ -0,0 +1,229 @@ +--- +title: Virtualization (Lit) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Lit examples: + +- [Virtualized Columns](../examples/virtualized-columns) +- [Virtualized Rows](../examples/virtualized-rows) +- [Virtualized Infinite Scrolling](../examples/virtualized-infinite-scrolling) + +### Lit Setup + +Install and import the Lit virtualizer adapter from `@tanstack/lit-virtual`. TanStack Table still owns rows, columns, and table state; the virtualizer owns scroll indexes and measurements. +Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/react/examples/table). + +## Virtualization (Lit) Guide + +The TanStack Table packages do not come with any virtualization APIs or features built in. Virtualization is a rendering strategy, not a table feature. You can use TanStack Table with any virtualization library, but the official examples use TanStack Virtual. + +TanStack Table and TanStack Virtual solve different parts of the problem: + +- TanStack Table builds the row models, columns, headers, cells, sizing, sorting, filtering, and other table state. +- TanStack Virtual decides which item indexes should be rendered for the current scroll position. +- Your table renderer maps those virtual indexes back to rows, headers, and cells. + +### When To Use Virtualization + +Use virtualization when your table has a very large number of rows, columns, or both. Virtualization keeps the DOM small by only rendering the items that are visible in the scroll viewport plus a small overscan buffer. + +Virtualization is not a replacement for server-side pagination, filtering, or sorting. If the data is virtualized on the client, the data still needs to exist on the client. If your dataset is too large to load into the browser, use server-side data operations or infinite scrolling. + +For small tables, normal rendering is simpler and usually preferable. + +### Install TanStack Virtual + +Install the Lit virtualizer adapter: + +```sh +npm install @tanstack/lit-virtual +``` + +The Lit examples use `VirtualizerController` from `@tanstack/lit-virtual`. TanStack Table still owns rows, columns, headers, cells, sizing, sorting, filtering, and other table state; TanStack Virtual decides which item indexes should render for the current scroll position. +### The Basic Pattern + +Most virtualized table implementations follow the same pattern: + +1. Create a fixed-height scroll container. +2. Pass the scroll element to the virtualizer. +3. Use `table.getRowModel().rows` or `table.getVisibleLeafColumns()` as the source list. +4. Configure `count`, `estimateSize`, `overscan`, and optional `measureElement`. +5. Render only virtual items. +6. Use virtual offsets or spacer padding to preserve the full scroll geometry. + +Here is a compact row virtualization example: + +```ts +const rows = table.getRowModel().rows + +const rowVirtualizer = new VirtualizerController(this, { + count: rows.length, + getScrollElement: () => this.tableContainer, + estimateSize: () => 33, + overscan: 5, +}) + +return html` + + ${rowVirtualizer.getVirtualItems().map((virtualRow) => { + const row = rows[virtualRow.index] + return html` + + ${row.getVisibleCells().map((cell) => html`${FlexRender({ cell })}`)} + + ` + })} + +` +``` + +### Virtualized Rows + +The [virtualized rows examples](../examples/virtualized-rows) show how to render large row counts while keeping the DOM small. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The core idea is that sorting, filtering, grouping, and other row-model work still comes from TanStack Table. The virtualizer reads from the final table row model: + +```ts +const rows = table.getRowModel().rows +``` + +The row virtualizer is configured with `count: rows.length`, a row height estimate, the scroll container, and an overscan value. The `tbody` is given the full virtual height with `rowVirtualizer.getTotalSize()`, while each rendered row is absolutely positioned with `transform: translateY(...)`. + +The examples render cells from the current row with APIs like `row.getVisibleCells()` or `row.getAllCells()`, depending on whether the example needs visibility-aware cells or all cells. + +The official examples use large generated datasets, commonly tens or hundreds of thousands of rows. They also support dynamic row heights by using `measureElement` when possible. The examples skip dynamic row measurement in Firefox because Firefox can measure table border height differently. + +### Virtualized Columns + +The [virtualized columns examples](../examples/virtualized-columns) show how to render large row and column counts. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +Column virtualization uses the current visible column list: + +```ts +const visibleColumns = table.getVisibleLeafColumns() +``` + +The column virtualizer is configured for horizontal virtualization: + +```ts +const columnVirtualizer = VirtualizerController({ + count: visibleColumns.length, + estimateSize: index => visibleColumns[index].getSize(), + getScrollElement: () => tableContainerRef.current, + horizontal: true, + overscan: 3, +}) +``` + +Column virtualization uses a different rendering strategy than row virtualization. Instead of absolutely positioning columns, the examples add fake spacer cells to the left and right: + +```ts +const virtualColumns = columnVirtualizer.getVirtualItems() +const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 +const virtualPaddingRight = + columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) +``` + +Those spacer cells preserve the horizontal scroll width while the renderer only mounts the virtual columns. This approach keeps row rendering table-like and allows dynamic row height measurement to keep working. + +### Virtualized Rows And Columns Together + +The official virtualized columns examples also virtualize rows. In those examples: + +- The row virtualizer controls vertical positioning and total body height. +- The column virtualizer controls horizontal header/cell rendering and left/right spacer cells. +- `virtualRow.index` maps to `rows[virtualRow.index]`. +- `virtualColumn.index` maps to `visibleCells[virtualColumn.index]`. + +Always use virtual indexes against the same current row and column lists returned by the table. If sorting, filtering, pagination, grouping, or column visibility changes, recompute the virtualized rows and columns from the current table state. + +### Virtualized Infinite Scrolling + +The [virtualized infinite scrolling examples](../examples/virtualized-infinite-scrolling) combine row virtualization with progressive data fetching. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The common pattern is: + +1. Fetch a page of rows. +2. Flatten fetched pages into the table `data`. +3. Use row virtualization over the loaded rows. +4. Listen to scroll events on the table container. +5. Fetch the next page when the user scrolls near the bottom. + +The Lit infinite scrolling pattern can use TanStack Query or any other data-fetching layer. + +```ts +const { scrollHeight, scrollTop, clientHeight } = scrollElement + +if (scrollHeight - scrollTop - clientHeight < 500) { + fetchNextPage() +} +``` + +If sorting is handled by the server, use manual sorting so the fetched data reflects the whole backend dataset rather than only the currently loaded rows. When sorting changes and the fetched dataset is replaced, scroll back to the top with `rowVirtualizer.scrollToIndex(0)`. + +### Dynamic Row Heights + +Dynamic row heights are useful when content can wrap or expand. They are also more complex than fixed-height rows. + +Use `estimateSize` as the virtualizer's initial guess: + +```ts +estimateSize: () => 33 +``` + +Then use `measureElement` to refine the actual row height after rendering: + +```ts +html` + + this.rowVirtualizerController + .getVirtualizer() + .measureElement(node ?? null), + )} + > + +` +``` + +Set `data-index` on each row so the virtualizer can associate measurements with the correct item. In non-React adapters, call `measureElement` through the adapter-appropriate ref, action, directive, or controller. + +Overscan helps avoid blank regions while measurements settle. If every row has a known fixed height, skip dynamic measurement and use the fixed height estimate instead. + +### Sticky Headers And Semantic Table Markup + +The examples still use semantic table tags, but they change table layout CSS to support virtual positioning and sticky headers. + +Dynamic row virtualization commonly requires: + +```css +table { + display: grid; +} + +thead { + display: grid; + position: sticky; + top: 0; +} + +tr { + display: flex; +} +``` + +Rows are absolutely positioned inside a relatively positioned `tbody`, and cells use flex sizing so they can match `column.getSize()` or `cell.column.getSize()`. This is intentional. Native table layout does not work well with dynamic-height virtual rows that are positioned independently. + +### Performance Tips + +- Keep virtualizers near the components that render the virtualized items. +- Avoid re-rendering the full table body on every scroll. +- Keep row, column, and data references stable where possible. +- Use `overscan` deliberately. More overscan reduces visible blanking, while less overscan reduces DOM nodes. +- Avoid expensive cell renderers in very large virtualized tables. +- Test production builds. Framework development builds can be slower than production builds; profile production bundles before optimizing. +- Prefer fixed row sizes when the UI allows it. +- For column virtualization, use `column.getSize()`, `header.getSize()`, and `cell.column.getSize()` consistently. diff --git a/docs/framework/preact/guide/column-faceting.md b/docs/framework/preact/guide/column-faceting.md new file mode 100644 index 0000000000..fa99a946d4 --- /dev/null +++ b/docs/framework/preact/guide/column-faceting.md @@ -0,0 +1,111 @@ +--- +title: Column Faceting (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Faceted Filters](../examples/filters-faceted) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/preact-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, +}) +``` + +## Column Faceting (Preact) Guide + +Column Faceting is a feature that allows you to generate lists of values for a given column from that column's data. For example, a list of unique values in a column can be generated from all rows in that column to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a column of numbers to be used as a range for a range slider filter component. + +### Column Faceting Row Models + +In order to use any of the column faceting features, add the `columnFacetingFeature` to your features and the appropriate faceted row models to `rowModels`. + +```ts +import { + useTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/preact-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = useTable({ + features, + rowModels: { + 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 + }, + columns, + 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]; +``` + +### Custom (Server-Side) Faceting + +If 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. You can use the `getFacetedUniqueValues` and `getFacetedMinMaxValues` table options to resolve the faceted values from the server-side. + +```ts +const facetingQuery = useQuery( + //... +) + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, + getFacetedUniqueValues: (table, columnId) => { + const uniqueValueMap = new Map() + //... + return uniqueValueMap + }, + getFacetedMinMaxValues: (table, columnId) => { + //... + return [min, max] + }, + //... +}) +``` + +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/preact/guide/column-filtering.md b/docs/framework/preact/guide/column-filtering.md new file mode 100644 index 0000000000..e72b45cdd0 --- /dev/null +++ b/docs/framework/preact/guide/column-filtering.md @@ -0,0 +1,368 @@ +--- +title: Column Filtering (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Fuzzy Search](../examples/filters-fuzzy) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/preact-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` + +## Column Filtering (Preact) 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 `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```tsx +const table = useTable({ + features: tableFeatures({ columnFilteringFeature }), + rowModels: {}, // no filteredRowModel needed for manual server-side filtering + 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` to your features and the `filteredRowModel` to your row models. Import `createFilteredRowModel` and `filterFns` from TanStack Table: + +```tsx +import { + useTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/preact-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + 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 + +You can access the column filter state from the table instance with `table.atoms.columnFilters.get()` or from the current `table.store.state` snapshot. + +```tsx +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.columnFilters.get()) // access the current column filters state +``` + +However, if you need to access the column filter state before the table is initialized, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state, you can control/manage the column filter state in your own state management with the `state.columnFilters` and `onColumnFiltersChange` table options. + +```tsx +const [columnFilters, setColumnFilters] = useState([]) // can set initial column filter state here +//... +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + state: { + columnFilters, + }, + onColumnFiltersChange: setColumnFilters, +}) +``` + +#### 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`. + +```tsx +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 10 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `arrIncludes` - Item inclusion within an array +- `arrIncludesAll` - All items included in an array +- `arrIncludesSome` - Some items included in an array +- `equals` - Object/referential equality `Object.is`/`===` +- `weakEquals` - Weak object/referential equality `==` +- `inNumberRange` - Number range inclusion + +You can also define your own custom filter functions either as the `filterFn` column option, or as a global filter function using the `filterFns` table option. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +When defining a custom filter function in either the `filterFn` column option or the `filterFns` table option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = (row: Row, columnId: string, filterValue: any, addMeta: (meta: any) => 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. + +```tsx +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // use custom global filter function + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + } +] +//... +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }), + }, + columns, + data, +}) +``` + +##### 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`. + +```tsx +const startsWithFilterFn = ( + row: Row, + columnId: string, + filterValue: number | string, //resolveFilterValue will transform this 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. + +```tsx +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + enableColumnFilters: false, // disable column filtering for all columns +}) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```tsx +const table = useTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + filterFromLeafRows: true, // filter and search through sub-rows +}) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```tsx +const table = useTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +}) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `onChange` or `onBlur` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/preact/guide/column-ordering.md b/docs/framework/preact/guide/column-ordering.md new file mode 100644 index 0000000000..4da6ae0143 --- /dev/null +++ b/docs/framework/preact/guide/column-ordering.md @@ -0,0 +1,148 @@ +--- +title: Column Ordering (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Column Ordering](../examples/column-ordering) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnOrderingFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Ordering (Preact) 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 left, center (unpinned), and right 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. + +```tsx +const table = useTable({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + 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. + +```tsx +const [columnOrder, setColumnOrder] = useState(['columnId1', 'columnId2', 'columnId3']) +//... +const table = useTable({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + state: { + columnOrder, + //... + } + onColumnOrderChange: setColumnOrder, + //... +}); +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, you can set up the logic something like this: + +```tsx +const [columnOrder, setColumnOrder] = useState(columns.map(c => c.id)); + +//depending on your dnd solution of choice, you may or may not need state like this +const [movingColumnId, setMovingColumnId] = useState(null); +const [targetColumnId, setTargetColumnId] = useState(null); + +//util function to splice and reorder the columnOrder array +const reorderColumn = ( + movingColumnId: Column, + targetColumnId: Column, +): string[] => { + const newColumnOrder = [...columnOrder]; + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + newColumnOrder.splice(newColumnOrder.indexOf(movingColumnId), 1)[0], + ); + setColumnOrder(newColumnOrder); +}; + +const handleDragEnd = (e: DragEvent) => { + if(!movingColumnId || !targetColumnId) return; + setColumnOrder(reorderColumn(movingColumnId, targetColumnId)); +}; + +//use your dnd solution of choice +``` + +### 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. + +```tsx +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. + +```tsx +column.getIndex() +column.getIndex('left') +column.getIndex('center') +column.getIndex('right') + +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 (Preact) + +There are undoubtedly many ways to implement drag and drop features along-side TanStack Table. Here are a few suggestions in order for you to not have a bad time: + +1. Do NOT try to use [`"react-dnd"`](https://react-dnd.github.io/react-dnd/docs/overview) _if you are using Preact or newer_. framework-specific DnD was an important library for its time, but it now does not get updated very often, and it has incompatibilities with Preact, especially in Preact development mode. It is still possible to get it to work, but there are newer alternatives that have better compatibility and are more actively maintained. framework-specific DnD's Provider may also interfere and conflict with any other DnD solutions you may want to try in your app. + +2. Use [`"@dnd-kit/core"`](https://dndkit.com/). DnD Kit is a modern, modular and lightweight drag and drop library that is highly compatible with the modern Preact ecosystem, and it works well with semantic `` markup. The official framework-specific DnD examples, Column DnD and Row DnD, use DnD Kit. + +3. Consider other DnD libraries like [`"react-beautiful-dnd"`](https://github.com/atlassian/react-beautiful-dnd), but be aware of their potentially large bundle sizes, maintenance status, and compatibility with `
` markup. + +4. Consider using native browser events and state management to implement lightweight drag and drop features. However, be aware that this approach may not be best for mobile users if you do not go the extra mile to implement proper touch events. [Material React Table V2](https://www.material-react-table.com/docs/examples/column-ordering) is an example of a library that implements TanStack Table with only browser drag and drop events such as `onDragStart`, `onDragEnd`, `onDragEnter` and no other dependencies. Browse its source code to see how it is done. diff --git a/docs/framework/preact/guide/column-pinning.md b/docs/framework/preact/guide/column-pinning.md new file mode 100644 index 0000000000..2fc168fdf8 --- /dev/null +++ b/docs/framework/preact/guide/column-pinning.md @@ -0,0 +1,161 @@ +--- +title: Column Pinning (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnPinningFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ columnPinningFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Pinning (Preact) 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 left or right. + +### 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 left, center (unpinned), and right 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.left` and `columnPinning.right` 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. + +```tsx +import { useTable, tableFeatures, columnPinningFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ columnPinningFeature }) + +const [columnPinning, setColumnPinning] = useState({ + left: [], + right: [], +}) + +const table = useTable({ + features, + rowModels: {}, + //... + state: { + columnPinning, + //... + }, + onColumnPinningChange: setColumnPinning, + //... +}) +``` + +### 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 + +```tsx +const table = useTable({ + features, + rowModels: {}, + //... + initialState: { + columnPinning: { + left: ['expand-column'], + right: ['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 left or right. 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 `left` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `right` 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. + +```tsx +table.setColumnPinning({ + left: ['firstName'], + right: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```tsx +table.getLeftLeafColumns() +table.getCenterLeafColumns() +table.getRightLeafColumns() + +table.getLeftVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getRightVisibleLeafColumns() + +table.getLeftHeaderGroups() +table.getCenterHeaderGroups() +table.getRightHeaderGroups() + +table.getLeftFooterGroups() +table.getCenterFooterGroups() +table.getRightFooterGroups() + +table.getLeftFlatHeaders() +table.getCenterFlatHeaders() +table.getRightFlatHeaders() + +table.getLeftLeafHeaders() +table.getCenterLeafHeaders() +table.getRightLeafHeaders() +``` + +You can also request pinned leaf columns by region with `table.getPinnedLeafColumns(position)` and visible pinned leaf columns with `table.getPinnedVisibleLeafColumns(position)`. + +```tsx +table.getPinnedLeafColumns('left') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('right') + +table.getPinnedVisibleLeafColumns('left') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('right') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'left'` or `'right'` to check one pinned side. + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getLeftHeaderGroups`, `table.getCenterHeaderGroups`, `table.getRightHeaderGroups`, `row.getLeftVisibleCells`, `row.getCenterVisibleCells`, and `row.getRightVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/preact/guide/column-resizing.md b/docs/framework/preact/guide/column-resizing.md new file mode 100644 index 0000000000..4454661f71 --- /dev/null +++ b/docs/framework/preact/guide/column-resizing.md @@ -0,0 +1,216 @@ +--- +title: Column Resizing (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnResizingFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ columnResizingFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Resizing (Preact) 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 `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. + +```tsx +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + useTable, +} from '@tanstack/preact-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, + rowModels: {}, + 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 React 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 React 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. + +```tsx +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"`. + +```tsx +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() +``` + +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. + +```tsx + +) +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `useState`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/preact/guide/expanding.md b/docs/framework/preact/guide/expanding.md new file mode 100644 index 0000000000..35a1cdddaf --- /dev/null +++ b/docs/framework/preact/guide/expanding.md @@ -0,0 +1,289 @@ +--- +title: Expanding (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Expanding](../examples/expanding) + +### Preact Setup + +```tsx +import { useTable, tableFeatures, rowExpandingFeature, createExpandedRowModel } from '@tanstack/preact-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = useTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, +}) +``` + +## Expanding Feature (Preact) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` to your features and the `expandedRowModel` to your row models: + +```ts +import { + useTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/preact-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = useTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are essentially child rows that inherit the same column structure as their parent rows. If your data object already includes these expanded rows data, you can utilize the `getSubRows` function to specify these child rows. However, if your data object does not contain the expanded rows data, they can be treated as custom expanded data, which is discussed in next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 } + ] + }, + { id: 3, + name: 'Doe', + age: 40, + children: [ + { id: 4, name: 'Alice', age: 10 } + ] + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +const table = useTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +}) +``` + +> **Note:** You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +```tsx +import { Fragment } from 'preact' + +//... +const table = useTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + getRowCanExpand: (row) => true, // Add your logic to determine if a row can be expanded. True means all rows include expanded data + // other options... +}) +//... + + {table.getRowModel().rows.map((row) => ( + + {/* Normal row UI */} + + {row.getVisibleCells().map((cell) => ( + + ))} + + {/* If the row is expanded, render the expanded UI as a separate row with a single cell that spans the width of the table */} + {row.getIsExpanded() && ( + + + + )} + + ))} + +//... +``` + +### Expanded rows state + +If you need to control the expanded state of the rows in your table, you can do so by using the expanded state and the `onExpandedChange` option. This allows you to manage the expanded state according to your requirements. + +```ts +const [expanded, setExpanded] = useState({}) + +const table = useTable({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + state: { + expanded, + }, + onExpandedChange: setExpanded, +}) +``` + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows with IDs present as keys in the record and have their value set to true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. For example, you can add a button UI within the columns definition. + +```tsx +const columns = [ + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'age', + header: 'Age', + }, + { + header: 'Children', + cell: ({ row }) => { + return row.getCanExpand() ? + + : ''; + }, + }, +] +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```tsx +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```tsx +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, the filtering process starts from the parent rows and moves downwards. This means if a parent row is excluded by the filter, all its child rows will also be excluded. However, you can change this behavior by using the `filterFromLeafRows` option. When this option is enabled, the filtering process starts from the leaf (child) rows and moves upwards. This ensures that a parent row will be included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. Additionally, you can control how deep into the child hierarchy the filter process goes by using the `maxLeafRowFilterDepth` option. This option allows you to specify the maximum depth of child rows that the filter should consider. + +```ts +//... +const table = useTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parents page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +const table = useTable({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Pinning Guide](../../../guide/pinning.md) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const table = useTable({ + features: tableFeatures({ rowExpandingFeature }), + rowModels: {}, // no expandedRowModel needed for manual expanding + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/preact/guide/fuzzy-filtering.md b/docs/framework/preact/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..13c6cb0beb --- /dev/null +++ b/docs/framework/preact/guide/fuzzy-filtering.md @@ -0,0 +1,151 @@ +--- +title: Fuzzy Filtering (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Fuzzy Search](../examples/filters-fuzzy) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/preact-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` + +## Fuzzy Filtering (Preact) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement a client side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> **Note:** You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked in order to work better with TanStack Table's row by row filtering approach. + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +Here's an example of a custom fuzzy filter function: + +```typescript +import { rankItem } from '@tanstack/match-sorter-utils'; +import { FilterFn } from '@tanstack/preact-table' + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the rankItem function from the @tanstack/match-sorter-utils library to rank the item. We then store the ranking information in the meta data of the row, and return whether the item passed the ranking criteria. + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, you can specify the fuzzy filter function in the globalFilterFn option of the table instance: + +```typescript +import { + useTable, + tableFeatures, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + filterFns, + sortFns, +} from '@tanstack/preact-table' + +const features = tableFeatures({ globalFilteringFeature, rowSortingFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + fuzzy: fuzzyFilter, + }), + sortedRowModel: createSortedRowModel(sortFns), // needed if you want sorting with fuzzy rank + }, + columns, + data, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, pass your fuzzy filter function to `createFilteredRowModel` (merging it with the built-in `filterFns`). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```typescript +const column = [ + { + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +]; +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```typescript +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFns } from '@tanstack/preact-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId]?.itemRank!, + rowB.columnFiltersMeta[columnId]?.itemRank! + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then specify this sorting function in the sortFn option of the column definition: + +```typescript +{ + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + sortFn: 'fuzzySort', //using our custom fuzzy sort function +} +``` diff --git a/docs/framework/preact/guide/global-faceting.md b/docs/framework/preact/guide/global-faceting.md new file mode 100644 index 0000000000..993144b3b8 --- /dev/null +++ b/docs/framework/preact/guide/global-faceting.md @@ -0,0 +1,107 @@ +--- +title: Global Faceting (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Faceted Filters](../examples/filters-faceted) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/preact-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, +}) +``` + +## Global Faceting (Preact) Guide + +Global Faceting allows you to generate lists of values for all columns from the table's data. For example, a list of unique values in a table can be generated from all rows in all columns to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a table of numbers to be used as a range for a range slider filter component. + +### Global Faceting Row Models + +In order to use any of the global faceting features, add the appropriate faceted row models to your `rowModels`: + +```ts +import { + useTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/preact-table' + +const features = tableFeatures({ columnFacetingFeature }) +// If the table also uses global filtering, include globalFilteringFeature too. + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), // required (other faceting methods depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + }, + // other options... +}) +``` + +### Use Global Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting table instance APIs to access the lists of values generated by the faceted row models. + +Use `table.getGlobalFacetedRowModel()` if you need to inspect the row model used to derive global facet 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 Global (Server-Side) Faceting + +If 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 `rowModels.facetedUniqueValues` and `rowModels.facetedMinMaxValues` factories. Global faceting requests use the internal `__global__` column ID. + +```ts +const globalFacets = await fetch('/api/faceting').then(res => res.json()) + +const table = useTable({ + features, + rowModels: { + facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() + return new Map(globalFacets.uniqueValues) + }, + facetedMinMaxValues: (_table, columnId) => () => { + if (columnId !== '__global__') return undefined + return globalFacets.minMaxValues + }, + }, + // other options... +}) +``` + +In this example, the custom factories return server-provided global facet values when TanStack Table asks for the `__global__` faceting context. Column-specific faceting can still use separate values by checking for each column ID. diff --git a/docs/framework/preact/guide/global-filtering.md b/docs/framework/preact/guide/global-filtering.md new file mode 100644 index 0000000000..5e5034e477 --- /dev/null +++ b/docs/framework/preact/guide/global-filtering.md @@ -0,0 +1,239 @@ +--- +title: Global Filtering (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Column Filters](../examples/filters) +- [Fuzzy Search](../examples/filters-fuzzy) +### Preact Setup + +```tsx +import { useTable, tableFeatures, globalFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/preact-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` + +## Global Filtering (Preact) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +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 Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```tsx +import { + useTable, + tableFeatures, + globalFilteringFeature, +} from '@tanstack/preact-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = useTable({ + features, + rowModels: {}, // no filteredRowModel needed for manual server-side global filtering + data, + columns, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` to your features and the `filteredRowModel` to your row models: + +```tsx +import { + useTable, + tableFeatures, + globalFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/preact-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + // other options... +}) +``` + +### Global Filter Function + +The globalFilterFn option allows you to specify the filter function that will be used for global filtering. The filter function can be a string that references a built-in filter function, a string that references a custom filter function provided via the tableOptions.filterFns option, or a custom filter function. + +```tsx +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + data, + columns, + globalFilterFn: 'text', // built-in filter function +}) +``` + +By default there are 10 built-in filter functions to choose from: + +- includesString - Case-insensitive string inclusion +- includesStringSensitive - Case-sensitive string inclusion +- equalsString - Case-insensitive string equality +- equalsStringSensitive - Case-sensitive string equality +- arrIncludes - Item inclusion within an array +- arrIncludesAll - All items included in an array +- arrIncludesSome - Some items included in an array +- equals - Object/referential equality Object.is/=== +- weakEquals - Weak object/referential equality == +- inNumberRange - Number range inclusion + +You can also define your own custom filter functions either as the globalFilterFn table option. + +### Global Filter State + +The global filter state is stored in the table's state atoms and can be read with `table.atoms.globalFilter.get()` or from the current `table.store.state.globalFilter` snapshot. If you want to persist the global filter state outside of the table, use the `state.globalFilter` and `onGlobalFilterChange` options together. + +```tsx +const [globalFilter, setGlobalFilter] = useState([]) + +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + state: { + globalFilter, + }, + onGlobalFilterChange: setGlobalFilter, +}) +``` + +The global filtering state is defined as an object with the following shape: + +```tsx +interface GlobalFilter { + globalFilter: any +} +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. + +```tsx +return ( +
+ table.setGlobalFilter(String(e.target.value))} + placeholder="Search..." + /> +
+) +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the globalFilterFn option. + +> **Note:** It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering.md). + +```tsx +const customFilterFn = (rows, columnId, filterValue) => { + // custom filter logic +} + +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table initialState option. + +However, you can also just specify the initial global filter state in the state.globalFilter option. + +```tsx +const [globalFilter, setGlobalFilter] = useState("search term") //recommended to initialize globalFilter state here + +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + initialState: { + globalFilter: 'search term', // if not managing globalFilter state, set initial state here + }, + state: { + globalFilter, // pass our managed globalFilter state to the table + }, +}) +``` + +> NOTE: Do not use both initialState.globalFilter and state.globalFilter at the same time, as the initialized state in the state.globalFilter will override the initialState.globalFilter. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```tsx +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` diff --git a/docs/framework/preact/guide/grouping.md b/docs/framework/preact/guide/grouping.md new file mode 100644 index 0000000000..6c1fc16c15 --- /dev/null +++ b/docs/framework/preact/guide/grouping.md @@ -0,0 +1,224 @@ +--- +title: Grouping (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Grouping](../examples/grouping) + +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnGroupingFeature, createGroupedRowModel, aggregationFns } from '@tanstack/preact-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + columns, + data, +}) +``` + +## Grouping (Preact) Guide + +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 left, center (unpinned), and right pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +To use the grouping feature, add the `columnGroupingFeature` to your features and the `groupedRowModel` to your row models. The grouped row model is responsible for grouping the rows based on the grouping state. + +```tsx +import { + useTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, + aggregationFns, +} from '@tanstack/preact-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```tsx +const features = tableFeatures({ columnGroupingFeature, rowExpandingFeature }) + +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```tsx +table.setGrouping(['column1', 'column2']); +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```tsx +table.resetGrouping(); +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```tsx +const table = useTable({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows by columns using the aggregationFn option. This is a string that is the ID of the aggregation function. You can define the aggregation functions using the aggregationFns option. + +```tsx +const column = columnHelper.accessor('key', { + aggregationFn: 'sum', +}) +``` + +In the above example, the sum aggregation function will be used to aggregate the data in the grouped rows. +By default, numeric columns will use the sum aggregation function, and non-numeric columns will use the count aggregation function. You can override this behavior by specifying the aggregationFn option in the column definition. + +There are several built-in aggregation functions that you can use: + +- sum - Sums the values in the grouped rows. +- count - Counts the number of rows in the grouped rows. +- min - Finds the minimum value in the grouped rows. +- max - Finds the maximum value in the grouped rows. +- extent - Finds the extent (min and max) of the values in the grouped rows. +- mean - Finds the mean of the values in the grouped rows. +- median - Finds the median of the values in the grouped rows. +- unique - Returns an array of unique values in the grouped rows. +- uniqueCount - Counts the number of unique values in the grouped rows. + +#### Custom Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows using the aggregationFns option. This is a record where the keys are the IDs of the aggregation functions, and the values are the aggregation functions themselves. You can then reference these aggregation functions in a column's aggregationFn option. + +```tsx +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel({ + ...aggregationFns, + myCustomAggregation: (columnId, leafRows, childRows) => { + // return the aggregated value + }, + }), + }, + // other options... +}) +``` + +In the above example, myCustomAggregation is a custom aggregation function that takes the column ID, the leaf rows, and the child rows, and returns the aggregated value. You can then use this aggregation function in a column's aggregationFn option: + +```tsx +const column = columnHelper.accessor('key', { + aggregationFn: 'myCustomAggregation', +}) +``` + +### Manual Grouping + +If you are doing server-side grouping and aggregation, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to manually group the rows before passing them to the table. + +```tsx +const table = useTable({ + features: tableFeatures({ columnGroupingFeature }), + rowModels: {}, // no groupedRowModel needed for manual grouping + // other options... + manualGrouping: true, +}) +``` + +> **Note:** There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Grouping Change Handler + +If you want to manage the grouping state yourself, you can use the onGroupingChange option. This option is a function that is called when the grouping state changes. You can pass the managed state back to the table via the tableOptions.state.grouping option. + +```tsx +const [grouping, setGrouping] = useState([]) + +const table = useTable({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + state: { + grouping, + }, + onGroupingChange: setGrouping, +}) +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```tsx +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +column.getAutoAggregationFn() +column.getAggregationFn() +``` + +Rows expose grouping helpers for grouped row rendering: + +```tsx +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose helpers for choosing between grouped, aggregated, placeholder, and normal cell rendering: + +```tsx +cell.getIsGrouped() +cell.getIsAggregated() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```tsx +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/preact/guide/pagination.md b/docs/framework/preact/guide/pagination.md new file mode 100644 index 0000000000..d4914b796b --- /dev/null +++ b/docs/framework/preact/guide/pagination.md @@ -0,0 +1,258 @@ +--- +title: Pagination (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Pagination](../examples/pagination) + +### Preact Setup + +```tsx +import { useTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/preact-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +## Pagination (Preact) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain ***ALL*** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. + +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. + +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 pagination and then switch to server-side pagination in the future as your data grows. + +#### Should You Use Virtualization Instead? + +Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` to your features and the `paginatedRowModel` to your row models: + +```tsx +import { + useTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/preact-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. + +```tsx +import { + useTable, + tableFeatures, + rowPaginationFeature, +} from '@tanstack/preact-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = useTable({ + features, + rowModels: {}, // no paginatedRowModel needed for server-side pagination + columns, + data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +You can manage the `pagination` state just like any other state in the table instance. + +```tsx +import { + useTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/preact-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const [pagination, setPagination] = useState({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + onPaginationChange: setPagination, + state: { + pagination, + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```tsx +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT pass the `pagination` state to both the `state` and `initialState` options. `state` will overwrite `initialState`. Only use one or the other. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` when page-altering state changes occur, such as when the `data` is updated, filters change, grouping changes, etc. This behavior is automatically disabled when `manualPagination` is true but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. + +```tsx +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + autoResetPageIndex: false, // turn off auto reset of pageIndex +}) +``` + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> **Note**: These pagination APIs are available when using `rowPaginationFeature`. + +```tsx + + + + + +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/preact/guide/row-pinning.md b/docs/framework/preact/guide/row-pinning.md new file mode 100644 index 0000000000..0685a68024 --- /dev/null +++ b/docs/framework/preact/guide/row-pinning.md @@ -0,0 +1,219 @@ +--- +title: Row Pinning (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Row Pinning](../examples/row-pinning) +### Preact Setup + +```tsx +import { useTable, tableFeatures, rowPinningFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Row Pinning (Preact) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory, so `rowModels` can stay empty unless your table uses other row-model features. + +```tsx +import { + rowPinningFeature, + tableFeatures, + useTable, +} from '@tanstack/preact-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```tsx +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```tsx +const table = useTable({ + features, + rowModels: {}, + columns, + data, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, use `state.rowPinning` with `onRowPinningChange`. + +```tsx +const [rowPinning, setRowPinning] = useState({ + top: [], + bottom: [], +}) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, + state: { + rowPinning, + }, + onRowPinningChange: setRowPinning, +}) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```tsx +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +}) + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```tsx +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls: + +```tsx +const columns = [ + { + id: 'pin', + header: 'Pin', + cell: ({ row }) => + row.getCanPin() ? ( +
+ + + +
+ ) : null, + }, + //... +] +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```tsx +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly: + +```tsx +
+ {table.getTopRows().map(row => ( + + ))} + {table.getCenterRows().map(row => ( + + ))} + {table.getBottomRows().map(row => ( + + ))} + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```tsx +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```tsx +const table = useTable({ + features, + rowModels: {}, + columns, + data, + enableRowPinning: row => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```tsx +const table = useTable({ + features, + rowModels: {}, + columns, + data, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/preact/guide/row-selection.md b/docs/framework/preact/guide/row-selection.md new file mode 100644 index 0000000000..562b66420c --- /dev/null +++ b/docs/framework/preact/guide/row-selection.md @@ -0,0 +1,200 @@ +--- +title: Row Selection (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Row Selection](../examples/row-selection) +### Preact Setup + +```tsx +import { useTable, tableFeatures, rowSelectionFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Row Selection (Preact) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you (though as seen down below, it may be more convenient to manage the row selection state in your own scope). You can access the internal row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the current row selection state +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +> Note: If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +Even though the table instance will already manage the row selection state for you, it is usually more convenient to manage the state yourself in order to have easy access to the selected row ids that you can use to make API calls or other actions. + +Use the `onRowSelectionChange` table option to hoist up the row selection state to your own scope. Then pass the row selection state back to the table instance using in the `state` table option. + +```ts +import { useTable, tableFeatures, rowSelectionFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const [rowSelection, setRowSelection] = useState({}) + +const table = useTable({ + features, + rowModels: {}, + //... + onRowSelectionChange: setRowSelection, + state: { + rowSelection, + }, +}) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +const table = useTable({ + features, + rowModels: {}, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +const table = useTable({ + //... + enableRowSelection: row => row.original.age > 18, //only enable row selection for adults +}) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +const table = useTable({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +}) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +const table = useTable({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +}) +``` + +### Render Row Selection UI + +TanStack table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These function automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +```tsx +const columns = [ + { + id: 'select-col', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + //... more column definitions... +] +``` + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. + +```tsx + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => { + return + })} + + ) + })} + +``` diff --git a/docs/framework/preact/guide/sorting.md b/docs/framework/preact/guide/sorting.md new file mode 100644 index 0000000000..1d6efc47e0 --- /dev/null +++ b/docs/framework/preact/guide/sorting.md @@ -0,0 +1,462 @@ +--- +title: Sorting (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Sorting](../examples/sorting) + +### Preact Setup + +```tsx +import { useTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/preact-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = useTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +## Sorting (Preact) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```tsx +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +You can access the sorting state directly from the table instance with `table.atoms.sorting.get()` or from the current `table.store.state` snapshot. + +```tsx +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.sorting.get()) // access the current sorting state +``` + +However, if you need to access the sorting state before the table is initialized, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state, you can control/manage the sorting state in your own state management with the `state.sorting` and `onSortingChange` table options. + +```tsx +const [sorting, setSorting] = useState([]) // can set initial sorting state here +//... +// use sorting state to fetch data from your server or something... +//... +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + state: { + sorting, + }, + onSortingChange: setSorting, +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```tsx +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. + +```tsx +const [sorting, setSorting] = useState([]) +//... +const table = useTable({ + features: tableFeatures({ rowSortingFeature }), // feature needed for sorting state/APIs + rowModels: {}, // no sortedRowModel needed for manual sorting + columns, + data, + manualSorting: true, // use pre-sorted row model instead of sorted row model + state: { + sorting, + }, + onSortingChange: setSorting, +}) +``` + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` to your features and the `sortedRowModel` to your row models. Import `createSortedRowModel` and `sortFns` from TanStack Table: + +```tsx +import { + useTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFns, +} from '@tanstack/preact-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = useTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions either as the `sortFn` column option, or as a global sorting function using the `sortFns` table option. + +#### Custom Sorting Functions + +When defining a custom sorting function in either the `sortFns` table option or as a `sortFn` column option, it should have the following signature: + +```tsx +//optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = (rowA: Row, rowB: Row, columnId: string) => { + return //-1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> Note: The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```tsx +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // use custom global sorting function + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + } +] +//... +const table = useTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel({ + ...sortFns, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }), + }, + columns, + data, +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```tsx +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```tsx +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, //sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, //sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + sortDescFirst: true, //sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> **NOTE**: You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```tsx +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +In not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> NOTE: `'first'` and `'last'` options are available in v9. + +```tsx +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this: + +`'none' -> 'desc' -> 'asc' -> 'none' -> 'desc' -> 'asc' -> ...` + +If you disable sorting removal, the behavior will be like this: + +`'none' -> 'desc' -> 'asc' -> 'desc' -> 'asc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```tsx +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (always none -> asc -> desc -> asc) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```tsx +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```tsx +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```tsx +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```tsx +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/preact/guide/virtualization.md b/docs/framework/preact/guide/virtualization.md new file mode 100644 index 0000000000..18bbfaf0b5 --- /dev/null +++ b/docs/framework/preact/guide/virtualization.md @@ -0,0 +1,225 @@ +--- +title: Virtualization (Preact) Guide +--- + +## Examples + +This repository does not currently include official Preact virtualization examples. The table-side APIs in this guide use `@tanstack/preact-table`; pair them with the virtualization library that fits your Preact app. + +Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/react/examples/table). + +## Virtualization (Preact) Guide + +The TanStack Table packages do not come with any virtualization APIs or features built in. Virtualization is a rendering strategy, not a table feature. You can use TanStack Table with any virtualization library, but the official examples use TanStack Virtual. + +TanStack Table and TanStack Virtual solve different parts of the problem: + +- TanStack Table builds the row models, columns, headers, cells, sizing, sorting, filtering, and other table state. +- TanStack Virtual decides which item indexes should be rendered for the current scroll position. +- Your table renderer maps those virtual indexes back to rows, headers, and cells. + +### When To Use Virtualization + +Use virtualization when your table has a very large number of rows, columns, or both. Virtualization keeps the DOM small by only rendering the items that are visible in the scroll viewport plus a small overscan buffer. + +Virtualization is not a replacement for server-side pagination, filtering, or sorting. If the data is virtualized on the client, the data still needs to exist on the client. If your dataset is too large to load into the browser, use server-side data operations or infinite scrolling. + +For small tables, normal rendering is simpler and usually preferable. + +### Install A Virtualizer + +This repository does not currently include official Preact virtualization examples or a Preact-specific TanStack Virtual package in the table examples. Use a virtualizer that fits your Preact app, then map its virtual indexes back to `table.getRowModel().rows` or `table.getVisibleLeafColumns()`. + +### The Basic Pattern + +Most virtualized table implementations follow the same pattern: + +1. Create a fixed-height scroll container. +2. Pass the scroll element to the virtualizer. +3. Use `table.getRowModel().rows` or `table.getVisibleLeafColumns()` as the source list. +4. Configure `count`, `estimateSize`, `overscan`, and optional `measureElement`. +5. Render only virtual items. +6. Use virtual offsets or spacer padding to preserve the full scroll geometry. + +Here is a compact row virtualization example: + +```tsx +const rows = table.getRowModel().rows + +const rowVirtualizer = your virtualizer({ + count: rows.length, + getScrollElement: () => tableContainerRef.current, + estimateSize: () => 33, + overscan: 5, +}) + + + {rowVirtualizer.getVirtualItems().map(virtualRow => { + const row = rows[virtualRow.index] + + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + +``` + +### Virtualized Rows + +The virtualized rows examples show how to render large row counts while keeping the DOM small. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The core idea is that sorting, filtering, grouping, and other row-model work still comes from TanStack Table. The virtualizer reads from the final table row model: + +```tsx +const rows = table.getRowModel().rows +``` + +The row virtualizer is configured with `count: rows.length`, a row height estimate, the scroll container, and an overscan value. The `tbody` is given the full virtual height with `rowVirtualizer.getTotalSize()`, while each rendered row is absolutely positioned with `transform: translateY(...)`. + +The examples render cells from the current row with APIs like `row.getVisibleCells()` or `row.getAllCells()`, depending on whether the example needs visibility-aware cells or all cells. + +The official examples use large generated datasets, commonly tens or hundreds of thousands of rows. They also support dynamic row heights by using `measureElement` when possible. The examples skip dynamic row measurement in Firefox because Firefox can measure table border height differently. + +### Virtualized Columns + +The virtualized columns examples show how to render large row and column counts. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +Column virtualization uses the current visible column list: + +```tsx +const visibleColumns = table.getVisibleLeafColumns() +``` + +The column virtualizer is configured for horizontal virtualization: + +```tsx +const columnVirtualizer = your virtualizer({ + count: visibleColumns.length, + estimateSize: index => visibleColumns[index].getSize(), + getScrollElement: () => tableContainerRef.current, + horizontal: true, + overscan: 3, +}) +``` + +Column virtualization uses a different rendering strategy than row virtualization. Instead of absolutely positioning columns, the examples add fake spacer cells to the left and right: + +```tsx +const virtualColumns = columnVirtualizer.getVirtualItems() +const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 +const virtualPaddingRight = + columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) +``` + +Those spacer cells preserve the horizontal scroll width while the renderer only mounts the virtual columns. This approach keeps row rendering table-like and allows dynamic row height measurement to keep working. + +### Virtualized Rows And Columns Together + +The official virtualized columns examples also virtualize rows. In those examples: + +- The row virtualizer controls vertical positioning and total body height. +- The column virtualizer controls horizontal header/cell rendering and left/right spacer cells. +- `virtualRow.index` maps to `rows[virtualRow.index]`. +- `virtualColumn.index` maps to `visibleCells[virtualColumn.index]`. + +Always use virtual indexes against the same current row and column lists returned by the table. If sorting, filtering, pagination, grouping, or column visibility changes, recompute the virtualized rows and columns from the current table state. + +### Virtualized Infinite Scrolling + +The virtualized infinite scrolling examples combine row virtualization with progressive data fetching. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The common pattern is: + +1. Fetch a page of rows. +2. Flatten fetched pages into the table `data`. +3. Use row virtualization over the loaded rows. +4. Listen to scroll events on the table container. +5. Fetch the next page when the user scrolls near the bottom. + +The Preact infinite scrolling pattern can use TanStack Query or any other data-fetching layer. + +```tsx +const { scrollHeight, scrollTop, clientHeight } = scrollElement + +if (scrollHeight - scrollTop - clientHeight < 500) { + fetchNextPage() +} +``` + +If sorting is handled by the server, use manual sorting so the fetched data reflects the whole backend dataset rather than only the currently loaded rows. When sorting changes and the fetched dataset is replaced, scroll back to the top with `rowVirtualizer.scrollToIndex(0)`. + +### Dynamic Row Heights + +Dynamic row heights are useful when content can wrap or expand. They are also more complex than fixed-height rows. + +Use `estimateSize` as the virtualizer's initial guess: + +```tsx +estimateSize: () => 33 +``` + +Then use `measureElement` to refine the actual row height after rendering: + +```tsx + rowVirtualizer.measureElement(node)} +> +``` + +Set `data-index` on each row so the virtualizer can associate measurements with the correct item. In non-React adapters, call `measureElement` through the adapter-appropriate ref, action, directive, or controller. + +Overscan helps avoid blank regions while measurements settle. If every row has a known fixed height, skip dynamic measurement and use the fixed height estimate instead. + +### Sticky Headers And Semantic Table Markup + +The examples still use semantic table tags, but they change table layout CSS to support virtual positioning and sticky headers. + +Dynamic row virtualization commonly requires: + +```css +table { + display: grid; +} + +thead { + display: grid; + position: sticky; + top: 0; +} + +tr { + display: flex; +} +``` + +Rows are absolutely positioned inside a relatively positioned `tbody`, and cells use flex sizing so they can match `column.getSize()` or `cell.column.getSize()`. This is intentional. Native table layout does not work well with dynamic-height virtual rows that are positioned independently. + +### Performance Tips + +- Keep virtualizers near the components that render the virtualized items. +- Avoid re-rendering the full table body on every scroll. +- Keep row, column, and data references stable where possible. +- Use `overscan` deliberately. More overscan reduces visible blanking, while less overscan reduces DOM nodes. +- Avoid expensive cell renderers in very large virtualized tables. +- Test production builds. Development builds can be slower than production builds; profile production bundles before optimizing. +- Prefer fixed row sizes when the UI allows it. +- For column virtualization, use `column.getSize()`, `header.getSize()`, and `cell.column.getSize()` consistently. + diff --git a/docs/guide/column-faceting.md b/docs/framework/react/guide/column-faceting.md similarity index 81% rename from docs/guide/column-faceting.md rename to docs/framework/react/guide/column-faceting.md index 03f2f6949d..2a0fe43c1e 100644 --- a/docs/guide/column-faceting.md +++ b/docs/framework/react/guide/column-faceting.md @@ -1,44 +1,32 @@ --- -title: Column Faceting Guide +title: Column Faceting (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Faceted Filters](../examples/filters-faceted) +### React Setup -# React +```tsx +import { useTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/react-table' -- [Faceted Filters](../framework/react/examples/filters-faceted) - -# Preact - -- [Faceted Filters](../framework/preact/examples/filters-faceted) - -# Solid - -- [Faceted Filters](../framework/solid/examples/filters-faceted) - -# Svelte - -- [Faceted Filters](../framework/svelte/examples/filters-faceted) - -# Vue - -- [Faceted Filters](../framework/vue/examples/filters-faceted) - -# Angular - -- [Faceted Filters](../framework/angular/examples/filters-faceted) - -# Lit - -- [Faceted Filters](../framework/lit/examples/filters-faceted) +const features = tableFeatures({ columnFacetingFeature }) - +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, +}) +``` -## Column Faceting Guide +## Column Faceting (React) Guide Column Faceting is a feature that allows you to generate lists of values for a given column from that column's data. For example, a list of unique values in a column can be generated from all rows in that column to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a column of numbers to be used as a range for a range slider filter component. diff --git a/docs/guide/column-filtering.md b/docs/framework/react/guide/column-filtering.md similarity index 90% rename from docs/guide/column-filtering.md rename to docs/framework/react/guide/column-filtering.md index aa3054d956..468df600a5 100644 --- a/docs/guide/column-filtering.md +++ b/docs/framework/react/guide/column-filtering.md @@ -1,58 +1,32 @@ --- -title: Column Filtering Guide +title: Column Filtering (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Fuzzy Search](../examples/filters-fuzzy) +### React Setup -# React - -- [Column Filters](../framework/react/examples/filters) -- [Faceted Filters](../framework/react/examples/filters-faceted) -- [Fuzzy Search](../framework/react/examples/filters-fuzzy) - -# Preact - -- [Column Filters](../framework/preact/examples/filters) -- [Faceted Filters](../framework/preact/examples/filters-faceted) -- [Fuzzy Search](../framework/preact/examples/filters-fuzzy) - -# Solid - -- [Column Filters](../framework/solid/examples/filters) -- [Faceted Filters](../framework/solid/examples/filters-faceted) -- [Fuzzy Search](../framework/solid/examples/filters-fuzzy) - -# Svelte - -- [Column Filters](../framework/svelte/examples/filtering) -- [Faceted Filters](../framework/svelte/examples/filters-faceted) -- [Fuzzy Search](../framework/svelte/examples/filters-fuzzy) - -# Vue - -- [Column Filters](../framework/vue/examples/filters) -- [Faceted Filters](../framework/vue/examples/filters-faceted) -- [Fuzzy Search](../framework/vue/examples/filters-fuzzy) - -# Angular - -- [Column Filters](../framework/angular/examples/filters) -- [Faceted Filters](../framework/angular/examples/filters-faceted) -- [Fuzzy Search](../framework/angular/examples/filters-fuzzy) - -# Lit +```tsx +import { useTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/react-table' -- [Column Filters](../framework/lit/examples/filters) -- [Faceted Filters](../framework/lit/examples/filters-faceted) -- [Fuzzy Search](../framework/lit/examples/filters-fuzzy) +const features = tableFeatures({ columnFilteringFeature }) - +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` -## Column Filtering Guide +## Column Filtering (React) Guide Filtering comes in 2 flavors: Column Filtering and Global Filtering. @@ -82,7 +56,7 @@ If you have decided that you need to implement server-side filtering instead of 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 `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. -```jsx +```tsx const table = useTable({ features: tableFeatures({ columnFilteringFeature }), rowModels: {}, // no filteredRowModel needed for manual server-side filtering @@ -98,7 +72,7 @@ const table = useTable({ If you are using the built-in client-side filtering features, add the `columnFilteringFeature` to your features and the `filteredRowModel` to your row models. Import `createFilteredRowModel` and `filterFns` from TanStack Table: -```jsx +```tsx import { useTable, tableFeatures, @@ -139,7 +113,7 @@ Since the column filter state is an array of objects, you can have multiple colu You can access the column filter state from the table instance with `table.atoms.columnFilters.get()` or from the current `table.store.state` snapshot. -```jsx +```tsx const table = useTable({ features, rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, @@ -177,7 +151,7 @@ const table = useTable({ 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`. -```jsx +```tsx const table = useTable({ features, rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, @@ -234,7 +208,7 @@ Every filter function receives: and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. -```jsx +```tsx const columns = [ { header: () => 'Name', @@ -315,7 +289,7 @@ By default, column filtering is enabled for all columns. You can disable the col Disabling column filtering for a column will cause the `column.getCanFilter` API to return `false` for that column. -```jsx +```tsx const columns = [ { header: () => 'Id', @@ -344,7 +318,7 @@ By default, filtering is done from parent rows down, so if a parent row is filte 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. -```jsx +```tsx const table = useTable({ features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), rowModels: { @@ -363,7 +337,7 @@ By default, filtering is done for all rows in a tree, no matter if they are root 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. -```jsx +```tsx const table = useTable({ features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), rowModels: { diff --git a/docs/guide/column-ordering.md b/docs/framework/react/guide/column-ordering.md similarity index 78% rename from docs/guide/column-ordering.md rename to docs/framework/react/guide/column-ordering.md index 2df3bfcb2a..41fbd6e382 100644 --- a/docs/guide/column-ordering.md +++ b/docs/framework/react/guide/column-ordering.md @@ -1,45 +1,29 @@ --- -title: Column Ordering Guide +title: Column Ordering (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Column Ordering](../examples/column-ordering) +- [Column DnD](../examples/column-dnd) +### React Setup -# React - -- [Column Ordering](../framework/react/examples/column-ordering) -- [Column DnD](../framework/react/examples/column-dnd) - -# Preact - -- [Column Ordering](../framework/preact/examples/column-ordering) - -# Solid - -- [Column Ordering](../framework/solid/examples/column-ordering) - -# Svelte - -- [Column Ordering](../framework/svelte/examples/column-ordering) - -# Vue - -- [Column Ordering](../framework/vue/examples/column-ordering) - -# Angular - -- [Column Ordering](../framework/angular/examples/column-ordering) - -# Lit +```tsx +import { useTable, tableFeatures, columnOrderingFeature } from '@tanstack/react-table' -- [Column Ordering](../framework/lit/examples/column-ordering) +const features = tableFeatures({ columnOrderingFeature }) - +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` -## Column Ordering Guide +## Column Ordering (React) 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. @@ -61,7 +45,7 @@ If you don't provide a `columnOrder` state, TanStack Table will just use the ord If all you need to do is specify the initial column order, you can just specify the `columnOrder` state in the `initialState` table option. -```jsx +```tsx const table = useTable({ features: tableFeatures({ columnOrderingFeature }), rowModels: {}, @@ -79,7 +63,7 @@ const table = useTable({ 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. -```jsx +```tsx const [columnOrder, setColumnOrder] = useState(['columnId1', 'columnId2', 'columnId3']) //... const table = useTable({ @@ -128,13 +112,37 @@ const handleDragEnd = (e: DragEvent) => { //use your dnd solution of choice ``` +### 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. + +```tsx +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. + +```tsx +column.getIndex() +column.getIndex('left') +column.getIndex('center') +column.getIndex('right') + +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 (React) There are undoubtedly many ways to implement drag and drop features along-side TanStack Table. Here are a few suggestions in order for you to not have a bad time: 1. Do NOT try to use [`"react-dnd"`](https://react-dnd.github.io/react-dnd/docs/overview) _if you are using React 18 or newer_. React DnD was an important library for its time, but it now does not get updated very often, and it has incompatibilities with React 18, especially in React Strict Mode. It is still possible to get it to work, but there are newer alternatives that have better compatibility and are more actively maintained. React DnD's Provider may also interfere and conflict with any other DnD solutions you may want to try in your app. -2. Use [`"@dnd-kit/core"`](https://dndkit.com/). DnD Kit is a modern, modular and lightweight drag and drop library that is highly compatible with the modern React ecosystem, and it works well with semantic `
+``` + +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. + +```tsx + +``` + +#### 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. + +```tsx + +``` + +The `columnResizing` state stores transient drag information: + +```tsx +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +Use `onColumnResizingChange` with `state.columnResizing` if you need to manage this state externally. + +```tsx +const [columnResizing, setColumnResizing] = useState({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, + state: { + columnResizing, + }, + onColumnResizingChange: setColumnResizing, +}) +``` + +### 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. + +```tsx +header.getResizeHandler() +column.getCanResize() +column.getIsResizing() +``` + +The table instance exposes APIs for the transient resize state. The current generated v9 API spelling is `table.setcolumnResizing` with a lowercase `c` in `column`; use that exact name. + +```tsx +table.setcolumnResizing(old => ({ + ...old, + deltaOffset: 12, +})) + +table.resetHeaderSizeInfo() +table.resetHeaderSizeInfo(true) +``` + +### Advanced Column Resizing Performance + +If you are creating large or complex tables with Preact, you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns. + +We have created a [performant column resizing example](../examples/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: + +1. Don't use `column.getSize()` on every header and every data cell. Instead, calculate all column widths once upfront, **memoized**! +2. Memoize your Table Body while resizing is in progress. +3. Use CSS variables to communicate column widths to your table cells. + +If you follow these steps, you should see significant performance improvements while resizing columns. + +If you are not using React, and are using the Svelte, Vue, or Solid adapters instead, you may not need to worry about this as much, but similar principles apply. diff --git a/docs/framework/preact/guide/column-sizing.md b/docs/framework/preact/guide/column-sizing.md new file mode 100644 index 0000000000..8041e7a872 --- /dev/null +++ b/docs/framework/preact/guide/column-sizing.md @@ -0,0 +1,120 @@ +--- +title: Column Sizing (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Column Sizing](../examples/column-sizing) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnSizingFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ columnSizingFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Sizing (Preact) 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: + +```tsx +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. + +```tsx +const columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +const table = useTable({ + features: tableFeatures({ columnSizingFeature }), + rowModels: {}, + 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. + +```tsx +column.getSize() +header.getSize() + +column.getStart() // left offset in the current column flow +column.getStart('left') +column.getStart('center') +column.getStart('right') + +column.getAfter() // right offset in the current column flow +column.getAfter('left') +column.getAfter('center') +column.getAfter('right') + +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. + +```tsx +table.getTotalSize() +table.getLeftTotalSize() +table.getCenterTotalSize() +table.getRightTotalSize() +``` + +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. + +```tsx +table.setColumnSizing({ + firstName: 180, + age: 80, +}) + +table.resetColumnSizing() +table.resetColumnSizing(true) +``` diff --git a/docs/framework/preact/guide/column-visibility.md b/docs/framework/preact/guide/column-visibility.md new file mode 100644 index 0000000000..80c0c4aa7b --- /dev/null +++ b/docs/framework/preact/guide/column-visibility.md @@ -0,0 +1,140 @@ +--- +title: Column Visibility (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Column Visibility](../examples/column-visibility) +### Preact Setup + +```tsx +import { useTable, tableFeatures, columnVisibilityFeature } from '@tanstack/preact-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Visibility (Preact) 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. + +```tsx +import { useTable, tableFeatures, columnVisibilityFeature } from '@tanstack/preact-table' + +const [columnVisibility, setColumnVisibility] = useState({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +const table = useTable({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + state: { + columnVisibility, + //... + }, + onColumnVisibilityChange: setColumnVisibility, +}) +``` + +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. + +```tsx +const table = useTable({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + 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. + +```tsx +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessor: '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. + +```tsx +{table.getAllColumns().map((column) => ( + +))} +``` + +### 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`. + +```tsx + + + + {table.getVisibleLeafColumns().map((column) => ( // takes column visibility into account + // + ))} + + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( // takes column visibility into account + // + ))} + + ))} + +
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/preact/guide/custom-features.md b/docs/framework/preact/guide/custom-features.md new file mode 100644 index 0000000000..e2e824adb1 --- /dev/null +++ b/docs/framework/preact/guide/custom-features.md @@ -0,0 +1,318 @@ +--- +title: Custom Features (Preact) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Preact examples: + +- [Custom Plugin](../examples/custom-plugin) +## Custom Features (Preact) Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers where TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable`, `useTable`, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrapper hooks around the `useTable` hook to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking—you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods do: + +#### Default Options and Initial State + +
+ +##### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +##### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is responsible for adding methods to the `table` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. + +
+ +##### assignColumnPrototype + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, the [Column Grouping](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts) feature adds cell instance API methods such as `getIsGrouped` and `getIsAggregated`. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full [custom-plugin](../examples/custom-plugin) example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/preact-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs options, and state in your application. + +```tsx +const features = tableFeatures({ densityPlugin }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, + //... + state: { + density, + }, + onDensityChange: setDensity, +}) +//... +const density = table.atoms.density.get() +return( +
+ +
+ +
// The number of columns you wish to span for the expanded data if it is not a row that shares the same columns as the parent row + // Your custom UI goes here +
{/* */}
{/* render cell */}
` markup. Both of the official TanStack DnD examples, [Column DnD](../framework/react/examples/column-dnd) and [Row DnD](../framework/react/examples/row-dnd), now use DnD Kit. +2. Use [`"@dnd-kit/core"`](https://dndkit.com/). DnD Kit is a modern, modular and lightweight drag and drop library that is highly compatible with the modern React ecosystem, and it works well with semantic `
` markup. The official React DnD examples, [Column DnD](../examples/column-dnd) and [Row DnD](../examples/row-dnd), use DnD Kit. 3. Consider other DnD libraries like [`"react-beautiful-dnd"`](https://github.com/atlassian/react-beautiful-dnd), but be aware of their potentially large bundle sizes, maintenance status, and compatibility with `
` markup. diff --git a/docs/guide/column-pinning.md b/docs/framework/react/guide/column-pinning.md similarity index 63% rename from docs/guide/column-pinning.md rename to docs/framework/react/guide/column-pinning.md index f53e6a62a3..f3fe9c07ad 100644 --- a/docs/guide/column-pinning.md +++ b/docs/framework/react/guide/column-pinning.md @@ -1,58 +1,30 @@ --- -title: Column Pinning Guide +title: Column Pinning (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) +### React Setup -# React - -- [Column Pinning](../framework/react/examples/column-pinning) -- [Column Pinning Split](../framework/react/examples/column-pinning-split) -- [Sticky Column Pinning](../framework/react/examples/column-pinning-sticky) - -# Preact - -- [Column Pinning](../framework/preact/examples/column-pinning) -- [Column Pinning Split](../framework/preact/examples/column-pinning-split) -- [Sticky Column Pinning](../framework/preact/examples/column-pinning-sticky) - -# Solid - -- [Column Pinning](../framework/solid/examples/column-pinning) -- [Column Pinning Split](../framework/solid/examples/column-pinning-split) -- [Sticky Column Pinning](../framework/solid/examples/column-pinning-sticky) - -# Svelte - -- [Column Pinning](../framework/svelte/examples/column-pinning) -- [Column Pinning Split](../framework/svelte/examples/column-pinning-split) -- [Sticky Column Pinning](../framework/svelte/examples/column-pinning-sticky) - -# Vue - -- [Column Pinning](../framework/vue/examples/column-pinning) -- [Column Pinning Split](../framework/vue/examples/column-pinning-split) -- [Sticky Column Pinning](../framework/vue/examples/column-pinning-sticky) - -# Angular - -- [Column Pinning](../framework/angular/examples/column-pinning) -- [Column Pinning Split](../framework/angular/examples/column-pinning-split) -- [Sticky Column Pinning](../framework/angular/examples/column-pinning-sticky) - -# Lit +```tsx +import { useTable, tableFeatures, columnPinningFeature } from '@tanstack/react-table' -- [Column Pinning](../framework/lit/examples/column-pinning) -- [Column Pinning Split](../framework/lit/examples/column-pinning-split) -- [Sticky Column Pinning](../framework/lit/examples/column-pinning-sticky) +const features = tableFeatures({ columnPinningFeature }) - +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` -## Column Pinning Guide +## Column Pinning (React) 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 left or right. @@ -70,7 +42,7 @@ The only way to change the order of the pinned columns is in the `columnPinning. 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. -```jsx +```tsx import { useTable, tableFeatures, columnPinningFeature } from '@tanstack/react-table' const features = tableFeatures({ columnPinningFeature }) @@ -97,7 +69,7 @@ const table = useTable({ 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 -```jsx +```tsx const table = useTable({ features, rowModels: {}, @@ -122,11 +94,66 @@ There are a handful of useful Column API methods to help you implement column pi - `column.getCanPin`: Use to determine if a column can be pinned. - `column.pin`: Use to pin a column to the left or right. 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 `left` CSS value for a pinned column. - `column.getAfter`: Use to provide the correct `right` 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. + +```tsx +table.setColumnPinning({ + left: ['firstName'], + right: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```tsx +table.getLeftLeafColumns() +table.getCenterLeafColumns() +table.getRightLeafColumns() + +table.getLeftVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getRightVisibleLeafColumns() + +table.getLeftHeaderGroups() +table.getCenterHeaderGroups() +table.getRightHeaderGroups() + +table.getLeftFooterGroups() +table.getCenterFooterGroups() +table.getRightFooterGroups() + +table.getLeftFlatHeaders() +table.getCenterFlatHeaders() +table.getRightFlatHeaders() + +table.getLeftLeafHeaders() +table.getCenterLeafHeaders() +table.getRightLeafHeaders() +``` + +You can also request pinned leaf columns by region with `table.getPinnedLeafColumns(position)` and visible pinned leaf columns with `table.getPinnedVisibleLeafColumns(position)`. + +```tsx +table.getPinnedLeafColumns('left') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('right') + +table.getPinnedVisibleLeafColumns('left') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('right') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'left'` or `'right'` to check one pinned side. + ### Split Table Column Pinning If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. diff --git a/docs/guide/column-resizing.md b/docs/framework/react/guide/column-resizing.md similarity index 63% rename from docs/guide/column-resizing.md rename to docs/framework/react/guide/column-resizing.md index 496c721bee..aaa506475d 100644 --- a/docs/guide/column-resizing.md +++ b/docs/framework/react/guide/column-resizing.md @@ -1,51 +1,29 @@ --- -title: Column Resizing Guide +title: Column Resizing (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) +### React Setup -# React - -- [Column Resizing](../framework/react/examples/column-resizing) -- [Performant Column Resizing](../framework/react/examples/column-resizing-performant) - -# Preact - -- [Column Resizing](../framework/preact/examples/column-resizing) -- [Performant Column Resizing](../framework/preact/examples/column-resizing-performant) - -# Solid - -- [Column Resizing](../framework/solid/examples/column-resizing) -- [Performant Column Resizing](../framework/solid/examples/column-resizing-performant) - -# Svelte - -- [Column Resizing](../framework/svelte/examples/column-resizing) -- [Performant Column Resizing](../framework/svelte/examples/column-resizing-performant) - -# Vue - -- [Column Resizing](../framework/vue/examples/column-resizing) -- [Performant Column Resizing](../framework/vue/examples/column-resizing-performant) - -# Angular - -- [Column Resizing](../framework/angular/examples/column-resizing) -- [Performant Column Resizing](../framework/angular/examples/column-resizing-performant) - -# Lit +```tsx +import { useTable, tableFeatures, columnResizingFeature } from '@tanstack/react-table' -- [Column Resizing](../framework/lit/examples/column-resizing) -- [Performant Column Resizing](../framework/lit/examples/column-resizing-performant) +const features = tableFeatures({ columnResizingFeature }) - +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` -## Column Resizing Guide +## Column Resizing (React) 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. @@ -150,25 +128,84 @@ TanStack Table provides a pre-built event handler to make your drag interactions /> ``` -#### Column Resize Indicator with ColumnSizingInfoState +#### Column Resize Indicator with Column Resizing State -TanStack Table keeps track of a state object called `columnSizingInfo` that you can use to render a column resize indicator UI. +TanStack Table keeps track of a `columnResizing` state object that you can use to render a column resize indicator UI. -```jsx +```tsx ``` +The `columnResizing` state stores transient drag information: + +```tsx +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +Use `onColumnResizingChange` with `state.columnResizing` if you need to manage this state externally. + +```tsx +const [columnResizing, setColumnResizing] = useState({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, + state: { + columnResizing, + }, + onColumnResizingChange: setColumnResizing, +}) +``` + +### 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. + +```tsx +header.getResizeHandler() +column.getCanResize() +column.getIsResizing() +``` + +The table instance exposes APIs for the transient resize state. The current generated v9 API spelling is `table.setcolumnResizing` with a lowercase `c` in `column`; use that exact name. + +```tsx +table.setcolumnResizing(old => ({ + ...old, + deltaOffset: 12, +})) + +table.resetHeaderSizeInfo() +table.resetHeaderSizeInfo(true) +``` + ### Advanced Column Resizing Performance -If you are creating large or complex tables (and using React 😉), you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns. +If you are creating large or complex tables with React, you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns. -We have created a [performant column resizing example](../framework/react/examples/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: +We have created a [performant column resizing example](../examples/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: 1. Don't use `column.getSize()` on every header and every data cell. Instead, calculate all column widths once upfront, **memoized**! 2. Memoize your Table Body while resizing is in progress. diff --git a/docs/guide/column-sizing.md b/docs/framework/react/guide/column-sizing.md similarity index 57% rename from docs/guide/column-sizing.md rename to docs/framework/react/guide/column-sizing.md index 2fbb01d607..b03476457b 100644 --- a/docs/guide/column-sizing.md +++ b/docs/framework/react/guide/column-sizing.md @@ -1,44 +1,28 @@ --- -title: Column Sizing Guide +title: Column Sizing (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Column Sizing](../examples/column-sizing) +### React Setup -# React - -- [Column Sizing](../framework/react/examples/column-sizing) - -# Preact - -- [Column Sizing](../framework/preact/examples/column-sizing) - -# Solid - -- [Column Sizing](../framework/solid/examples/column-sizing) - -# Svelte - -- [Column Sizing](../framework/svelte/examples/column-sizing) - -# Vue - -- [Column Sizing](../framework/vue/examples/column-sizing) - -# Angular - -- [Column Sizing](../framework/angular/examples/column-sizing) - -# Lit +```tsx +import { useTable, tableFeatures, columnSizingFeature } from '@tanstack/react-table' -- [Column Sizing](../framework/lit/examples/column-sizing) +const features = tableFeatures({ columnSizingFeature }) - +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` -## Column Sizing Guide +## Column Sizing (React) Guide The column sizing feature allows you to optionally specify the width of each column including min and max widths. @@ -92,3 +76,45 @@ As a headless utility, table logic for column sizing is really only a collection - 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. + +```tsx +column.getSize() +header.getSize() + +column.getStart() // left offset in the current column flow +column.getStart('left') +column.getStart('center') +column.getStart('right') + +column.getAfter() // right offset in the current column flow +column.getAfter('left') +column.getAfter('center') +column.getAfter('right') + +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. + +```tsx +table.getTotalSize() +table.getLeftTotalSize() +table.getCenterTotalSize() +table.getRightTotalSize() +``` + +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. + +```tsx +table.setColumnSizing({ + firstName: 180, + age: 80, +}) + +table.resetColumnSizing() +table.resetColumnSizing(true) +``` diff --git a/docs/guide/column-visibility.md b/docs/framework/react/guide/column-visibility.md similarity index 83% rename from docs/guide/column-visibility.md rename to docs/framework/react/guide/column-visibility.md index 51ace38b4c..fdc9175eba 100644 --- a/docs/guide/column-visibility.md +++ b/docs/framework/react/guide/column-visibility.md @@ -1,44 +1,28 @@ --- -title: Column Visibility Guide +title: Column Visibility (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Column Visibility](../examples/column-visibility) +### React Setup -# React - -- [Column Visibility](../framework/react/examples/column-visibility) - -# Preact - -- [Column Visibility](../framework/preact/examples/column-visibility) - -# Solid - -- [Column Visibility](../framework/solid/examples/column-visibility) - -# Svelte - -- [Column Visibility](../framework/svelte/examples/column-visibility) - -# Vue - -- [Column Visibility](../framework/vue/examples/column-visibility) - -# Angular - -- [Column Visibility](../framework/angular/examples/column-visibility) - -# Lit +```tsx +import { useTable, tableFeatures, columnVisibilityFeature } from '@tanstack/react-table' -- [Column Visibility](../framework/lit/examples/column-visibility) +const features = tableFeatures({ columnVisibilityFeature }) - +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` -## Column Visibility Guide +## Column Visibility (React) 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. @@ -46,7 +30,7 @@ The column visibility feature allows table columns to be hidden or shown dynamic 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. -```jsx +```tsx import { useTable, tableFeatures, columnVisibilityFeature } from '@tanstack/react-table' const [columnVisibility, setColumnVisibility] = useState({ @@ -71,7 +55,7 @@ Alternatively, if you don't need to manage the column visibility state outside o > **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. -```jsx +```tsx const table = useTable({ features: tableFeatures({ columnVisibilityFeature }), rowModels: {}, @@ -91,7 +75,7 @@ const table = useTable({ 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. -```jsx +```tsx const columns = [ { header: 'ID', @@ -114,7 +98,7 @@ There are several column API methods that are useful for rendering column visibi - `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. -```jsx +```tsx {table.getAllColumns().map((column) => ( diff --git a/docs/guide/custom-features.md b/docs/framework/react/guide/custom-features.md similarity index 95% rename from docs/guide/custom-features.md rename to docs/framework/react/guide/custom-features.md index 4f6ba90442..09f7d7df0c 100644 --- a/docs/guide/custom-features.md +++ b/docs/framework/react/guide/custom-features.md @@ -1,28 +1,13 @@ --- -title: Custom Features Guide +title: Custom Features (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - - -# React - -- [Custom Plugin](../framework/react/examples/custom-plugin) - -# Preact - -- [Custom Plugin](../framework/preact/examples/custom-plugin) - -# Angular - -- [Custom Plugin](../framework/angular/examples/custom-plugin) - - - -## Custom Features Guide +- [Custom Plugin](../examples/custom-plugin) +## Custom Features (React) Guide In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. @@ -151,7 +136,7 @@ The `assignCellPrototype` method in a table feature is responsible for adding me Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. -Check out the full [custom-plugin](../framework/react/examples/custom-plugin) example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. +Check out the full [custom-plugin](../examples/custom-plugin) example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. #### Step 1: Set up TypeScript Types @@ -323,7 +308,7 @@ return( transition: 'padding 0.2s', }} > - {flexRender(cell.column.columnDef.cell, cell.getContext())} + ) ``` diff --git a/docs/guide/expanding.md b/docs/framework/react/guide/expanding.md similarity index 85% rename from docs/guide/expanding.md rename to docs/framework/react/guide/expanding.md index a56a093fc6..2124115380 100644 --- a/docs/guide/expanding.md +++ b/docs/framework/react/guide/expanding.md @@ -1,51 +1,31 @@ --- -title: Expanding Guide +title: Expanding (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Expanding](../examples/expanding) -# React +### React Setup -- [Expanding](../framework/react/examples/expanding) -- [Sub Components](../framework/react/examples/sub-components) +```tsx +import { useTable, tableFeatures, rowExpandingFeature, createExpandedRowModel } from '@tanstack/react-table' -# Preact - -- [Expanding](../framework/preact/examples/expanding) -- [Sub Components](../framework/preact/examples/sub-components) - -# Solid - -- [Expanding](../framework/solid/examples/expanding) -- [Sub Components](../framework/solid/examples/sub-components) - -# Svelte - -- [Expanding](../framework/svelte/examples/expanding) -- [Sub Components](../framework/svelte/examples/sub-components) - -# Vue - -- [Expanding](../framework/vue/examples/expanding) -- [Sub Components](../framework/vue/examples/sub-components) - -# Angular - -- [Expanding](../framework/angular/examples/expanding) -- [Sub Components](../framework/angular/examples/sub-components) - -# Lit - -- [Expanding](../framework/lit/examples/expanding) -- [Sub Components](../framework/lit/examples/sub-components) +const features = tableFeatures({ rowExpandingFeature }) - +const table = useTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, +}) +``` -## Expanding Feature Guide +## Expanding Feature (React) Guide Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. @@ -135,7 +115,9 @@ In some cases, you may wish to show extra details or information, which may or m By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. -```ts +```tsx +import { Fragment } from 'react' + //... const table = useTable({ features, @@ -148,12 +130,12 @@ const table = useTable({ //... {table.getRowModel().rows.map((row) => ( - + {/* Normal row UI */} {row.getVisibleCells().map((cell) => ( ))} @@ -165,7 +147,7 @@ const table = useTable({ )} - + ))} //... @@ -201,7 +183,7 @@ If the ExpandedState is true, it means all rows are expanded. If it's a record, TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. For example, you can add a button UI within the columns definition. -```ts +```tsx const columns = [ { accessorKey: 'name', @@ -227,6 +209,32 @@ const columns = [ ] ``` +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```tsx +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```tsx +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + ### Filtering Expanded Rows By default, the filtering process starts from the parent rows and moves downwards. This means if a parent row is excluded by the filter, all its child rows will also be excluded. However, you can change this behavior by using the `filterFromLeafRows` option. When this option is enabled, the filtering process starts from the leaf (child) rows and moves upwards. This ensures that a parent row will be included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. Additionally, you can control how deep into the child hierarchy the filter process goes by using the `maxLeafRowFilterDepth` option. This option allows you to specify the maximum depth of child rows that the filter should consider. @@ -261,7 +269,7 @@ const table = useTable({ ### Pinning Expanded Rows -Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Pinning Guide](./pinning.md) for more information on row pinning. +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Pinning Guide](../../../guide/pinning.md) for more information on row pinning. ### Sorting Expanded Rows diff --git a/docs/guide/fuzzy-filtering.md b/docs/framework/react/guide/fuzzy-filtering.md similarity index 86% rename from docs/guide/fuzzy-filtering.md rename to docs/framework/react/guide/fuzzy-filtering.md index 9e34012f8f..afa5788cb5 100644 --- a/docs/guide/fuzzy-filtering.md +++ b/docs/framework/react/guide/fuzzy-filtering.md @@ -1,44 +1,30 @@ --- -title: Fuzzy Filtering Guide +title: Fuzzy Filtering (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Fuzzy Search](../examples/filters-fuzzy) +### React Setup -# React +```tsx +import { useTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/react-table' -- [Fuzzy Search](../framework/react/examples/filters-fuzzy) +const features = tableFeatures({ columnFilteringFeature }) -# Preact - -- [Fuzzy Search](../framework/preact/examples/filters-fuzzy) - -# Solid - -- [Fuzzy Search](../framework/solid/examples/filters-fuzzy) - -# Svelte - -- [Fuzzy Search](../framework/svelte/examples/filters-fuzzy) - -# Vue - -- [Fuzzy Search](../framework/vue/examples/filters-fuzzy) - -# Angular - -- [Fuzzy Search](../framework/angular/examples/filters-fuzzy) - -# Lit - -- [Fuzzy Search](../framework/lit/examples/filters-fuzzy) - - +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` -## Fuzzy Filtering Guide +## Fuzzy Filtering (React) Guide Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. @@ -57,7 +43,7 @@ Here's an example of a custom fuzzy filter function: ```typescript import { rankItem } from '@tanstack/match-sorter-utils'; -import { FilterFn } from '@tanstack/table'; +import { FilterFn } from '@tanstack/react-table' const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { // Rank the item @@ -131,7 +117,7 @@ When using fuzzy filtering with column filtering, you might also want to sort th ```typescript import { compareItems } from '@tanstack/match-sorter-utils' -import { sortFns } from '@tanstack/table' +import { sortFns } from '@tanstack/react-table' const fuzzySort: SortFn = (rowA, rowB, columnId) => { let dir = 0 diff --git a/docs/framework/react/guide/global-faceting.md b/docs/framework/react/guide/global-faceting.md new file mode 100644 index 0000000000..f09ffaddf8 --- /dev/null +++ b/docs/framework/react/guide/global-faceting.md @@ -0,0 +1,107 @@ +--- +title: Global Faceting (React) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these React examples: + +- [Faceted Filters](../examples/filters-faceted) +### React Setup + +```tsx +import { useTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/react-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, +}) +``` + +## Global Faceting (React) Guide + +Global Faceting allows you to generate lists of values for all columns from the table's data. For example, a list of unique values in a table can be generated from all rows in all columns to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a table of numbers to be used as a range for a range slider filter component. + +### Global Faceting Row Models + +In order to use any of the global faceting features, add the appropriate faceted row models to your `rowModels`: + +```ts +import { + useTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/react-table' + +const features = tableFeatures({ columnFacetingFeature }) +// If the table also uses global filtering, include globalFilteringFeature too. + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), // required (other faceting methods depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + }, + // other options... +}) +``` + +### Use Global Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting table instance APIs to access the lists of values generated by the faceted row models. + +Use `table.getGlobalFacetedRowModel()` if you need to inspect the row model used to derive global facet 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 Global (Server-Side) Faceting + +If 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 `rowModels.facetedUniqueValues` and `rowModels.facetedMinMaxValues` factories. Global faceting requests use the internal `__global__` column ID. + +```ts +const globalFacets = await fetch('/api/faceting').then(res => res.json()) + +const table = useTable({ + features, + rowModels: { + facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() + return new Map(globalFacets.uniqueValues) + }, + facetedMinMaxValues: (_table, columnId) => () => { + if (columnId !== '__global__') return undefined + return globalFacets.minMaxValues + }, + }, + // other options... +}) +``` + +In this example, the custom factories return server-provided global facet values when TanStack Table asks for the `__global__` faceting context. Column-specific faceting can still use separate values by checking for each column ID. diff --git a/docs/guide/global-filtering.md b/docs/framework/react/guide/global-filtering.md similarity index 84% rename from docs/guide/global-filtering.md rename to docs/framework/react/guide/global-filtering.md index c9c3f01399..c062f8979d 100644 --- a/docs/guide/global-filtering.md +++ b/docs/framework/react/guide/global-filtering.md @@ -1,58 +1,31 @@ --- -title: Global Filtering Guide +title: Global Filtering (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Column Filters](../examples/filters) +- [Fuzzy Search](../examples/filters-fuzzy) +### React Setup -# React +```tsx +import { useTable, tableFeatures, globalFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/react-table' -- [Column Filters](../framework/react/examples/filters) -- [Faceted Filters](../framework/react/examples/filters-faceted) -- [Fuzzy Search](../framework/react/examples/filters-fuzzy) - -# Preact - -- [Column Filters](../framework/preact/examples/filters) -- [Faceted Filters](../framework/preact/examples/filters-faceted) -- [Fuzzy Search](../framework/preact/examples/filters-fuzzy) - -# Solid - -- [Column Filters](../framework/solid/examples/filters) -- [Faceted Filters](../framework/solid/examples/filters-faceted) -- [Fuzzy Search](../framework/solid/examples/filters-fuzzy) - -# Svelte - -- [Column Filters](../framework/svelte/examples/filtering) -- [Faceted Filters](../framework/svelte/examples/filters-faceted) -- [Fuzzy Search](../framework/svelte/examples/filters-fuzzy) - -# Vue - -- [Column Filters](../framework/vue/examples/filters) -- [Faceted Filters](../framework/vue/examples/filters-faceted) -- [Fuzzy Search](../framework/vue/examples/filters-fuzzy) - -# Angular - -- [Column Filters](../framework/angular/examples/filters) -- [Faceted Filters](../framework/angular/examples/filters-faceted) -- [Fuzzy Search](../framework/angular/examples/filters-fuzzy) - -# Lit - -- [Column Filters](../framework/lit/examples/filters) -- [Faceted Filters](../framework/lit/examples/filters-faceted) -- [Fuzzy Search](../framework/lit/examples/filters-fuzzy) +const features = tableFeatures({ globalFilteringFeature }) - +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` -## Global Filtering Guide +## Global Filtering (React) Guide Filtering comes in 2 flavors: Column Filtering and Global Filtering. @@ -80,7 +53,7 @@ If you have decided that you need to implement server-side global filtering inst No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. -```jsx +```tsx import { useTable, tableFeatures, @@ -104,7 +77,7 @@ Note: When using manual global filtering, many of the options that are discussed If you are using the built-in client-side global filtering, add the `globalFilteringFeature` to your features and the `filteredRowModel` to your row models: -```jsx +```tsx import { useTable, tableFeatures, @@ -128,7 +101,7 @@ const table = useTable({ The globalFilterFn option allows you to specify the filter function that will be used for global filtering. The filter function can be a string that references a built-in filter function, a string that references a custom filter function provided via the tableOptions.filterFns option, or a custom filter function. -```jsx +```tsx const table = useTable({ features, rowModels: { @@ -159,7 +132,7 @@ You can also define your own custom filter functions either as the globalFilterF The global filter state is stored in the table's state atoms and can be read with `table.atoms.globalFilter.get()` or from the current `table.store.state.globalFilter` snapshot. If you want to persist the global filter state outside of the table, use the `state.globalFilter` and `onGlobalFilterChange` options together. -```jsx +```tsx const [globalFilter, setGlobalFilter] = useState([]) const table = useTable({ @@ -175,7 +148,7 @@ const table = useTable({ The global filtering state is defined as an object with the following shape: -```jsx +```tsx interface GlobalFilter { globalFilter: any } @@ -185,7 +158,7 @@ interface GlobalFilter { TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. -```jsx +```tsx return (
**Note:** It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering.md). -```jsx +```tsx const customFilterFn = (rows, columnId, filterValue) => { // custom filter logic } @@ -222,7 +195,7 @@ If you want to set an initial global filter state when the table is initialized, However, you can also just specify the initial global filter state in the state.globalFilter option. -```jsx +```tsx const [globalFilter, setGlobalFilter] = useState("search term") //recommended to initialize globalFilter state here const table = useTable({ @@ -246,7 +219,7 @@ By default, global filtering is enabled for all columns. You can disable the glo Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. -```jsx +```tsx const columns = [ { header: () => 'Id', diff --git a/docs/guide/grouping.md b/docs/framework/react/guide/grouping.md similarity index 83% rename from docs/guide/grouping.md rename to docs/framework/react/guide/grouping.md index 0999694110..86406d3f47 100644 --- a/docs/guide/grouping.md +++ b/docs/framework/react/guide/grouping.md @@ -1,44 +1,31 @@ --- -title: Grouping Guide +title: Grouping (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Grouping](../examples/grouping) -# React +### React Setup -- [Grouping](../framework/react/examples/grouping) - -# Preact - -- [Grouping](../framework/preact/examples/grouping) - -# Solid - -- [Grouping](../framework/solid/examples/grouping) - -# Svelte - -- [Grouping](../framework/svelte/examples/grouping) - -# Vue - -- [Grouping](../framework/vue/examples/grouping) - -# Angular - -- [Grouping](../framework/angular/examples/grouping) - -# Lit +```tsx +import { useTable, tableFeatures, columnGroupingFeature, createGroupedRowModel, aggregationFns } from '@tanstack/react-table' -- [Grouping](../framework/lit/examples/grouping) +const features = tableFeatures({ columnGroupingFeature }) - +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + columns, + data, +}) +``` -## Grouping Guide +## Grouping (React) Guide There are 3 table features that can reorder columns, which happen in the following order: @@ -195,3 +182,43 @@ const table = useTable({ onGroupingChange: setGrouping, }) ``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```tsx +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +column.getAutoAggregationFn() +column.getAggregationFn() +``` + +Rows expose grouping helpers for grouped row rendering: + +```tsx +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose helpers for choosing between grouped, aggregated, placeholder, and normal cell rendering: + +```tsx +cell.getIsGrouped() +cell.getIsAggregated() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```tsx +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/guide/pagination.md b/docs/framework/react/guide/pagination.md similarity index 86% rename from docs/guide/pagination.md rename to docs/framework/react/guide/pagination.md index 30d99b490e..cf4dceb0bb 100644 --- a/docs/guide/pagination.md +++ b/docs/framework/react/guide/pagination.md @@ -1,55 +1,31 @@ --- -title: Pagination Guide +title: Pagination (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Pagination](../examples/pagination) -# React +### React Setup -- [Pagination](../framework/react/examples/pagination) -- [With TanStack Query](../framework/react/examples/with-tanstack-query) +```tsx +import { useTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/react-table' -# Preact - -- [Pagination](../framework/preact/examples/pagination) -- [With TanStack Query](../framework/preact/examples/with-tanstack-query) - -# Solid - -- [Pagination](../framework/solid/examples/pagination) -- [With TanStack Query](../framework/solid/examples/with-tanstack-query) - -# Svelte - -- [Pagination](../framework/svelte/examples/pagination) -- [With TanStack Query](../framework/svelte/examples/with-tanstack-query) - -# Vue - -- [Pagination](../framework/vue/examples/pagination) -- [With TanStack Query](../framework/vue/examples/with-tanstack-query) - -# Angular - -- [Pagination](../framework/angular/examples/pagination) -- [With TanStack Query](../framework/angular/examples/with-tanstack-query) -- [Remote Data](../framework/angular/examples/remote-data) - -# Lit - -- [Pagination](../framework/lit/examples/pagination) - -# Vanilla - -- [Pagination](../framework/vanilla/examples/pagination) +const features = tableFeatures({ rowPaginationFeature }) - +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` -## Pagination Guide +## Pagination (React) Guide TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. @@ -61,7 +37,7 @@ Using client-side pagination means that the `data` that you fetch will contain * Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../framework/react/examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. 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: @@ -79,7 +55,7 @@ Alternatively, instead of paginating the data, you can render all rows of a larg If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` to your features and the `paginatedRowModel` to your row models: -```jsx +```tsx import { useTable, tableFeatures, @@ -99,7 +75,7 @@ const table = useTable({ }) ``` -> **Migrating from v8?** See [useLegacyTable](../framework/react/guide/use-legacy-table) for incremental migration. +> **Migrating from v8?** See [useLegacyTable](./use-legacy-table) for incremental migration. ### Manual Server-Side Pagination @@ -111,7 +87,7 @@ No pagination row model is needed for server-side pagination, but if you have pr The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. -```jsx +```tsx import { useTable, tableFeatures, @@ -144,7 +120,7 @@ The `pagination` state is an object that contains the following properties: You can manage the `pagination` state just like any other state in the table instance. -```jsx +```tsx import { useTable, tableFeatures, @@ -175,7 +151,7 @@ const table = useTable({ Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. -```jsx +```tsx const table = useTable({ features, rowModels: { @@ -202,7 +178,7 @@ Besides the `manualPagination`, `pageCount`, and `rowCount` options which are us By default, `pageIndex` is reset to `0` when page-altering state changes occur, such as when the `data` is updated, filters change, grouping changes, etc. This behavior is automatically disabled when `manualPagination` is true but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. -```jsx +```tsx const table = useTable({ features, rowModels: { @@ -237,7 +213,7 @@ There are several pagination table instance APIs that are useful for hooking up > **Note**: These pagination APIs are available when using `rowPaginationFeature`. -```jsx +```tsx + + +
+ ) : null, + }, + //... +] +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```tsx +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly: + +```tsx +
+ {table.getTopRows().map(row => ( + + ))} + {table.getCenterRows().map(row => ( + + ))} + {table.getBottomRows().map(row => ( + + ))} + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```tsx +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```tsx +const table = useTable({ + features, + rowModels: {}, + columns, + data, + enableRowPinning: row => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```tsx +const table = useTable({ + features, + rowModels: {}, + columns, + data, + keepPinnedRows: false, +}) +``` diff --git a/docs/guide/row-selection.md b/docs/framework/react/guide/row-selection.md similarity index 90% rename from docs/guide/row-selection.md rename to docs/framework/react/guide/row-selection.md index 37018b0b17..4c136ef725 100644 --- a/docs/guide/row-selection.md +++ b/docs/framework/react/guide/row-selection.md @@ -1,45 +1,28 @@ --- -title: Row Selection Guide +title: Row Selection (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Row Selection](../examples/row-selection) +### React Setup -# React - -- [Row Selection](../framework/react/examples/row-selection) - -# Preact - -- [Row Selection](../framework/preact/examples/row-selection) - -# Solid - -- [Row Selection](../framework/solid/examples/row-selection) - -# Svelte - -- [Row Selection](../framework/svelte/examples/row-selection) - -# Vue - -- [Row Selection](../framework/vue/examples/row-selection) - -# Angular - -- [Row Selection](../framework/angular/examples/row-selection) -- [Row Selection Signal](../framework/angular/examples/row-selection-signal) - -# Lit +```tsx +import { useTable, tableFeatures, rowSelectionFeature } from '@tanstack/react-table' -- [Row Selection](../framework/lit/examples/row-selection) +const features = tableFeatures({ rowSelectionFeature }) - +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` -## Row Selection Guide +## Row Selection (React) Guide The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. diff --git a/docs/guide/sorting.md b/docs/framework/react/guide/sorting.md similarity index 96% rename from docs/guide/sorting.md rename to docs/framework/react/guide/sorting.md index 2bfe98c713..f80fb803d9 100644 --- a/docs/guide/sorting.md +++ b/docs/framework/react/guide/sorting.md @@ -1,48 +1,31 @@ --- -title: Sorting Guide +title: Sorting (React) Guide --- ## Examples -Want to skip to the implementation? Check out these examples: +Want to skip to the implementation? Check out these React examples: - +- [Sorting](../examples/sorting) -# React +### React Setup -- [Sorting](../framework/react/examples/sorting) - -# Preact - -- [Sorting](../framework/preact/examples/sorting) - -# Solid - -- [Sorting](../framework/solid/examples/sorting) - -# Svelte - -- [Sorting](../framework/svelte/examples/sorting) - -# Vue - -- [Sorting](../framework/vue/examples/sorting) - -# Angular - -- [Sorting](../framework/angular/examples/sorting) - -# Lit - -- [Sorting](../framework/lit/examples/sorting) - -# Vanilla +```tsx +import { useTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/react-table' -- [Sorting](../framework/vanilla/examples/sorting) +const features = tableFeatures({ rowSortingFeature }) - +const table = useTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` -## Sorting Guide +## Sorting (React) Guide TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. @@ -104,7 +87,7 @@ const table = useTable({ If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. -```jsx +```tsx const table = useTable({ features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, @@ -132,7 +115,7 @@ Whether or not you should use client-side or server-side sorting depends entirel If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. -```jsx +```tsx const [sorting, setSorting] = useState([]) //... const table = useTable({ @@ -154,7 +137,7 @@ const table = useTable({ To implement client-side sorting, add the `rowSortingFeature` to your features and the `sortedRowModel` to your row models. Import `createSortedRowModel` and `sortFns` from TanStack Table: -```jsx +```tsx import { useTable, tableFeatures, @@ -213,7 +196,7 @@ Every sorting function receives 2 rows and a column ID and are expected to compa | `0` | `a === b` | | `1` | `a > b` | -```jsx +```tsx const columns = [ { header: () => 'Name', @@ -266,7 +249,7 @@ There are a lot of table and column options that you can use to further customiz You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. -```jsx +```tsx const columns = [ { header: () => 'ID', @@ -293,7 +276,7 @@ const table = useTable({ By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. -```jsx +```tsx const columns = [ { header: () => 'Name', @@ -323,7 +306,7 @@ const table = useTable({ Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. -```jsx +```tsx const columns = [ { header: () => 'Rank', @@ -348,7 +331,7 @@ In not specified, the default value for `sortUndefined` is `1`, and undefined va > NOTE: `'first'` and `'last'` options are available in v9. -```jsx +```tsx const columns = [ { header: () => 'Rank', @@ -374,7 +357,7 @@ Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sort > Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. -```jsx +```tsx const table = useTable({ features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, @@ -392,7 +375,7 @@ Sorting by multiple columns at once is enabled by default if using the `column.g You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. -```jsx +```tsx const columns = [ { header: () => 'Created At', @@ -415,7 +398,7 @@ const table = useTable({ By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. -```jsx +```tsx const table = useTable({ features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, @@ -431,7 +414,7 @@ const table = useTable({ By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. -```jsx +```tsx const table = useTable({ features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, @@ -445,7 +428,7 @@ const table = useTable({ By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. -```jsx +```tsx const table = useTable({ features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, diff --git a/docs/framework/react/guide/virtualization.md b/docs/framework/react/guide/virtualization.md new file mode 100644 index 0000000000..7b053c89e1 --- /dev/null +++ b/docs/framework/react/guide/virtualization.md @@ -0,0 +1,302 @@ +--- +title: Virtualization (React) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these React examples: + +- [Virtualized Columns](../examples/virtualized-columns) +- [Virtualized Rows](../examples/virtualized-rows) +- [Virtualized Infinite Scrolling](../examples/virtualized-infinite-scrolling) +- [Virtualized Columns Experimental](../examples/virtualized-columns-experimental) +- [Virtualized Rows Experimental](../examples/virtualized-rows-experimental) + +### React Setup + +Install and import the React virtualizer adapter from `@tanstack/react-virtual`. TanStack Table still owns rows, columns, and table state; the virtualizer owns scroll indexes and measurements. +Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/react/examples/table). + +## Virtualization (React) Guide + +The TanStack Table packages do not come with any virtualization APIs or features built in. Virtualization is a rendering strategy, not a table feature. You can use TanStack Table with any virtualization library, but the official examples use TanStack Virtual. + +TanStack Table and TanStack Virtual solve different parts of the problem: + +- TanStack Table builds the row models, columns, headers, cells, sizing, sorting, filtering, and other table state. +- TanStack Virtual decides which item indexes should be rendered for the current scroll position. +- Your table renderer maps those virtual indexes back to rows, headers, and cells. + +### When To Use Virtualization + +Use virtualization when your table has a very large number of rows, columns, or both. Virtualization keeps the DOM small by only rendering the items that are visible in the scroll viewport plus a small overscan buffer. + +Virtualization is not a replacement for server-side pagination, filtering, or sorting. If the data is virtualized on the client, the data still needs to exist on the client. If your dataset is too large to load into the browser, use server-side data operations or infinite scrolling. + +For small tables, normal rendering is simpler and usually preferable. + +### Install TanStack Virtual + +Install the React virtualizer adapter: + +```sh +npm install @tanstack/react-virtual +``` + +The React examples use `useVirtualizer` from `@tanstack/react-virtual`. TanStack Table still owns rows, columns, headers, cells, sizing, sorting, filtering, and other table state; TanStack Virtual decides which item indexes should render for the current scroll position. +### The Basic Pattern + +Most virtualized table implementations follow the same pattern: + +1. Create a fixed-height scroll container. +2. Pass the scroll element to the virtualizer. +3. Use `table.getRowModel().rows` or `table.getVisibleLeafColumns()` as the source list. +4. Configure `count`, `estimateSize`, `overscan`, and optional `measureElement`. +5. Render only virtual items. +6. Use virtual offsets or spacer padding to preserve the full scroll geometry. + +Here is a compact row virtualization example: + +```tsx +const rows = table.getRowModel().rows + +const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => tableContainerRef.current, + estimateSize: () => 33, + overscan: 5, +}) + + + {rowVirtualizer.getVirtualItems().map(virtualRow => { + const row = rows[virtualRow.index] + + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + +``` + +### Virtualized Rows + +The [virtualized rows examples](../examples/virtualized-rows) show how to render large row counts while keeping the DOM small. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The core idea is that sorting, filtering, grouping, and other row-model work still comes from TanStack Table. The virtualizer reads from the final table row model: + +```tsx +const rows = table.getRowModel().rows +``` + +The row virtualizer is configured with `count: rows.length`, a row height estimate, the scroll container, and an overscan value. The `tbody` is given the full virtual height with `rowVirtualizer.getTotalSize()`, while each rendered row is absolutely positioned with `transform: translateY(...)`. + +The examples render cells from the current row with APIs like `row.getVisibleCells()` or `row.getAllCells()`, depending on whether the example needs visibility-aware cells or all cells. + +The official examples use large generated datasets, commonly tens or hundreds of thousands of rows. They also support dynamic row heights by using `measureElement` when possible. The examples skip dynamic row measurement in Firefox because Firefox can measure table border height differently. + +### Virtualized Columns + +The [virtualized columns examples](../examples/virtualized-columns) show how to render large row and column counts. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +Column virtualization uses the current visible column list: + +```tsx +const visibleColumns = table.getVisibleLeafColumns() +``` + +The column virtualizer is configured for horizontal virtualization: + +```tsx +const columnVirtualizer = useVirtualizer({ + count: visibleColumns.length, + estimateSize: index => visibleColumns[index].getSize(), + getScrollElement: () => tableContainerRef.current, + horizontal: true, + overscan: 3, +}) +``` + +Column virtualization uses a different rendering strategy than row virtualization. Instead of absolutely positioning columns, the examples add fake spacer cells to the left and right: + +```tsx +const virtualColumns = columnVirtualizer.getVirtualItems() +const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 +const virtualPaddingRight = + columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) +``` + +Those spacer cells preserve the horizontal scroll width while the renderer only mounts the virtual columns. This approach keeps row rendering table-like and allows dynamic row height measurement to keep working. + +### Virtualized Rows And Columns Together + +The official virtualized columns examples also virtualize rows. In those examples: + +- The row virtualizer controls vertical positioning and total body height. +- The column virtualizer controls horizontal header/cell rendering and left/right spacer cells. +- `virtualRow.index` maps to `rows[virtualRow.index]`. +- `virtualColumn.index` maps to `visibleCells[virtualColumn.index]`. + +Always use virtual indexes against the same current row and column lists returned by the table. If sorting, filtering, pagination, grouping, or column visibility changes, recompute the virtualized rows and columns from the current table state. + +### Virtualized Infinite Scrolling + +The [virtualized infinite scrolling examples](../examples/virtualized-infinite-scrolling) combine row virtualization with progressive data fetching. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The common pattern is: + +1. Fetch a page of rows. +2. Flatten fetched pages into the table `data`. +3. Use row virtualization over the loaded rows. +4. Listen to scroll events on the table container. +5. Fetch the next page when the user scrolls near the bottom. + +The React example uses TanStack Query's `useInfiniteQuery`, but the same pattern works with any data-fetching layer. + +```tsx +const { scrollHeight, scrollTop, clientHeight } = scrollElement + +if (scrollHeight - scrollTop - clientHeight < 500) { + fetchNextPage() +} +``` + +If sorting is handled by the server, use manual sorting so the fetched data reflects the whole backend dataset rather than only the currently loaded rows. When sorting changes and the fetched dataset is replaced, scroll back to the top with `rowVirtualizer.scrollToIndex(0)`. + +### Dynamic Row Heights + +Dynamic row heights are useful when content can wrap or expand. They are also more complex than fixed-height rows. + +Use `estimateSize` as the virtualizer's initial guess: + +```tsx +estimateSize: () => 33 +``` + +Then use `measureElement` to refine the actual row height after rendering: + +```tsx + rowVirtualizer.measureElement(node)} +> +``` + +Set `data-index` on each row so the virtualizer can associate measurements with the correct item. In non-React adapters, call `measureElement` through the adapter-appropriate ref, action, directive, or controller. + +Overscan helps avoid blank regions while measurements settle. If every row has a known fixed height, skip dynamic measurement and use the fixed height estimate instead. + +### Sticky Headers And Semantic Table Markup + +The examples still use semantic table tags, but they change table layout CSS to support virtual positioning and sticky headers. + +Dynamic row virtualization commonly requires: + +```css +table { + display: grid; +} + +thead { + display: grid; + position: sticky; + top: 0; +} + +tr { + display: flex; +} +``` + +Rows are absolutely positioned inside a relatively positioned `tbody`, and cells use flex sizing so they can match `column.getSize()` or `cell.column.getSize()`. This is intentional. Native table layout does not work well with dynamic-height virtual rows that are positioned independently. + +### Performance Tips + +- Keep virtualizers near the components that render the virtualized items. +- Avoid re-rendering the full table body on every scroll. +- Keep row, column, and data references stable where possible. +- Use `overscan` deliberately. More overscan reduces visible blanking, while less overscan reduces DOM nodes. +- Avoid expensive cell renderers in very large virtualized tables. +- Test production builds. React development mode is slower, and the React examples call this out. +- Prefer fixed row sizes when the UI allows it. +- For column virtualization, use `column.getSize()`, `header.getSize()`, and `cell.column.getSize()` consistently. + +### Experimental React Virtualization Examples + +The React examples include [Virtualized Rows Experimental](../examples/virtualized-rows-experimental) and [Virtualized Columns Experimental](../examples/virtualized-columns-experimental). These examples are advanced experiments for reducing React render work during scroll. + +Use the standard examples first. Reach for the experimental examples only after profiling shows that React render work during scroll is the bottleneck. + +The experimental examples use TanStack Virtual's `onChange` callback to imperatively update DOM styles instead of passing every scroll-position change through React render. + +The experimental row example: + +- Keeps a `rowRefsMap` from virtual row index to DOM row element. +- Uses `onChange` to update `rowRef.style.transform`. +- Memoizes rows with a custom `React.memo` comparator while scrolling. + +```tsx +onChange: instance => { + instance.getVirtualItems().forEach(virtualRow => { + const rowRef = rowRefsMap.current.get(virtualRow.index) + if (!rowRef) return + rowRef.style.transform = `translateY(${virtualRow.start}px)` + }) +} +``` + +The experimental column example also mutates scroll-position-only DOM styles: + +```tsx +tableContainerRef.current?.style.setProperty( + '--virtual-padding-left', + `${virtualPaddingLeft}px`, +) + +tableContainerRef.current?.style.setProperty( + '--virtual-padding-right', + `${virtualPaddingRight}px`, +) + +tableBodyRef.current!.style.height = `${instance.getTotalSize()}px` +``` + +It updates row transforms through DOM refs and memoizes header and cell components while the column virtualizer is scrolling. + +Because these examples update the DOM outside React's normal render flow, keep the imperative updates narrowly scoped to scroll-position-only styles like `transform`, body height, and spacer widths. Do not use this pattern for business state, table state, data, sorting, filtering, or cell values. + +### Choosing An Example + +| Need | Start with | +| --- | --- | +| Many rows, normal columns | [Virtualized Rows](../examples/virtualized-rows) | +| Many columns | [Virtualized Columns](../examples/virtualized-columns) | +| Many rows and columns | [Virtualized Columns](../examples/virtualized-columns) | +| Remote data loaded as the user scrolls | [Virtualized Infinite Scrolling](../examples/virtualized-infinite-scrolling) | +| React scroll performance after profiling | [Experimental React examples](../examples/virtualized-rows-experimental) | + +### Common Pitfalls + +- Forgetting to give the scroll container a fixed height. +- Rendering `table.getRowModel().rows.map(...)` instead of `virtualizer.getVirtualItems().map(...)`. +- Using virtual indexes against stale rows or columns after sorting, filtering, pagination, grouping, or column visibility changes. +- Forgetting left and right spacer cells for horizontal virtualization. +- Using `measureElement` when every row has a fixed height. +- Expecting TanStack Table to provide virtualization APIs as a feature. +- Mixing client-side virtualization with server-side sorting or filtering inconsistently. diff --git a/docs/framework/solid/guide/column-faceting.md b/docs/framework/solid/guide/column-faceting.md new file mode 100644 index 0000000000..4833f55789 --- /dev/null +++ b/docs/framework/solid/guide/column-faceting.md @@ -0,0 +1,115 @@ +--- +title: Column Faceting (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Faceted Filters](../examples/filters-faceted) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/solid-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = createTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + get data() { + return data + }, +}) +``` + +## Column Faceting (Solid) Guide + +Column Faceting is a feature that allows you to generate lists of values for a given column from that column's data. For example, a list of unique values in a column can be generated from all rows in that column to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a column of numbers to be used as a range for a range slider filter component. + +### Column Faceting Row Models + +In order to use any of the column faceting features, add the `columnFacetingFeature` to your features and the appropriate faceted row models to `rowModels`. + +```ts +import { + createTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/solid-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = createTable({ + features, + rowModels: { + 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 + }, + columns, + 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]; +``` + +### Custom (Server-Side) Faceting + +If 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. You can use the `getFacetedUniqueValues` and `getFacetedMinMaxValues` table options to resolve the faceted values from the server-side. + +```ts +const facetingQuery = useQuery( + //... +) + +const table = createTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, + getFacetedUniqueValues: (table, columnId) => { + const uniqueValueMap = new Map() + //... + return uniqueValueMap + }, + getFacetedMinMaxValues: (table, columnId) => { + //... + return [min, max] + }, + //... +}) +``` + +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/solid/guide/column-filtering.md b/docs/framework/solid/guide/column-filtering.md new file mode 100644 index 0000000000..6b322db5db --- /dev/null +++ b/docs/framework/solid/guide/column-filtering.md @@ -0,0 +1,372 @@ +--- +title: Column Filtering (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Fuzzy Search](../examples/filters-fuzzy) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/solid-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Column Filtering (Solid) 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 `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```tsx +const table = createTable({ + features: tableFeatures({ columnFilteringFeature }), + rowModels: {}, // no filteredRowModel needed for manual server-side filtering + 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` to your features and the `filteredRowModel` to your row models. Import `createFilteredRowModel` and `filterFns` from TanStack Table: + +```tsx +import { + createTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/solid-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + 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 + +You can access the column filter state from the table instance with `table.atoms.columnFilters.get()` or from the current `table.store.state` snapshot. + +```tsx +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.columnFilters.get()) // access the current column filters state +``` + +However, if you need to access the column filter state before the table is initialized, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state, you can control/manage the column filter state in your own state management with the `state.columnFilters` and `onColumnFiltersChange` table options. + +```tsx +const [columnFilters, setColumnFilters] = createSignal([]) // can set initial column filter state here +//... +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + state: { + columnFilters, + }, + onColumnFiltersChange: setColumnFilters, +}) +``` + +#### 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`. + +```tsx +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 10 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `arrIncludes` - Item inclusion within an array +- `arrIncludesAll` - All items included in an array +- `arrIncludesSome` - Some items included in an array +- `equals` - Object/referential equality `Object.is`/`===` +- `weakEquals` - Weak object/referential equality `==` +- `inNumberRange` - Number range inclusion + +You can also define your own custom filter functions either as the `filterFn` column option, or as a global filter function using the `filterFns` table option. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +When defining a custom filter function in either the `filterFn` column option or the `filterFns` table option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = (row: Row, columnId: string, filterValue: any, addMeta: (meta: any) => 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. + +```tsx +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // use custom global filter function + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + } +] +//... +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }), + }, + columns, + data, +}) +``` + +##### 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`. + +```tsx +const startsWithFilterFn = ( + row: Row, + columnId: string, + filterValue: number | string, //resolveFilterValue will transform this 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. + +```tsx +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + enableColumnFilters: false, // disable column filtering for all columns +}) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```tsx +const table = createTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + filterFromLeafRows: true, // filter and search through sub-rows +}) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```tsx +const table = createTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +}) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `onChange` or `onBlur` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/solid/guide/column-ordering.md b/docs/framework/solid/guide/column-ordering.md new file mode 100644 index 0000000000..6fcda49688 --- /dev/null +++ b/docs/framework/solid/guide/column-ordering.md @@ -0,0 +1,152 @@ +--- +title: Column Ordering (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Column Ordering](../examples/column-ordering) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnOrderingFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Ordering (Solid) 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 left, center (unpinned), and right 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. + +```tsx +const table = createTable({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + 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. + +```tsx +const [columnOrder, setColumnOrder] = createSignal(['columnId1', 'columnId2', 'columnId3']) +//... +const table = createTable({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + state: { + columnOrder, + //... + } + onColumnOrderChange: setColumnOrder, + //... +}); +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, you can set up the logic something like this: + +```tsx +const [columnOrder, setColumnOrder] = createSignal(columns.map(c => c.id)); + +//depending on your dnd solution of choice, you may or may not need state like this +const [movingColumnId, setMovingColumnId] = createSignal(null); +const [targetColumnId, setTargetColumnId] = createSignal(null); + +//util function to splice and reorder the columnOrder array +const reorderColumn = ( + movingColumnId: Column, + targetColumnId: Column, +): string[] => { + const newColumnOrder = [...columnOrder]; + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + newColumnOrder.splice(newColumnOrder.indexOf(movingColumnId), 1)[0], + ); + setColumnOrder(newColumnOrder); +}; + +const handleDragEnd = (e: DragEvent) => { + if(!movingColumnId || !targetColumnId) return; + setColumnOrder(reorderColumn(movingColumnId, targetColumnId)); +}; + +//use your dnd solution of choice +``` + +### 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. + +```tsx +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. + +```tsx +column.getIndex() +column.getIndex('left') +column.getIndex('center') +column.getIndex('right') + +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 (Solid) + +There are undoubtedly many ways to implement drag and drop features along-side TanStack Table. Here are a few suggestions in order for you to not have a bad time: + +1. Do NOT try to use [`"react-dnd"`](https://react-dnd.github.io/react-dnd/docs/overview) _if you are using Solid or newer_. framework-specific DnD was an important library for its time, but it now does not get updated very often, and it has incompatibilities with Solid, especially in Solid development mode. It is still possible to get it to work, but there are newer alternatives that have better compatibility and are more actively maintained. framework-specific DnD's Provider may also interfere and conflict with any other DnD solutions you may want to try in your app. + +2. Use [`"@dnd-kit/core"`](https://dndkit.com/). DnD Kit is a modern, modular and lightweight drag and drop library that is highly compatible with the modern Solid ecosystem, and it works well with semantic `
- {flexRender(cell.column.columnDef.cell, cell.getContext())} +
{/* render cell */}
` markup. The official framework-specific DnD examples, Column DnD and Row DnD, use DnD Kit. + +3. Consider other DnD libraries like [`"react-beautiful-dnd"`](https://github.com/atlassian/react-beautiful-dnd), but be aware of their potentially large bundle sizes, maintenance status, and compatibility with `
` markup. + +4. Consider using native browser events and state management to implement lightweight drag and drop features. However, be aware that this approach may not be best for mobile users if you do not go the extra mile to implement proper touch events. [Material React Table V2](https://www.material-react-table.com/docs/examples/column-ordering) is an example of a library that implements TanStack Table with only browser drag and drop events such as `onDragStart`, `onDragEnd`, `onDragEnter` and no other dependencies. Browse its source code to see how it is done. diff --git a/docs/framework/solid/guide/column-pinning.md b/docs/framework/solid/guide/column-pinning.md new file mode 100644 index 0000000000..b59f404395 --- /dev/null +++ b/docs/framework/solid/guide/column-pinning.md @@ -0,0 +1,165 @@ +--- +title: Column Pinning (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnPinningFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ columnPinningFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Pinning (Solid) 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 left or right. + +### 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 left, center (unpinned), and right 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.left` and `columnPinning.right` 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. + +```tsx +import { createTable, tableFeatures, columnPinningFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ columnPinningFeature }) + +const [columnPinning, setColumnPinning] = createSignal({ + left: [], + right: [], +}) + +const table = createTable({ + features, + rowModels: {}, + //... + state: { + columnPinning, + //... + }, + onColumnPinningChange: setColumnPinning, + //... +}) +``` + +### 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 + +```tsx +const table = createTable({ + features, + rowModels: {}, + //... + initialState: { + columnPinning: { + left: ['expand-column'], + right: ['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 left or right. 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 `left` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `right` 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. + +```tsx +table.setColumnPinning({ + left: ['firstName'], + right: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```tsx +table.getLeftLeafColumns() +table.getCenterLeafColumns() +table.getRightLeafColumns() + +table.getLeftVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getRightVisibleLeafColumns() + +table.getLeftHeaderGroups() +table.getCenterHeaderGroups() +table.getRightHeaderGroups() + +table.getLeftFooterGroups() +table.getCenterFooterGroups() +table.getRightFooterGroups() + +table.getLeftFlatHeaders() +table.getCenterFlatHeaders() +table.getRightFlatHeaders() + +table.getLeftLeafHeaders() +table.getCenterLeafHeaders() +table.getRightLeafHeaders() +``` + +You can also request pinned leaf columns by region with `table.getPinnedLeafColumns(position)` and visible pinned leaf columns with `table.getPinnedVisibleLeafColumns(position)`. + +```tsx +table.getPinnedLeafColumns('left') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('right') + +table.getPinnedVisibleLeafColumns('left') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('right') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'left'` or `'right'` to check one pinned side. + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getLeftHeaderGroups`, `table.getCenterHeaderGroups`, `table.getRightHeaderGroups`, `row.getLeftVisibleCells`, `row.getCenterVisibleCells`, and `row.getRightVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/solid/guide/column-resizing.md b/docs/framework/solid/guide/column-resizing.md new file mode 100644 index 0000000000..15aef71c4c --- /dev/null +++ b/docs/framework/solid/guide/column-resizing.md @@ -0,0 +1,220 @@ +--- +title: Column Resizing (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnResizingFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ columnResizingFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Resizing (Solid) 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 `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. + +```tsx +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + createTable, +} from '@tanstack/solid-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns = [ + { + accessorKey: 'id', + enableResizing: false, // disable resizing for just this column + size: 200, // starting column size + }, + //... +] + +const table = createTable({ + features, + rowModels: {}, + 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 React 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 React 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. + +```tsx +const table = createTable({ + //... + columnResizeMode: 'onChange', // change column resize mode to "onChange" +}) +``` + +### Column Resize Direction + +By default, TanStack Table assumes that the table markup is laid out in a left-to-right direction. For right-to-left layouts, you may need to change the column resize direction to `"rtl"`. + +```tsx +const table = createTable({ + //... + columnResizeDirection: 'rtl', // change column resize direction to "rtl" for certain locales +}) +``` + +### Connect Column Resizing APIs to UI + +There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI. + +#### Column Size APIs + +To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs: + +```ts +header.getSize() +column.getSize() +cell.column.getSize() +``` + +How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes. + +```tsx + +) +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `createSignal`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/solid/guide/expanding.md b/docs/framework/solid/guide/expanding.md new file mode 100644 index 0000000000..56e400651a --- /dev/null +++ b/docs/framework/solid/guide/expanding.md @@ -0,0 +1,279 @@ +--- +title: Expanding (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Expanding](../examples/expanding) + +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, rowExpandingFeature, createExpandedRowModel } from '@tanstack/solid-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = createTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + columns, + get data() { + return data + }, +}) +``` + +## Expanding Feature (Solid) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` to your features and the `expandedRowModel` to your row models: + +```ts +import { + createTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/solid-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = createTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are essentially child rows that inherit the same column structure as their parent rows. If your data object already includes these expanded rows data, you can utilize the `getSubRows` function to specify these child rows. However, if your data object does not contain the expanded rows data, they can be treated as custom expanded data, which is discussed in next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 } + ] + }, + { id: 3, + name: 'Doe', + age: 40, + children: [ + { id: 4, name: 'Alice', age: 10 } + ] + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +const table = createTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +}) +``` + +> **Note:** You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +```tsx + + + {(row) => ( + <> + + + {(cell) => ( + + )} + + + + + + + + + )} + + +``` + +### Expanded rows state + +If you need to control the expanded state of the rows in your table, you can do so by using the expanded state and the `onExpandedChange` option. This allows you to manage the expanded state according to your requirements. + +```ts +const [expanded, setExpanded] = createSignal({}) + +const table = createTable({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + state: { + expanded, + }, + onExpandedChange: setExpanded, +}) +``` + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows with IDs present as keys in the record and have their value set to true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. For example, you can add a button UI within the columns definition. + +```tsx +const columns = [ + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'age', + header: 'Age', + }, + { + header: 'Children', + cell: ({ row }) => + row.getCanExpand() ? ( + + ) : null, + }, +] +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```tsx +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```tsx +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, the filtering process starts from the parent rows and moves downwards. This means if a parent row is excluded by the filter, all its child rows will also be excluded. However, you can change this behavior by using the `filterFromLeafRows` option. When this option is enabled, the filtering process starts from the leaf (child) rows and moves upwards. This ensures that a parent row will be included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. Additionally, you can control how deep into the child hierarchy the filter process goes by using the `maxLeafRowFilterDepth` option. This option allows you to specify the maximum depth of child rows that the filter should consider. + +```ts +//... +const table = createTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parents page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +const table = createTable({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Pinning Guide](../../../guide/pinning.md) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const table = createTable({ + features: tableFeatures({ rowExpandingFeature }), + rowModels: {}, // no expandedRowModel needed for manual expanding + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/solid/guide/fuzzy-filtering.md b/docs/framework/solid/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..b52b4a097f --- /dev/null +++ b/docs/framework/solid/guide/fuzzy-filtering.md @@ -0,0 +1,155 @@ +--- +title: Fuzzy Filtering (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Fuzzy Search](../examples/filters-fuzzy) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/solid-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Fuzzy Filtering (Solid) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement a client side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> **Note:** You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked in order to work better with TanStack Table's row by row filtering approach. + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +Here's an example of a custom fuzzy filter function: + +```typescript +import { rankItem } from '@tanstack/match-sorter-utils'; +import { FilterFn } from '@tanstack/solid-table' + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the rankItem function from the @tanstack/match-sorter-utils library to rank the item. We then store the ranking information in the meta data of the row, and return whether the item passed the ranking criteria. + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, you can specify the fuzzy filter function in the globalFilterFn option of the table instance: + +```typescript +import { + createTable, + tableFeatures, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + filterFns, + sortFns, +} from '@tanstack/solid-table' + +const features = tableFeatures({ globalFilteringFeature, rowSortingFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + fuzzy: fuzzyFilter, + }), + sortedRowModel: createSortedRowModel(sortFns), // needed if you want sorting with fuzzy rank + }, + columns, + data, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, pass your fuzzy filter function to `createFilteredRowModel` (merging it with the built-in `filterFns`). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```typescript +const column = [ + { + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +]; +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```typescript +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFns } from '@tanstack/solid-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId]?.itemRank!, + rowB.columnFiltersMeta[columnId]?.itemRank! + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then specify this sorting function in the sortFn option of the column definition: + +```typescript +{ + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + sortFn: 'fuzzySort', //using our custom fuzzy sort function +} +``` diff --git a/docs/framework/solid/guide/global-faceting.md b/docs/framework/solid/guide/global-faceting.md new file mode 100644 index 0000000000..78fdc51b8a --- /dev/null +++ b/docs/framework/solid/guide/global-faceting.md @@ -0,0 +1,111 @@ +--- +title: Global Faceting (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Faceted Filters](../examples/filters-faceted) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/solid-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = createTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + get data() { + return data + }, +}) +``` + +## Global Faceting (Solid) Guide + +Global Faceting allows you to generate lists of values for all columns from the table's data. For example, a list of unique values in a table can be generated from all rows in all columns to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a table of numbers to be used as a range for a range slider filter component. + +### Global Faceting Row Models + +In order to use any of the global faceting features, add the appropriate faceted row models to your `rowModels`: + +```ts +import { + createTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/solid-table' + +const features = tableFeatures({ columnFacetingFeature }) +// If the table also uses global filtering, include globalFilteringFeature too. + +const table = createTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), // required (other faceting methods depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + }, + // other options... +}) +``` + +### Use Global Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting table instance APIs to access the lists of values generated by the faceted row models. + +Use `table.getGlobalFacetedRowModel()` if you need to inspect the row model used to derive global facet 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 Global (Server-Side) Faceting + +If 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 `rowModels.facetedUniqueValues` and `rowModels.facetedMinMaxValues` factories. Global faceting requests use the internal `__global__` column ID. + +```ts +const globalFacets = await fetch('/api/faceting').then(res => res.json()) + +const table = createTable({ + features, + rowModels: { + facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() + return new Map(globalFacets.uniqueValues) + }, + facetedMinMaxValues: (_table, columnId) => () => { + if (columnId !== '__global__') return undefined + return globalFacets.minMaxValues + }, + }, + // other options... +}) +``` + +In this example, the custom factories return server-provided global facet values when TanStack Table asks for the `__global__` faceting context. Column-specific faceting can still use separate values by checking for each column ID. diff --git a/docs/framework/solid/guide/global-filtering.md b/docs/framework/solid/guide/global-filtering.md new file mode 100644 index 0000000000..85a7da9c37 --- /dev/null +++ b/docs/framework/solid/guide/global-filtering.md @@ -0,0 +1,243 @@ +--- +title: Global Filtering (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Column Filters](../examples/filters) +- [Fuzzy Search](../examples/filters-fuzzy) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, globalFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/solid-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Global Filtering (Solid) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +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 Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```tsx +import { + createTable, + tableFeatures, + globalFilteringFeature, +} from '@tanstack/solid-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = createTable({ + features, + rowModels: {}, // no filteredRowModel needed for manual server-side global filtering + data, + columns, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` to your features and the `filteredRowModel` to your row models: + +```tsx +import { + createTable, + tableFeatures, + globalFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/solid-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + // other options... +}) +``` + +### Global Filter Function + +The globalFilterFn option allows you to specify the filter function that will be used for global filtering. The filter function can be a string that references a built-in filter function, a string that references a custom filter function provided via the tableOptions.filterFns option, or a custom filter function. + +```tsx +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + data, + columns, + globalFilterFn: 'text', // built-in filter function +}) +``` + +By default there are 10 built-in filter functions to choose from: + +- includesString - Case-insensitive string inclusion +- includesStringSensitive - Case-sensitive string inclusion +- equalsString - Case-insensitive string equality +- equalsStringSensitive - Case-sensitive string equality +- arrIncludes - Item inclusion within an array +- arrIncludesAll - All items included in an array +- arrIncludesSome - Some items included in an array +- equals - Object/referential equality Object.is/=== +- weakEquals - Weak object/referential equality == +- inNumberRange - Number range inclusion + +You can also define your own custom filter functions either as the globalFilterFn table option. + +### Global Filter State + +The global filter state is stored in the table's state atoms and can be read with `table.atoms.globalFilter.get()` or from the current `table.store.state.globalFilter` snapshot. If you want to persist the global filter state outside of the table, use the `state.globalFilter` and `onGlobalFilterChange` options together. + +```tsx +const [globalFilter, setGlobalFilter] = createSignal([]) + +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + state: { + globalFilter, + }, + onGlobalFilterChange: setGlobalFilter, +}) +``` + +The global filtering state is defined as an object with the following shape: + +```tsx +interface GlobalFilter { + globalFilter: any +} +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. + +```tsx +return ( +
+ table.setGlobalFilter(String(e.target.value))} + placeholder="Search..." + /> +
+) +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the globalFilterFn option. + +> **Note:** It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering.md). + +```tsx +const customFilterFn = (rows, columnId, filterValue) => { + // custom filter logic +} + +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table initialState option. + +However, you can also just specify the initial global filter state in the state.globalFilter option. + +```tsx +const [globalFilter, setGlobalFilter] = createSignal("search term") //recommended to initialize globalFilter state here + +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + initialState: { + globalFilter: 'search term', // if not managing globalFilter state, set initial state here + }, + state: { + globalFilter, // pass our managed globalFilter state to the table + }, +}) +``` + +> NOTE: Do not use both initialState.globalFilter and state.globalFilter at the same time, as the initialized state in the state.globalFilter will override the initialState.globalFilter. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```tsx +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` diff --git a/docs/framework/solid/guide/grouping.md b/docs/framework/solid/guide/grouping.md new file mode 100644 index 0000000000..5ec608a08f --- /dev/null +++ b/docs/framework/solid/guide/grouping.md @@ -0,0 +1,228 @@ +--- +title: Grouping (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Grouping](../examples/grouping) + +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnGroupingFeature, createGroupedRowModel, aggregationFns } from '@tanstack/solid-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = createTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Grouping (Solid) Guide + +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 left, center (unpinned), and right pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +To use the grouping feature, add the `columnGroupingFeature` to your features and the `groupedRowModel` to your row models. The grouped row model is responsible for grouping the rows based on the grouping state. + +```tsx +import { + createTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, + aggregationFns, +} from '@tanstack/solid-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = createTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```tsx +const features = tableFeatures({ columnGroupingFeature, rowExpandingFeature }) + +const table = createTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```tsx +table.setGrouping(['column1', 'column2']); +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```tsx +table.resetGrouping(); +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```tsx +const table = createTable({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows by columns using the aggregationFn option. This is a string that is the ID of the aggregation function. You can define the aggregation functions using the aggregationFns option. + +```tsx +const column = columnHelper.accessor('key', { + aggregationFn: 'sum', +}) +``` + +In the above example, the sum aggregation function will be used to aggregate the data in the grouped rows. +By default, numeric columns will use the sum aggregation function, and non-numeric columns will use the count aggregation function. You can override this behavior by specifying the aggregationFn option in the column definition. + +There are several built-in aggregation functions that you can use: + +- sum - Sums the values in the grouped rows. +- count - Counts the number of rows in the grouped rows. +- min - Finds the minimum value in the grouped rows. +- max - Finds the maximum value in the grouped rows. +- extent - Finds the extent (min and max) of the values in the grouped rows. +- mean - Finds the mean of the values in the grouped rows. +- median - Finds the median of the values in the grouped rows. +- unique - Returns an array of unique values in the grouped rows. +- uniqueCount - Counts the number of unique values in the grouped rows. + +#### Custom Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows using the aggregationFns option. This is a record where the keys are the IDs of the aggregation functions, and the values are the aggregation functions themselves. You can then reference these aggregation functions in a column's aggregationFn option. + +```tsx +const table = createTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel({ + ...aggregationFns, + myCustomAggregation: (columnId, leafRows, childRows) => { + // return the aggregated value + }, + }), + }, + // other options... +}) +``` + +In the above example, myCustomAggregation is a custom aggregation function that takes the column ID, the leaf rows, and the child rows, and returns the aggregated value. You can then use this aggregation function in a column's aggregationFn option: + +```tsx +const column = columnHelper.accessor('key', { + aggregationFn: 'myCustomAggregation', +}) +``` + +### Manual Grouping + +If you are doing server-side grouping and aggregation, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to manually group the rows before passing them to the table. + +```tsx +const table = createTable({ + features: tableFeatures({ columnGroupingFeature }), + rowModels: {}, // no groupedRowModel needed for manual grouping + // other options... + manualGrouping: true, +}) +``` + +> **Note:** There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Grouping Change Handler + +If you want to manage the grouping state yourself, you can use the onGroupingChange option. This option is a function that is called when the grouping state changes. You can pass the managed state back to the table via the tableOptions.state.grouping option. + +```tsx +const [grouping, setGrouping] = createSignal([]) + +const table = createTable({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + state: { + grouping, + }, + onGroupingChange: setGrouping, +}) +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```tsx +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +column.getAutoAggregationFn() +column.getAggregationFn() +``` + +Rows expose grouping helpers for grouped row rendering: + +```tsx +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose helpers for choosing between grouped, aggregated, placeholder, and normal cell rendering: + +```tsx +cell.getIsGrouped() +cell.getIsAggregated() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```tsx +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/solid/guide/pagination.md b/docs/framework/solid/guide/pagination.md new file mode 100644 index 0000000000..86eda63e60 --- /dev/null +++ b/docs/framework/solid/guide/pagination.md @@ -0,0 +1,260 @@ +--- +title: Pagination (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Pagination](../examples/pagination) + +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/solid-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + get data() { + return data + }, +}) +``` + +## Pagination (Solid) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain ***ALL*** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. + +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. + +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 pagination and then switch to server-side pagination in the future as your data grows. + +#### Should You Use Virtualization Instead? + +Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` to your features and the `paginatedRowModel` to your row models: + +```tsx +import { + createTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/solid-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. + +```tsx +import { + createTable, + tableFeatures, + rowPaginationFeature, +} from '@tanstack/solid-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = createTable({ + features, + rowModels: {}, // no paginatedRowModel needed for server-side pagination + columns, + data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +You can manage the `pagination` state just like any other state in the table instance. + +```tsx +import { + createTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/solid-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const [pagination, setPagination] = createSignal({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + onPaginationChange: setPagination, + state: { + pagination, + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```tsx +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT pass the `pagination` state to both the `state` and `initialState` options. `state` will overwrite `initialState`. Only use one or the other. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` when page-altering state changes occur, such as when the `data` is updated, filters change, grouping changes, etc. This behavior is automatically disabled when `manualPagination` is true but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. + +```tsx +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + autoResetPageIndex: false, // turn off auto reset of pageIndex +}) +``` + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> **Note**: These pagination APIs are available when using `rowPaginationFeature`. + +```tsx + + + + + +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/solid/guide/row-pinning.md b/docs/framework/solid/guide/row-pinning.md new file mode 100644 index 0000000000..439ca0e6ae --- /dev/null +++ b/docs/framework/solid/guide/row-pinning.md @@ -0,0 +1,223 @@ +--- +title: Row Pinning (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Row Pinning](../examples/row-pinning) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, rowPinningFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Row Pinning (Solid) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory, so `rowModels` can stay empty unless your table uses other row-model features. + +```tsx +import { + rowPinningFeature, + tableFeatures, + createTable, +} from '@tanstack/solid-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```tsx +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```tsx +const table = createTable({ + features, + rowModels: {}, + columns, + data, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, use `state.rowPinning` with `onRowPinningChange`. + +```tsx +const [rowPinning, setRowPinning] = createSignal({ + top: [], + bottom: [], +}) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, + state: { + rowPinning, + }, + onRowPinningChange: setRowPinning, +}) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```tsx +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +}) + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```tsx +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls: + +```tsx +const columns = [ + { + id: 'pin', + header: 'Pin', + cell: ({ row }) => + row.getCanPin() ? ( +
+ + + +
+ ) : null, + }, + //... +] +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```tsx +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly: + +```tsx +
+ + {row => } + + + {row => } + + + {row => } + + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```tsx +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```tsx +const table = createTable({ + features, + rowModels: {}, + columns, + data, + enableRowPinning: row => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```tsx +const table = createTable({ + features, + rowModels: {}, + columns, + data, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/solid/guide/row-selection.md b/docs/framework/solid/guide/row-selection.md new file mode 100644 index 0000000000..714dea5fc7 --- /dev/null +++ b/docs/framework/solid/guide/row-selection.md @@ -0,0 +1,201 @@ +--- +title: Row Selection (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Row Selection](../examples/row-selection) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, rowSelectionFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Row Selection (Solid) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you (though as seen down below, it may be more convenient to manage the row selection state in your own scope). You can access the internal row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the current row selection state +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +> Note: If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +Even though the table instance will already manage the row selection state for you, it is usually more convenient to manage the state yourself in order to have easy access to the selected row ids that you can use to make API calls or other actions. + +Use the `onRowSelectionChange` table option to hoist up the row selection state to your own scope. Then pass the row selection state back to the table instance using in the `state` table option. + +```ts +import { createTable, tableFeatures, rowSelectionFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const [rowSelection, setRowSelection] = createSignal({}) + +const table = createTable({ + features, + rowModels: {}, + //... + onRowSelectionChange: setRowSelection, + state: { + rowSelection, + }, +}) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +const table = createTable({ + features, + rowModels: {}, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +const table = createTable({ + //... + enableRowSelection: row => row.original.age > 18, //only enable row selection for adults +}) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +const table = createTable({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +}) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +const table = createTable({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +}) +``` + +### Render Row Selection UI + +TanStack table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These function automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +```tsx +const columns = [ + { + id: 'select-col', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + //... more column definitions... +] +``` + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. + +```tsx + + + {row => ( + + {cell => } + + )} + + +``` diff --git a/docs/framework/solid/guide/sorting.md b/docs/framework/solid/guide/sorting.md new file mode 100644 index 0000000000..7d5785499b --- /dev/null +++ b/docs/framework/solid/guide/sorting.md @@ -0,0 +1,466 @@ +--- +title: Sorting (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Sorting](../examples/sorting) + +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/solid-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = createTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Sorting (Solid) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```tsx +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +You can access the sorting state directly from the table instance with `table.atoms.sorting.get()` or from the current `table.store.state` snapshot. + +```tsx +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.sorting.get()) // access the current sorting state +``` + +However, if you need to access the sorting state before the table is initialized, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state, you can control/manage the sorting state in your own state management with the `state.sorting` and `onSortingChange` table options. + +```tsx +const [sorting, setSorting] = createSignal([]) // can set initial sorting state here +//... +// use sorting state to fetch data from your server or something... +//... +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + state: { + sorting, + }, + onSortingChange: setSorting, +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```tsx +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. + +```tsx +const [sorting, setSorting] = createSignal([]) +//... +const table = createTable({ + features: tableFeatures({ rowSortingFeature }), // feature needed for sorting state/APIs + rowModels: {}, // no sortedRowModel needed for manual sorting + columns, + data, + manualSorting: true, // use pre-sorted row model instead of sorted row model + state: { + sorting, + }, + onSortingChange: setSorting, +}) +``` + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` to your features and the `sortedRowModel` to your row models. Import `createSortedRowModel` and `sortFns` from TanStack Table: + +```tsx +import { + createTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFns, +} from '@tanstack/solid-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = createTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions either as the `sortFn` column option, or as a global sorting function using the `sortFns` table option. + +#### Custom Sorting Functions + +When defining a custom sorting function in either the `sortFns` table option or as a `sortFn` column option, it should have the following signature: + +```tsx +//optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = (rowA: Row, rowB: Row, columnId: string) => { + return //-1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> Note: The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```tsx +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // use custom global sorting function + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + } +] +//... +const table = createTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel({ + ...sortFns, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }), + }, + columns, + data, +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```tsx +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```tsx +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, //sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, //sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + sortDescFirst: true, //sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> **NOTE**: You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```tsx +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +In not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> NOTE: `'first'` and `'last'` options are available in v9. + +```tsx +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this: + +`'none' -> 'desc' -> 'asc' -> 'none' -> 'desc' -> 'asc' -> ...` + +If you disable sorting removal, the behavior will be like this: + +`'none' -> 'desc' -> 'asc' -> 'desc' -> 'asc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```tsx +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (always none -> asc -> desc -> asc) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```tsx +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```tsx +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```tsx +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```tsx +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/solid/guide/virtualization.md b/docs/framework/solid/guide/virtualization.md new file mode 100644 index 0000000000..a33d4f30a7 --- /dev/null +++ b/docs/framework/solid/guide/virtualization.md @@ -0,0 +1,239 @@ +--- +title: Virtualization (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Virtualized Columns](../examples/virtualized-columns) +- [Virtualized Rows](../examples/virtualized-rows) +- [Virtualized Infinite Scrolling](../examples/virtualized-infinite-scrolling) + +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +Install and import the Solid virtualizer adapter from `@tanstack/solid-virtual`. TanStack Table still owns rows, columns, and table state; the virtualizer owns scroll indexes and measurements. +Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/react/examples/table). + +## Virtualization (Solid) Guide + +The TanStack Table packages do not come with any virtualization APIs or features built in. Virtualization is a rendering strategy, not a table feature. You can use TanStack Table with any virtualization library, but the official examples use TanStack Virtual. + +TanStack Table and TanStack Virtual solve different parts of the problem: + +- TanStack Table builds the row models, columns, headers, cells, sizing, sorting, filtering, and other table state. +- TanStack Virtual decides which item indexes should be rendered for the current scroll position. +- Your table renderer maps those virtual indexes back to rows, headers, and cells. + +### When To Use Virtualization + +Use virtualization when your table has a very large number of rows, columns, or both. Virtualization keeps the DOM small by only rendering the items that are visible in the scroll viewport plus a small overscan buffer. + +Virtualization is not a replacement for server-side pagination, filtering, or sorting. If the data is virtualized on the client, the data still needs to exist on the client. If your dataset is too large to load into the browser, use server-side data operations or infinite scrolling. + +For small tables, normal rendering is simpler and usually preferable. + +### Install TanStack Virtual + +Install the Solid virtualizer adapter: + +```sh +npm install @tanstack/solid-virtual +``` + +The Solid examples use `createVirtualizer` from `@tanstack/solid-virtual`. TanStack Table still owns rows, columns, headers, cells, sizing, sorting, filtering, and other table state; TanStack Virtual decides which item indexes should render for the current scroll position. +### The Basic Pattern + +Most virtualized table implementations follow the same pattern: + +1. Create a fixed-height scroll container. +2. Pass the scroll element to the virtualizer. +3. Use `table.getRowModel().rows` or `table.getVisibleLeafColumns()` as the source list. +4. Configure `count`, `estimateSize`, `overscan`, and optional `measureElement`. +5. Render only virtual items. +6. Use virtual offsets or spacer padding to preserve the full scroll geometry. + +Here is a compact row virtualization example: + +```tsx +const rows = table.getRowModel().rows + +const rowVirtualizer = createVirtualizer({ + count: rows.length, + getScrollElement: () => tableContainerRef, + estimateSize: () => 33, + overscan: 5, +}) + + + + {(virtualRow) => { + const row = rows[virtualRow.index] + return ( + + + {(cell) => } + + + ) + }} + + +``` + +### Virtualized Rows + +The [virtualized rows examples](../examples/virtualized-rows) show how to render large row counts while keeping the DOM small. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The core idea is that sorting, filtering, grouping, and other row-model work still comes from TanStack Table. The virtualizer reads from the final table row model: + +```tsx +const rows = table.getRowModel().rows +``` + +The row virtualizer is configured with `count: rows.length`, a row height estimate, the scroll container, and an overscan value. The `tbody` is given the full virtual height with `rowVirtualizer.getTotalSize()`, while each rendered row is absolutely positioned with `transform: translateY(...)`. + +The examples render cells from the current row with APIs like `row.getVisibleCells()` or `row.getAllCells()`, depending on whether the example needs visibility-aware cells or all cells. + +The official examples use large generated datasets, commonly tens or hundreds of thousands of rows. They also support dynamic row heights by using `measureElement` when possible. The examples skip dynamic row measurement in Firefox because Firefox can measure table border height differently. + +### Virtualized Columns + +The [virtualized columns examples](../examples/virtualized-columns) show how to render large row and column counts. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +Column virtualization uses the current visible column list: + +```tsx +const visibleColumns = table.getVisibleLeafColumns() +``` + +The column virtualizer is configured for horizontal virtualization: + +```tsx +const columnVirtualizer = createVirtualizer({ + count: visibleColumns.length, + estimateSize: index => visibleColumns[index].getSize(), + getScrollElement: () => tableContainerRef.current, + horizontal: true, + overscan: 3, +}) +``` + +Column virtualization uses a different rendering strategy than row virtualization. Instead of absolutely positioning columns, the examples add fake spacer cells to the left and right: + +```tsx +const virtualColumns = columnVirtualizer.getVirtualItems() +const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 +const virtualPaddingRight = + columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) +``` + +Those spacer cells preserve the horizontal scroll width while the renderer only mounts the virtual columns. This approach keeps row rendering table-like and allows dynamic row height measurement to keep working. + +### Virtualized Rows And Columns Together + +The official virtualized columns examples also virtualize rows. In those examples: + +- The row virtualizer controls vertical positioning and total body height. +- The column virtualizer controls horizontal header/cell rendering and left/right spacer cells. +- `virtualRow.index` maps to `rows[virtualRow.index]`. +- `virtualColumn.index` maps to `visibleCells[virtualColumn.index]`. + +Always use virtual indexes against the same current row and column lists returned by the table. If sorting, filtering, pagination, grouping, or column visibility changes, recompute the virtualized rows and columns from the current table state. + +### Virtualized Infinite Scrolling + +The [virtualized infinite scrolling examples](../examples/virtualized-infinite-scrolling) combine row virtualization with progressive data fetching. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The common pattern is: + +1. Fetch a page of rows. +2. Flatten fetched pages into the table `data`. +3. Use row virtualization over the loaded rows. +4. Listen to scroll events on the table container. +5. Fetch the next page when the user scrolls near the bottom. + +The Solid infinite scrolling pattern can use TanStack Query or any other data-fetching layer. + +```tsx +const { scrollHeight, scrollTop, clientHeight } = scrollElement + +if (scrollHeight - scrollTop - clientHeight < 500) { + fetchNextPage() +} +``` + +If sorting is handled by the server, use manual sorting so the fetched data reflects the whole backend dataset rather than only the currently loaded rows. When sorting changes and the fetched dataset is replaced, scroll back to the top with `rowVirtualizer.scrollToIndex(0)`. + +### Dynamic Row Heights + +Dynamic row heights are useful when content can wrap or expand. They are also more complex than fixed-height rows. + +Use `estimateSize` as the virtualizer's initial guess: + +```tsx +estimateSize: () => 33 +``` + +Then use `measureElement` to refine the actual row height after rendering: + +```tsx + rowVirtualizer.measureElement(node)} +> +``` + +Set `data-index` on each row so the virtualizer can associate measurements with the correct item. In non-React adapters, call `measureElement` through the adapter-appropriate ref, action, directive, or controller. + +Overscan helps avoid blank regions while measurements settle. If every row has a known fixed height, skip dynamic measurement and use the fixed height estimate instead. + +### Sticky Headers And Semantic Table Markup + +The examples still use semantic table tags, but they change table layout CSS to support virtual positioning and sticky headers. + +Dynamic row virtualization commonly requires: + +```css +table { + display: grid; +} + +thead { + display: grid; + position: sticky; + top: 0; +} + +tr { + display: flex; +} +``` + +Rows are absolutely positioned inside a relatively positioned `tbody`, and cells use flex sizing so they can match `column.getSize()` or `cell.column.getSize()`. This is intentional. Native table layout does not work well with dynamic-height virtual rows that are positioned independently. + +### Performance Tips + +- Keep virtualizers near the components that render the virtualized items. +- Avoid re-rendering the full table body on every scroll. +- Keep row, column, and data references stable where possible. +- Use `overscan` deliberately. More overscan reduces visible blanking, while less overscan reduces DOM nodes. +- Avoid expensive cell renderers in very large virtualized tables. +- Test production builds. Framework development builds can be slower than production builds; profile production bundles before optimizing. +- Prefer fixed row sizes when the UI allows it. +- For column virtualization, use `column.getSize()`, `header.getSize()`, and `cell.column.getSize()` consistently. + diff --git a/docs/framework/svelte/guide/column-faceting.md b/docs/framework/svelte/guide/column-faceting.md new file mode 100644 index 0000000000..3944731e2d --- /dev/null +++ b/docs/framework/svelte/guide/column-faceting.md @@ -0,0 +1,115 @@ +--- +title: Column Faceting (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Faceted Filters](../examples/filters-faceted) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = createTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + get data() { + return data + }, +}) +``` + +## Column Faceting (Svelte) Guide + +Column Faceting is a feature that allows you to generate lists of values for a given column from that column's data. For example, a list of unique values in a column can be generated from all rows in that column to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a column of numbers to be used as a range for a range slider filter component. + +### Column Faceting Row Models + +In order to use any of the column faceting features, add the `columnFacetingFeature` to your features and the appropriate faceted row models to `rowModels`. + +```ts +import { + createTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = createTable({ + features, + rowModels: { + 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 + }, + columns, + 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]; +``` + +### Custom (Server-Side) Faceting + +If 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. You can use the `getFacetedUniqueValues` and `getFacetedMinMaxValues` table options to resolve the faceted values from the server-side. + +```ts +const facetingQuery = useQuery( + //... +) + +const table = createTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, + getFacetedUniqueValues: (table, columnId) => { + const uniqueValueMap = new Map() + //... + return uniqueValueMap + }, + getFacetedMinMaxValues: (table, columnId) => { + //... + return [min, max] + }, + //... +}) +``` + +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/svelte/guide/column-filtering.md b/docs/framework/svelte/guide/column-filtering.md new file mode 100644 index 0000000000..d4e93e0537 --- /dev/null +++ b/docs/framework/svelte/guide/column-filtering.md @@ -0,0 +1,377 @@ +--- +title: Column Filtering (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Column Filters](../examples/filtering) +- [Faceted Filters](../examples/filters-faceted) +- [Fuzzy Search](../examples/filters-fuzzy) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Column Filtering (Svelte) 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 `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +const table = createTable({ + features: tableFeatures({ columnFilteringFeature }), + rowModels: {}, // no filteredRowModel needed for manual server-side filtering + 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` to your features and the `filteredRowModel` to your row models. Import `createFilteredRowModel` and `filterFns` from TanStack Table: + +```ts +import { + createTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + 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 + +You can access the column filter state from the table instance with `table.atoms.columnFilters.get()` or from the current `table.store.state` snapshot. + +```ts +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.columnFilters.get()) // access the current column filters state +``` + +However, if you need to access the column filter state before the table is initialized, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state, you can control/manage the column filter state in your own state management with the `state.columnFilters` and `onColumnFiltersChange` table options. + +```ts +import { createTableState } from '@tanstack/svelte-table' + +const [columnFilters, setColumnFilters] = + createTableState([]) // can set initial column filter state here + +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + state: { + get columnFilters() { + return columnFilters() + }, + }, + onColumnFiltersChange: setColumnFilters, +}) +``` + +#### Initial Column Filter State + +If you do not need to control the column filter state in your own state management or scope, but you still want to set an initial column filter state, you can use the `initialState` table option instead of `state`. + +```ts +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 10 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `arrIncludes` - Item inclusion within an array +- `arrIncludesAll` - All items included in an array +- `arrIncludesSome` - Some items included in an array +- `equals` - Object/referential equality `Object.is`/`===` +- `weakEquals` - Weak object/referential equality `==` +- `inNumberRange` - Number range inclusion + +You can also define your own custom filter functions either as the `filterFn` column option, or as a global filter function using the `filterFns` table option. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +When defining a custom filter function in either the `filterFn` column option or the `filterFns` table option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = (row: Row, columnId: string, filterValue: any, addMeta: (meta: any) => void) => boolean +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // use custom global filter function + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + } +] +//... +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }), + }, + columns, + data, +}) +``` + +##### 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 = ( + row: Row, + columnId: string, + filterValue: number | string, //resolveFilterValue will transform this 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 = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + enableColumnFilters: false, // disable column filtering for all columns +}) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```ts +const table = createTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + filterFromLeafRows: true, // filter and search through sub-rows +}) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```ts +const table = createTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +}) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `onChange` or `onBlur` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/svelte/guide/column-ordering.md b/docs/framework/svelte/guide/column-ordering.md new file mode 100644 index 0000000000..4c7c10101f --- /dev/null +++ b/docs/framework/svelte/guide/column-ordering.md @@ -0,0 +1,160 @@ +--- +title: Column Ordering (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Column Ordering](../examples/column-ordering) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnOrderingFeature } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Ordering (Svelte) 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 left, center (unpinned), and right 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 table = createTable({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + 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. + +```ts +import { createTableState } from '@tanstack/svelte-table' + +const [columnOrder, setColumnOrder] = createTableState([ + 'columnId1', + 'columnId2', + 'columnId3', +]) + +const table = createTable({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + state: { + get columnOrder() { + return columnOrder() + }, + }, + onColumnOrderChange: setColumnOrder, +}) +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, you can set up the logic something like this: + +```ts +const [columnOrder, setColumnOrder] = createTableState( + columns.map(column => column.id), +) + +//depending on your dnd solution of choice, you may or may not need state like this +let movingColumnId = $state(null) +let targetColumnId = $state(null) + +//util function to splice and reorder the columnOrder array +const reorderColumn = ( + movingColumnId: string, + targetColumnId: string, +): string[] => { + const newColumnOrder = [...columnOrder()] + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + newColumnOrder.splice(newColumnOrder.indexOf(movingColumnId), 1)[0], + ) + return newColumnOrder +} + +const handleDragEnd = (e: DragEvent) => { + if (!movingColumnId || !targetColumnId) return + setColumnOrder(reorderColumn(movingColumnId, targetColumnId)) +} + +//use your dnd solution of choice +``` + +### 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('left') +column.getIndex('center') +column.getIndex('right') + +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 (Svelte) + +There are undoubtedly many ways to implement drag and drop features along-side TanStack Table. Here are a few suggestions in order for you to not have a bad time: + +1. Do NOT try to use [`"react-dnd"`](https://react-dnd.github.io/react-dnd/docs/overview) _if you are using Svelte or newer_. framework-specific DnD was an important library for its time, but it now does not get updated very often, and it has incompatibilities with Svelte, especially in Svelte development mode. It is still possible to get it to work, but there are newer alternatives that have better compatibility and are more actively maintained. framework-specific DnD's Provider may also interfere and conflict with any other DnD solutions you may want to try in your app. + +2. Use [`"@dnd-kit/core"`](https://dndkit.com/). DnD Kit is a modern, modular and lightweight drag and drop library that is highly compatible with the modern Svelte ecosystem, and it works well with semantic `
+``` + +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. + +```tsx + +``` + +#### 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. + +```tsx + +``` + +The `columnResizing` state stores transient drag information: + +```tsx +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +Use `onColumnResizingChange` with `state.columnResizing` if you need to manage this state externally. + +```tsx +const [columnResizing, setColumnResizing] = createSignal({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, + state: { + columnResizing, + }, + onColumnResizingChange: setColumnResizing, +}) +``` + +### 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. + +```tsx +header.getResizeHandler() +column.getCanResize() +column.getIsResizing() +``` + +The table instance exposes APIs for the transient resize state. The current generated v9 API spelling is `table.setcolumnResizing` with a lowercase `c` in `column`; use that exact name. + +```tsx +table.setcolumnResizing(old => ({ + ...old, + deltaOffset: 12, +})) + +table.resetHeaderSizeInfo() +table.resetHeaderSizeInfo(true) +``` + +### Advanced Column Resizing Performance + +If you are creating large or complex tables with Solid, you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns. + +We have created a [performant column resizing example](../examples/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: + +1. Don't use `column.getSize()` on every header and every data cell. Instead, calculate all column widths once upfront, **memoized**! +2. Memoize your Table Body while resizing is in progress. +3. Use CSS variables to communicate column widths to your table cells. + +If you follow these steps, you should see significant performance improvements while resizing columns. + +If you are not using React, and are using the Svelte, Vue, or Solid adapters instead, you may not need to worry about this as much, but similar principles apply. diff --git a/docs/framework/solid/guide/column-sizing.md b/docs/framework/solid/guide/column-sizing.md new file mode 100644 index 0000000000..14d2b64168 --- /dev/null +++ b/docs/framework/solid/guide/column-sizing.md @@ -0,0 +1,124 @@ +--- +title: Column Sizing (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Column Sizing](../examples/column-sizing) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnSizingFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ columnSizingFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Sizing (Solid) 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: + +```tsx +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. + +```tsx +const columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +const table = createTable({ + features: tableFeatures({ columnSizingFeature }), + rowModels: {}, + 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. + +```tsx +column.getSize() +header.getSize() + +column.getStart() // left offset in the current column flow +column.getStart('left') +column.getStart('center') +column.getStart('right') + +column.getAfter() // right offset in the current column flow +column.getAfter('left') +column.getAfter('center') +column.getAfter('right') + +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. + +```tsx +table.getTotalSize() +table.getLeftTotalSize() +table.getCenterTotalSize() +table.getRightTotalSize() +``` + +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. + +```tsx +table.setColumnSizing({ + firstName: 180, + age: 80, +}) + +table.resetColumnSizing() +table.resetColumnSizing(true) +``` diff --git a/docs/framework/solid/guide/column-visibility.md b/docs/framework/solid/guide/column-visibility.md new file mode 100644 index 0000000000..b49a24ecdd --- /dev/null +++ b/docs/framework/solid/guide/column-visibility.md @@ -0,0 +1,152 @@ +--- +title: Column Visibility (Solid) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Solid examples: + +- [Column Visibility](../examples/column-visibility) +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +### Solid Setup + +```tsx +import { createTable, tableFeatures, columnVisibilityFeature } from '@tanstack/solid-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Visibility (Solid) 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. + +```tsx +import { createTable, tableFeatures, columnVisibilityFeature } from '@tanstack/solid-table' + +const [columnVisibility, setColumnVisibility] = createSignal({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +const table = createTable({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + state: { + columnVisibility, + //... + }, + onColumnVisibilityChange: setColumnVisibility, +}) +``` + +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. + +```tsx +const table = createTable({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + 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. + +```tsx +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessor: '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. + +```tsx + + {column => ( + + )} + +``` + +### 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`. + +```tsx + + + + + {column => { + // takes column visibility into account + }} + + + + + + {row => ( + + + {cell => { + // takes column visibility into account + }} + + + )} + + +
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/solid/guide/custom-features.md b/docs/framework/solid/guide/custom-features.md new file mode 100644 index 0000000000..f6774a93e1 --- /dev/null +++ b/docs/framework/solid/guide/custom-features.md @@ -0,0 +1,315 @@ +--- +title: Custom Features (Solid) Guide +--- + +Use getters for reactive inputs such as `data` when passing Solid signals to `createTable`. + +## Custom Features (Solid) Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers where TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable`, `createTable`, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrapper hooks around the `createTable` hook to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking—you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods do: + +#### Default Options and Initial State + +
+ +##### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +##### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is responsible for adding methods to the `table` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. + +
+ +##### assignColumnPrototype + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, the [Column Grouping](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts) feature adds cell instance API methods such as `getIsGrouped` and `getIsAggregated`. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full custom-plugin example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/solid-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs options, and state in your application. + +```tsx +const features = tableFeatures({ densityPlugin }) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, + //... + state: { + density, + }, + onDensityChange: setDensity, +}) +//... +const density = table.atoms.density.get() +return( +
+ +
+ +
{/* Your custom UI goes here */}
{/* */}
` markup. The official framework-specific DnD examples, Column DnD and Row DnD, use DnD Kit. + +3. Consider other DnD libraries like [`"react-beautiful-dnd"`](https://github.com/atlassian/react-beautiful-dnd), but be aware of their potentially large bundle sizes, maintenance status, and compatibility with `
` markup. + +4. Consider using native browser events and state management to implement lightweight drag and drop features. However, be aware that this approach may not be best for mobile users if you do not go the extra mile to implement proper touch events. [Material React Table V2](https://www.material-react-table.com/docs/examples/column-ordering) is an example of a library that implements TanStack Table with only browser drag and drop events such as `onDragStart`, `onDragEnd`, `onDragEnter` and no other dependencies. Browse its source code to see how it is done. diff --git a/docs/framework/svelte/guide/column-pinning.md b/docs/framework/svelte/guide/column-pinning.md new file mode 100644 index 0000000000..7f3e30cfb0 --- /dev/null +++ b/docs/framework/svelte/guide/column-pinning.md @@ -0,0 +1,171 @@ +--- +title: Column Pinning (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnPinningFeature } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnPinningFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Pinning (Svelte) 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 left or right. + +### 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 left, center (unpinned), and right 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.left` and `columnPinning.right` 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. + +```ts +import { + createTable, + createTableState, + tableFeatures, + columnPinningFeature, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ columnPinningFeature }) + +const [columnPinning, setColumnPinning] = + createTableState({ + left: [], + right: [], +}) + +const table = createTable({ + features, + rowModels: {}, + //... + state: { + get columnPinning() { + return columnPinning() + }, + }, + onColumnPinningChange: setColumnPinning, +}) +``` + +### Pin Columns by Default + +A very common use case is to pin some columns by default. You can do this by either initializing the `columnPinning` state with the pinned columnIds, or by using the `initialState` table option + +```ts +const table = createTable({ + features, + rowModels: {}, + //... + initialState: { + columnPinning: { + left: ['expand-column'], + right: ['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 left or right. 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 `left` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `right` 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({ + left: ['firstName'], + right: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```ts +table.getLeftLeafColumns() +table.getCenterLeafColumns() +table.getRightLeafColumns() + +table.getLeftVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getRightVisibleLeafColumns() + +table.getLeftHeaderGroups() +table.getCenterHeaderGroups() +table.getRightHeaderGroups() + +table.getLeftFooterGroups() +table.getCenterFooterGroups() +table.getRightFooterGroups() + +table.getLeftFlatHeaders() +table.getCenterFlatHeaders() +table.getRightFlatHeaders() + +table.getLeftLeafHeaders() +table.getCenterLeafHeaders() +table.getRightLeafHeaders() +``` + +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('left') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('right') + +table.getPinnedVisibleLeafColumns('left') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('right') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'left'` or `'right'` to check one pinned side. + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getLeftHeaderGroups`, `table.getCenterHeaderGroups`, `table.getRightHeaderGroups`, `row.getLeftVisibleCells`, `row.getCenterVisibleCells`, and `row.getRightVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/svelte/guide/column-resizing.md b/docs/framework/svelte/guide/column-resizing.md new file mode 100644 index 0000000000..a1f9edc37d --- /dev/null +++ b/docs/framework/svelte/guide/column-resizing.md @@ -0,0 +1,223 @@ +--- +title: Column Resizing (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnResizingFeature } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnResizingFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Resizing (Svelte) 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 `columnResizingFeature` to your features. The `column.getCanResize()` API will return `true` by default for all columns, but you can either disable column resizing for all columns with the `enableColumnResizing` table option, or disable column resizing on a per-column basis with the `enableResizing` column option. + +```ts +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + createTable, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns = [ + { + accessorKey: 'id', + enableResizing: false, // disable resizing for just this column + size: 200, // starting column size + }, + //... +] + +const table = createTable({ + features, + rowModels: {}, + 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 React 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 React 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 = createTable({ + //... + columnResizeMode: 'onChange', // change column resize mode to "onChange" +}) +``` + +### Column Resize Direction + +By default, TanStack Table assumes that the table markup is laid out in a left-to-right direction. For right-to-left layouts, you may need to change the column resize direction to `"rtl"`. + +```ts +const table = createTable({ + //... + columnResizeDirection: 'rtl', // change column resize direction to "rtl" for certain locales +}) +``` + +### Connect Column Resizing APIs to UI + +There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI. + +#### Column Size APIs + +To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs: + +```ts +header.getSize() +column.getSize() +cell.column.getSize() +``` + +How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes. + +```svelte + +``` + +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. + +```svelte +
+``` + +#### 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. + +```svelte +
+``` + +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 +} +``` + +Use `onColumnResizingChange` with `state.columnResizing` if you need to manage this state externally. + +```ts +import { createTableState } from '@tanstack/svelte-table' + +const [columnResizing, setColumnResizing] = + createTableState({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, + state: { + get columnResizing() { + return columnResizing() + }, + }, + onColumnResizingChange: setColumnResizing, +}) +``` + +### 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. The current generated v9 API spelling is `table.setcolumnResizing` with a lowercase `c` in `column`; use that exact name. + +```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 Svelte, you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns. + +We have created a [performant column resizing example](../examples/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: + +1. Don't use `column.getSize()` on every header and every data cell. Instead, calculate all column widths once upfront, **memoized**! +2. Memoize your Table Body while resizing is in progress. +3. Use CSS variables to communicate column widths to your table cells. + +If you follow these steps, you should see significant performance improvements while resizing columns. + +If you are not using React, and are using the Svelte, Vue, or Solid adapters instead, you may not need to worry about this as much, but similar principles apply. diff --git a/docs/framework/svelte/guide/column-sizing.md b/docs/framework/svelte/guide/column-sizing.md new file mode 100644 index 0000000000..83c480aceb --- /dev/null +++ b/docs/framework/svelte/guide/column-sizing.md @@ -0,0 +1,124 @@ +--- +title: Column Sizing (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Column Sizing](../examples/column-sizing) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnSizingFeature } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnSizingFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Sizing (Svelte) 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 columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +const table = createTable({ + features: tableFeatures({ columnSizingFeature }), + rowModels: {}, + 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() // left offset in the current column flow +column.getStart('left') +column.getStart('center') +column.getStart('right') + +column.getAfter() // right offset in the current column flow +column.getAfter('left') +column.getAfter('center') +column.getAfter('right') + +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.getLeftTotalSize() +table.getCenterTotalSize() +table.getRightTotalSize() +``` + +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) +``` diff --git a/docs/framework/svelte/guide/column-visibility.md b/docs/framework/svelte/guide/column-visibility.md new file mode 100644 index 0000000000..38cd7e1bbc --- /dev/null +++ b/docs/framework/svelte/guide/column-visibility.md @@ -0,0 +1,151 @@ +--- +title: Column Visibility (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Column Visibility](../examples/column-visibility) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnVisibilityFeature } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Column Visibility (Svelte) 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. + +```ts +import { + createTable, + createTableState, + tableFeatures, + columnVisibilityFeature, +} from '@tanstack/svelte-table' + +const [columnVisibility, setColumnVisibility] = + createTableState({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +const table = createTable({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + state: { + get columnVisibility() { + return columnVisibility() + }, + }, + onColumnVisibilityChange: setColumnVisibility, +}) +``` + +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 table = createTable({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +}) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```ts +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessor: '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. + +```svelte +{#each table.getAllColumns() as column (column.id)} + +{/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`. + +```svelte +
+ +
+ + + {#each table.getVisibleLeafColumns() as column (column.id)} + + {/each} + + + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + {/each} + + {/each} + +
{column.id}
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/svelte/guide/custom-features.md b/docs/framework/svelte/guide/custom-features.md new file mode 100644 index 0000000000..2a4902f475 --- /dev/null +++ b/docs/framework/svelte/guide/custom-features.md @@ -0,0 +1,290 @@ +--- +title: Custom Features (Svelte) Guide +--- + +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +## Custom Features (Svelte) Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers where TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable`, `createTable`, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrapper hooks around the `createTable` hook to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking—you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods do: + +#### Default Options and Initial State + +
+ +##### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +##### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is responsible for adding methods to the `table` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. + +
+ +##### assignColumnPrototype + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, the [Column Grouping](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts) feature adds cell instance API methods such as `getIsGrouped` and `getIsAggregated`. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full custom-plugin example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/svelte-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs options, and state in your application. + +```svelte +{@const density = table.state.density} + + + +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `$state`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/svelte/guide/expanding.md b/docs/framework/svelte/guide/expanding.md new file mode 100644 index 0000000000..a83b883122 --- /dev/null +++ b/docs/framework/svelte/guide/expanding.md @@ -0,0 +1,281 @@ +--- +title: Expanding (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Expanding](../examples/expanding) + +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, rowExpandingFeature, createExpandedRowModel } from '@tanstack/svelte-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = createTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + columns, + get data() { + return data + }, +}) +``` + +## Expanding Feature (Svelte) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` to your features and the `expandedRowModel` to your row models: + +```ts +import { + createTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = createTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are essentially child rows that inherit the same column structure as their parent rows. If your data object already includes these expanded rows data, you can utilize the `getSubRows` function to specify these child rows. However, if your data object does not contain the expanded rows data, they can be treated as custom expanded data, which is discussed in next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 } + ] + }, + { id: 3, + name: 'Doe', + age: 40, + children: [ + { id: 4, name: 'Alice', age: 10 } + ] + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +const table = createTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +}) +``` + +> **Note:** You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +```svelte + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + {/each} + + {#if row.getIsExpanded()} + + + + + + {/if} + {/each} + +``` + +### Expanded rows state + +If you need to control the expanded state of the rows in your table, you can do so by using the expanded state and the `onExpandedChange` option. This allows you to manage the expanded state according to your requirements. + +```ts +import { createTableState } from '@tanstack/svelte-table' + +const [expanded, setExpanded] = createTableState({}) + +const table = createTable({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + state: { + get expanded() { + return expanded() + }, + }, + onExpandedChange: setExpanded, +}) +``` + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows with IDs present as keys in the record and have their value set to true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. For example, you can add a button UI within the columns definition. + +```ts +const columns = [ + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'age', + header: 'Age', + }, + { + header: 'Children', + cell: ({ row }) => row.getCanExpand(), + }, +] +``` + +```svelte +{#if row.getCanExpand()} + +{/if} +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```ts +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```ts +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, the filtering process starts from the parent rows and moves downwards. This means if a parent row is excluded by the filter, all its child rows will also be excluded. However, you can change this behavior by using the `filterFromLeafRows` option. When this option is enabled, the filtering process starts from the leaf (child) rows and moves upwards. This ensures that a parent row will be included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. Additionally, you can control how deep into the child hierarchy the filter process goes by using the `maxLeafRowFilterDepth` option. This option allows you to specify the maximum depth of child rows that the filter should consider. + +```ts +//... +const table = createTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parents page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +const table = createTable({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Pinning Guide](../../../guide/pinning.md) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const table = createTable({ + features: tableFeatures({ rowExpandingFeature }), + rowModels: {}, // no expandedRowModel needed for manual expanding + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/svelte/guide/fuzzy-filtering.md b/docs/framework/svelte/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..ee444ec339 --- /dev/null +++ b/docs/framework/svelte/guide/fuzzy-filtering.md @@ -0,0 +1,155 @@ +--- +title: Fuzzy Filtering (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Fuzzy Search](../examples/filters-fuzzy) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Fuzzy Filtering (Svelte) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement a client side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> **Note:** You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked in order to work better with TanStack Table's row by row filtering approach. + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +Here's an example of a custom fuzzy filter function: + +```typescript +import { rankItem } from '@tanstack/match-sorter-utils'; +import { FilterFn } from '@tanstack/svelte-table' + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the rankItem function from the @tanstack/match-sorter-utils library to rank the item. We then store the ranking information in the meta data of the row, and return whether the item passed the ranking criteria. + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, you can specify the fuzzy filter function in the globalFilterFn option of the table instance: + +```typescript +import { + createTable, + tableFeatures, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + filterFns, + sortFns, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ globalFilteringFeature, rowSortingFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + fuzzy: fuzzyFilter, + }), + sortedRowModel: createSortedRowModel(sortFns), // needed if you want sorting with fuzzy rank + }, + columns, + data, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, pass your fuzzy filter function to `createFilteredRowModel` (merging it with the built-in `filterFns`). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```typescript +const column = [ + { + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +]; +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```typescript +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFns } from '@tanstack/svelte-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId]?.itemRank!, + rowB.columnFiltersMeta[columnId]?.itemRank! + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then specify this sorting function in the sortFn option of the column definition: + +```typescript +{ + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + sortFn: 'fuzzySort', //using our custom fuzzy sort function +} +``` diff --git a/docs/framework/svelte/guide/global-faceting.md b/docs/framework/svelte/guide/global-faceting.md new file mode 100644 index 0000000000..6d3a62458e --- /dev/null +++ b/docs/framework/svelte/guide/global-faceting.md @@ -0,0 +1,111 @@ +--- +title: Global Faceting (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Faceted Filters](../examples/filters-faceted) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = createTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + get data() { + return data + }, +}) +``` + +## Global Faceting (Svelte) Guide + +Global Faceting allows you to generate lists of values for all columns from the table's data. For example, a list of unique values in a table can be generated from all rows in all columns to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a table of numbers to be used as a range for a range slider filter component. + +### Global Faceting Row Models + +In order to use any of the global faceting features, add the appropriate faceted row models to your `rowModels`: + +```ts +import { + createTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ columnFacetingFeature }) +// If the table also uses global filtering, include globalFilteringFeature too. + +const table = createTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), // required (other faceting methods depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + }, + // other options... +}) +``` + +### Use Global Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting table instance APIs to access the lists of values generated by the faceted row models. + +Use `table.getGlobalFacetedRowModel()` if you need to inspect the row model used to derive global facet 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 Global (Server-Side) Faceting + +If 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 `rowModels.facetedUniqueValues` and `rowModels.facetedMinMaxValues` factories. Global faceting requests use the internal `__global__` column ID. + +```ts +const globalFacets = await fetch('/api/faceting').then(res => res.json()) + +const table = createTable({ + features, + rowModels: { + facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() + return new Map(globalFacets.uniqueValues) + }, + facetedMinMaxValues: (_table, columnId) => () => { + if (columnId !== '__global__') return undefined + return globalFacets.minMaxValues + }, + }, + // other options... +}) +``` + +In this example, the custom factories return server-provided global facet values when TanStack Table asks for the `__global__` faceting context. Column-specific faceting can still use separate values by checking for each column ID. diff --git a/docs/framework/svelte/guide/global-filtering.md b/docs/framework/svelte/guide/global-filtering.md new file mode 100644 index 0000000000..b3257074ec --- /dev/null +++ b/docs/framework/svelte/guide/global-filtering.md @@ -0,0 +1,247 @@ +--- +title: Global Filtering (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Column Filters](../examples/filtering) +- [Fuzzy Search](../examples/filters-fuzzy) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, globalFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/svelte-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Global Filtering (Svelte) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +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 Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +import { + createTable, + tableFeatures, + globalFilteringFeature, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = createTable({ + features, + rowModels: {}, // no filteredRowModel needed for manual server-side global filtering + data, + columns, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` to your features and the `filteredRowModel` to your row models: + +```ts +import { + createTable, + tableFeatures, + globalFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + // other options... +}) +``` + +### Global Filter Function + +The globalFilterFn option allows you to specify the filter function that will be used for global filtering. The filter function can be a string that references a built-in filter function, a string that references a custom filter function provided via the tableOptions.filterFns option, or a custom filter function. + +```ts +const table = createTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + data, + columns, + globalFilterFn: 'text', // built-in filter function +}) +``` + +By default there are 10 built-in filter functions to choose from: + +- includesString - Case-insensitive string inclusion +- includesStringSensitive - Case-sensitive string inclusion +- equalsString - Case-insensitive string equality +- equalsStringSensitive - Case-sensitive string equality +- arrIncludes - Item inclusion within an array +- arrIncludesAll - All items included in an array +- arrIncludesSome - Some items included in an array +- equals - Object/referential equality Object.is/=== +- weakEquals - Weak object/referential equality == +- inNumberRange - Number range inclusion + +You can also define your own custom filter functions either as the globalFilterFn table option. + +### Global Filter State + +The global filter state is stored in the table's state atoms and can be read with `table.atoms.globalFilter.get()` or from the current `table.store.state.globalFilter` snapshot. If you want to persist the global filter state outside of the table, use the `state.globalFilter` and `onGlobalFilterChange` options together. + +```ts +import { createTableState } from '@tanstack/svelte-table' + +const [globalFilter, setGlobalFilter] = createTableState('') + +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + state: { + get globalFilter() { + return globalFilter() + }, + }, + onGlobalFilterChange: setGlobalFilter, +}) +``` + +The global filtering state is defined as an object with the following shape: + +```ts +interface GlobalFilter { + globalFilter: any +} +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. + +```svelte + table.setGlobalFilter((e.target as HTMLInputElement).value)} + placeholder="Search all columns..." +/> +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the globalFilterFn option. + +> **Note:** It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering.md). + +```ts +const customFilterFn = (rows, columnId, filterValue) => { + // custom filter logic +} + +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table initialState option. + +However, you can also just specify the initial global filter state in the state.globalFilter option. + +```ts +const [globalFilter, setGlobalFilter] = createTableState('search term') // recommended to initialize globalFilter state here + +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + initialState: { + globalFilter: 'search term', // if not managing globalFilter state, set initial state here + }, + state: { + get globalFilter() { + return globalFilter() + }, // pass our managed globalFilter state to the table + }, + onGlobalFilterChange: setGlobalFilter, +}) +``` + +> NOTE: Do not use both initialState.globalFilter and state.globalFilter at the same time, as the initialized state in the state.globalFilter will override the initialState.globalFilter. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` diff --git a/docs/framework/svelte/guide/grouping.md b/docs/framework/svelte/guide/grouping.md new file mode 100644 index 0000000000..8d869c371c --- /dev/null +++ b/docs/framework/svelte/guide/grouping.md @@ -0,0 +1,232 @@ +--- +title: Grouping (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Grouping](../examples/grouping) + +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, columnGroupingFeature, createGroupedRowModel, aggregationFns } from '@tanstack/svelte-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = createTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Grouping (Svelte) Guide + +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 left, center (unpinned), and right pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +To use the grouping feature, add the `columnGroupingFeature` to your features and the `groupedRowModel` to your row models. The grouped row model is responsible for grouping the rows based on the grouping state. + +```ts +import { + createTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, + aggregationFns, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = createTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```ts +const features = tableFeatures({ columnGroupingFeature, rowExpandingFeature }) + +const table = createTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```ts +table.setGrouping(['column1', 'column2']); +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```ts +table.resetGrouping(); +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```ts +const table = createTable({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows by columns using the aggregationFn option. This is a string that is the ID of the aggregation function. You can define the aggregation functions using the aggregationFns option. + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'sum', +}) +``` + +In the above example, the sum aggregation function will be used to aggregate the data in the grouped rows. +By default, numeric columns will use the sum aggregation function, and non-numeric columns will use the count aggregation function. You can override this behavior by specifying the aggregationFn option in the column definition. + +There are several built-in aggregation functions that you can use: + +- sum - Sums the values in the grouped rows. +- count - Counts the number of rows in the grouped rows. +- min - Finds the minimum value in the grouped rows. +- max - Finds the maximum value in the grouped rows. +- extent - Finds the extent (min and max) of the values in the grouped rows. +- mean - Finds the mean of the values in the grouped rows. +- median - Finds the median of the values in the grouped rows. +- unique - Returns an array of unique values in the grouped rows. +- uniqueCount - Counts the number of unique values in the grouped rows. + +#### Custom Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows using the aggregationFns option. This is a record where the keys are the IDs of the aggregation functions, and the values are the aggregation functions themselves. You can then reference these aggregation functions in a column's aggregationFn option. + +```ts +const table = createTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel({ + ...aggregationFns, + myCustomAggregation: (columnId, leafRows, childRows) => { + // return the aggregated value + }, + }), + }, + // other options... +}) +``` + +In the above example, myCustomAggregation is a custom aggregation function that takes the column ID, the leaf rows, and the child rows, and returns the aggregated value. You can then use this aggregation function in a column's aggregationFn option: + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'myCustomAggregation', +}) +``` + +### Manual Grouping + +If you are doing server-side grouping and aggregation, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to manually group the rows before passing them to the table. + +```ts +const table = createTable({ + features: tableFeatures({ columnGroupingFeature }), + rowModels: {}, // no groupedRowModel needed for manual grouping + // other options... + manualGrouping: true, +}) +``` + +> **Note:** There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Grouping Change Handler + +If you want to manage the grouping state yourself, you can use the onGroupingChange option. This option is a function that is called when the grouping state changes. You can pass the managed state back to the table via the tableOptions.state.grouping option. + +```ts +import { createTableState } from '@tanstack/svelte-table' + +const [grouping, setGrouping] = createTableState([]) + +const table = createTable({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + state: { + get grouping() { + return grouping() + }, + }, + onGroupingChange: setGrouping, +}) +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```ts +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +column.getAutoAggregationFn() +column.getAggregationFn() +``` + +Rows expose grouping helpers for grouped row rendering: + +```ts +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose helpers for choosing between grouped, aggregated, placeholder, and normal cell rendering: + +```ts +cell.getIsGrouped() +cell.getIsAggregated() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```ts +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/svelte/guide/pagination.md b/docs/framework/svelte/guide/pagination.md new file mode 100644 index 0000000000..0dcc215fe1 --- /dev/null +++ b/docs/framework/svelte/guide/pagination.md @@ -0,0 +1,241 @@ +--- +title: Pagination (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Pagination](../examples/pagination) + +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/svelte-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + get data() { + return data + }, +}) +``` + +## Pagination (Svelte) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain ***ALL*** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. + +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. + +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 pagination and then switch to server-side pagination in the future as your data grows. + +#### Should You Use Virtualization Instead? + +Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` to your features and the `paginatedRowModel` to your row models: + +```ts +import { + createTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. + +```ts +import { + createTable, + tableFeatures, + rowPaginationFeature, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = createTable({ + features, + rowModels: {}, // no paginatedRowModel needed for server-side pagination + columns, + data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +You can manage the `pagination` state just like any other state in the table instance. + +```ts +import { + createTable, + createTableState, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const [pagination, setPagination] = createTableState({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + onPaginationChange: setPagination, + state: { + get pagination() { + return pagination() + }, + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```ts +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT pass the `pagination` state to both the `state` and `initialState` options. `state` will overwrite `initialState`. Only use one or the other. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` when page-altering state changes occur, such as when the `data` is updated, filters change, grouping changes, etc. This behavior is automatically disabled when `manualPagination` is true but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. + +```ts +const table = createTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + autoResetPageIndex: false, // turn off auto reset of pageIndex +}) +``` + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> **Note**: These pagination APIs are available when using `rowPaginationFeature`. + +```svelte + + + + + +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/svelte/guide/row-pinning.md b/docs/framework/svelte/guide/row-pinning.md new file mode 100644 index 0000000000..b90cbc84ea --- /dev/null +++ b/docs/framework/svelte/guide/row-pinning.md @@ -0,0 +1,213 @@ +--- +title: Row Pinning (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Row Pinning](../examples/row-pinning) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, rowPinningFeature } from '@tanstack/svelte-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Row Pinning (Svelte) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory, so `rowModels` can stay empty unless your table uses other row-model features. + +```ts +import { + rowPinningFeature, + tableFeatures, + createTable, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```ts +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```ts +const table = createTable({ + features, + rowModels: {}, + columns, + data, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, use `state.rowPinning` with `onRowPinningChange`. + +```ts +import { createTableState } from '@tanstack/svelte-table' + +const [rowPinning, setRowPinning] = createTableState({ + top: [], + bottom: [], +}) + +const table = createTable({ + features, + rowModels: {}, + columns, + data, + state: { + get rowPinning() { + return rowPinning() + }, + }, + onRowPinningChange: setRowPinning, +}) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```ts +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +}) + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```ts +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls: + +```svelte +{#if row.getCanPin()} +
+ + + +
+{/if} +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```ts +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly: + +```svelte + + {#each table.getTopRows() as row (row.id)} + + {/each} + {#each table.getCenterRows() as row (row.id)} + + {/each} + {#each table.getBottomRows() as row (row.id)} + + {/each} + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```ts +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```ts +const table = createTable({ + features, + rowModels: {}, + columns, + data, + enableRowPinning: row => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```ts +const table = createTable({ + features, + rowModels: {}, + columns, + data, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/svelte/guide/row-selection.md b/docs/framework/svelte/guide/row-selection.md new file mode 100644 index 0000000000..74cd24c625 --- /dev/null +++ b/docs/framework/svelte/guide/row-selection.md @@ -0,0 +1,202 @@ +--- +title: Row Selection (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Row Selection](../examples/row-selection) +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, rowSelectionFeature } from '@tanstack/svelte-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, +}) +``` + +## Row Selection (Svelte) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you (though as seen down below, it may be more convenient to manage the row selection state in your own scope). You can access the internal row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the current row selection state +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +> Note: If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +Even though the table instance will already manage the row selection state for you, it is usually more convenient to manage the state yourself in order to have easy access to the selected row ids that you can use to make API calls or other actions. + +Use the `onRowSelectionChange` table option to hoist up the row selection state to your own scope. Then pass the row selection state back to the table instance using in the `state` table option. + +```ts +import { + createTable, + createTableState, + tableFeatures, + rowSelectionFeature, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const [rowSelection, setRowSelection] = createTableState({}) + +const table = createTable({ + features, + rowModels: {}, + //... + onRowSelectionChange: setRowSelection, + state: { + get rowSelection() { + return rowSelection() + }, + }, +}) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +const table = createTable({ + features, + rowModels: {}, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +const table = createTable({ + //... + enableRowSelection: row => row.original.age > 18, //only enable row selection for adults +}) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +const table = createTable({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +}) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +const table = createTable({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +}) +``` + +### Render Row Selection UI + +TanStack table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These function automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +```svelte + + + +``` + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. + +```svelte + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + {/each} + + {/each} + +``` diff --git a/docs/framework/svelte/guide/sorting.md b/docs/framework/svelte/guide/sorting.md new file mode 100644 index 0000000000..dda9c345c0 --- /dev/null +++ b/docs/framework/svelte/guide/sorting.md @@ -0,0 +1,470 @@ +--- +title: Sorting (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Sorting](../examples/sorting) + +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +```ts +import { createTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/svelte-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = createTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + get data() { + return data + }, +}) +``` + +## Sorting (Svelte) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```ts +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +You can access the sorting state directly from the table instance with `table.atoms.sorting.get()` or from the current `table.store.state` snapshot. + +```ts +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.sorting.get()) // access the current sorting state +``` + +However, if you need to access the sorting state before the table is initialized, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state, you can control/manage the sorting state in your own state management with the `state.sorting` and `onSortingChange` table options. + +```ts +import { createTableState } from '@tanstack/svelte-table' + +const [sorting, setSorting] = createTableState([]) // can set initial sorting state here + +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + state: { + get sorting() { + return sorting() + }, + }, + onSortingChange: setSorting, +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```ts +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. + +```ts +const [sorting, setSorting] = createTableState([]) + +const table = createTable({ + features: tableFeatures({ rowSortingFeature }), // feature needed for sorting state/APIs + rowModels: {}, // no sortedRowModel needed for manual sorting + columns, + data, + manualSorting: true, // use pre-sorted row model instead of sorted row model + state: { + get sorting() { + return sorting() + }, + }, + onSortingChange: setSorting, +}) +``` + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` to your features and the `sortedRowModel` to your row models. Import `createSortedRowModel` and `sortFns` from TanStack Table: + +```ts +import { + createTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFns, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = createTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions either as the `sortFn` column option, or as a global sorting function using the `sortFns` table option. + +#### Custom Sorting Functions + +When defining a custom sorting function in either the `sortFns` table option or as a `sortFn` column option, it should have the following signature: + +```ts +//optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = (rowA: Row, rowB: Row, columnId: string) => { + return //-1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> Note: The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // use custom global sorting function + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + } +] +//... +const table = createTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel({ + ...sortFns, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }), + }, + columns, + data, +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```ts +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, //sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, //sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + sortDescFirst: true, //sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> **NOTE**: You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +In not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> NOTE: `'first'` and `'last'` options are available in v9. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this: + +`'none' -> 'desc' -> 'asc' -> 'none' -> 'desc' -> 'asc' -> ...` + +If you disable sorting removal, the behavior will be like this: + +`'none' -> 'desc' -> 'asc' -> 'desc' -> 'asc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```ts +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (always none -> asc -> desc -> asc) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```ts +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```ts +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```ts +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```ts +const table = createTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/svelte/guide/virtualization.md b/docs/framework/svelte/guide/virtualization.md new file mode 100644 index 0000000000..7356a9121a --- /dev/null +++ b/docs/framework/svelte/guide/virtualization.md @@ -0,0 +1,239 @@ +--- +title: Virtualization (Svelte) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Svelte examples: + +- [Virtualized Columns](../examples/virtualized-columns) +- [Virtualized Rows](../examples/virtualized-rows) +- [Virtualized Infinite Scrolling](../examples/virtualized-infinite-scrolling) + +Use getters for reactive inputs such as `data` when passing Svelte state to `createTable`. + +### Svelte Setup + +Install and import the Svelte virtualizer adapter from `@tanstack/svelte-virtual`. TanStack Table still owns rows, columns, and table state; the virtualizer owns scroll indexes and measurements. +Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/react/examples/table). + +## Virtualization (Svelte) Guide + +The TanStack Table packages do not come with any virtualization APIs or features built in. Virtualization is a rendering strategy, not a table feature. You can use TanStack Table with any virtualization library, but the official examples use TanStack Virtual. + +TanStack Table and TanStack Virtual solve different parts of the problem: + +- TanStack Table builds the row models, columns, headers, cells, sizing, sorting, filtering, and other table state. +- TanStack Virtual decides which item indexes should be rendered for the current scroll position. +- Your table renderer maps those virtual indexes back to rows, headers, and cells. + +### When To Use Virtualization + +Use virtualization when your table has a very large number of rows, columns, or both. Virtualization keeps the DOM small by only rendering the items that are visible in the scroll viewport plus a small overscan buffer. + +Virtualization is not a replacement for server-side pagination, filtering, or sorting. If the data is virtualized on the client, the data still needs to exist on the client. If your dataset is too large to load into the browser, use server-side data operations or infinite scrolling. + +For small tables, normal rendering is simpler and usually preferable. + +### Install TanStack Virtual + +Install the Svelte virtualizer adapter: + +```sh +npm install @tanstack/svelte-virtual +``` + +The Svelte examples use `createVirtualizer` from `@tanstack/svelte-virtual`. TanStack Table still owns rows, columns, headers, cells, sizing, sorting, filtering, and other table state; TanStack Virtual decides which item indexes should render for the current scroll position. +### The Basic Pattern + +Most virtualized table implementations follow the same pattern: + +1. Create a fixed-height scroll container. +2. Pass the scroll element to the virtualizer. +3. Use `table.getRowModel().rows` or `table.getVisibleLeafColumns()` as the source list. +4. Configure `count`, `estimateSize`, `overscan`, and optional `measureElement`. +5. Render only virtual items. +6. Use virtual offsets or spacer padding to preserve the full scroll geometry. + +Here is a compact row virtualization example: + +```ts +const rows = table.getRowModel().rows + +const rowVirtualizer = createVirtualizer({ + get count() { + return rows.length + }, + getScrollElement: () => tableContainerRef, + estimateSize: () => 33, + overscan: 5, +}) +``` + +```svelte + + {#each $rowVirtualizer.getVirtualItems() as virtualRow (virtualRow.key)} + {@const row = rows[virtualRow.index]} + + {#each row.getVisibleCells() as cell (cell.id)} + + {/each} + + {/each} + +``` + +### Virtualized Rows + +The [virtualized rows examples](../examples/virtualized-rows) show how to render large row counts while keeping the DOM small. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The core idea is that sorting, filtering, grouping, and other row-model work still comes from TanStack Table. The virtualizer reads from the final table row model: + +```ts +const rows = table.getRowModel().rows +``` + +The row virtualizer is configured with `count: rows.length`, a row height estimate, the scroll container, and an overscan value. The `tbody` is given the full virtual height with `rowVirtualizer.getTotalSize()`, while each rendered row is absolutely positioned with `transform: translateY(...)`. + +The examples render cells from the current row with APIs like `row.getVisibleCells()` or `row.getAllCells()`, depending on whether the example needs visibility-aware cells or all cells. + +The official examples use large generated datasets, commonly tens or hundreds of thousands of rows. They also support dynamic row heights by using `measureElement` when possible. The examples skip dynamic row measurement in Firefox because Firefox can measure table border height differently. + +### Virtualized Columns + +The [virtualized columns examples](../examples/virtualized-columns) show how to render large row and column counts. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +Column virtualization uses the current visible column list: + +```ts +const visibleColumns = table.getVisibleLeafColumns() +``` + +The column virtualizer is configured for horizontal virtualization: + +```ts +const columnVirtualizer = createVirtualizer({ + count: visibleColumns.length, + estimateSize: index => visibleColumns[index].getSize(), + getScrollElement: () => tableContainerRef.current, + horizontal: true, + overscan: 3, +}) +``` + +Column virtualization uses a different rendering strategy than row virtualization. Instead of absolutely positioning columns, the examples add fake spacer cells to the left and right: + +```ts +const virtualColumns = columnVirtualizer.getVirtualItems() +const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 +const virtualPaddingRight = + columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) +``` + +Those spacer cells preserve the horizontal scroll width while the renderer only mounts the virtual columns. This approach keeps row rendering table-like and allows dynamic row height measurement to keep working. + +### Virtualized Rows And Columns Together + +The official virtualized columns examples also virtualize rows. In those examples: + +- The row virtualizer controls vertical positioning and total body height. +- The column virtualizer controls horizontal header/cell rendering and left/right spacer cells. +- `virtualRow.index` maps to `rows[virtualRow.index]`. +- `virtualColumn.index` maps to `visibleCells[virtualColumn.index]`. + +Always use virtual indexes against the same current row and column lists returned by the table. If sorting, filtering, pagination, grouping, or column visibility changes, recompute the virtualized rows and columns from the current table state. + +### Virtualized Infinite Scrolling + +The [virtualized infinite scrolling examples](../examples/virtualized-infinite-scrolling) combine row virtualization with progressive data fetching. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The common pattern is: + +1. Fetch a page of rows. +2. Flatten fetched pages into the table `data`. +3. Use row virtualization over the loaded rows. +4. Listen to scroll events on the table container. +5. Fetch the next page when the user scrolls near the bottom. + +The Svelte infinite scrolling pattern can use TanStack Query or any other data-fetching layer. + +```ts +const { scrollHeight, scrollTop, clientHeight } = scrollElement + +if (scrollHeight - scrollTop - clientHeight < 500) { + fetchNextPage() +} +``` + +If sorting is handled by the server, use manual sorting so the fetched data reflects the whole backend dataset rather than only the currently loaded rows. When sorting changes and the fetched dataset is replaced, scroll back to the top with `rowVirtualizer.scrollToIndex(0)`. + +### Dynamic Row Heights + +Dynamic row heights are useful when content can wrap or expand. They are also more complex than fixed-height rows. + +Use `estimateSize` as the virtualizer's initial guess: + +```ts +estimateSize: () => 33 +``` + +Then use `measureElement` to refine the actual row height after rendering: + +```ts +import { get } from 'svelte/store' + +function measureElement(node: HTMLTableRowElement) { + get(rowVirtualizer).measureElement(node) +} +``` + +```svelte + +``` + +Set `data-index` on each row so the virtualizer can associate measurements with the correct item. In non-React adapters, call `measureElement` through the adapter-appropriate ref, action, directive, or controller. + +Overscan helps avoid blank regions while measurements settle. If every row has a known fixed height, skip dynamic measurement and use the fixed height estimate instead. + +### Sticky Headers And Semantic Table Markup + +The examples still use semantic table tags, but they change table layout CSS to support virtual positioning and sticky headers. + +Dynamic row virtualization commonly requires: + +```css +table { + display: grid; +} + +thead { + display: grid; + position: sticky; + top: 0; +} + +tr { + display: flex; +} +``` + +Rows are absolutely positioned inside a relatively positioned `tbody`, and cells use flex sizing so they can match `column.getSize()` or `cell.column.getSize()`. This is intentional. Native table layout does not work well with dynamic-height virtual rows that are positioned independently. + +### Performance Tips + +- Keep virtualizers near the components that render the virtualized items. +- Avoid re-rendering the full table body on every scroll. +- Keep row, column, and data references stable where possible. +- Use `overscan` deliberately. More overscan reduces visible blanking, while less overscan reduces DOM nodes. +- Avoid expensive cell renderers in very large virtualized tables. +- Test production builds. Framework development builds can be slower than production builds; profile production bundles before optimizing. +- Prefer fixed row sizes when the UI allows it. +- For column virtualization, use `column.getSize()`, `header.getSize()`, and `cell.column.getSize()` consistently. diff --git a/docs/framework/vanilla/guide/pagination.md b/docs/framework/vanilla/guide/pagination.md new file mode 100644 index 0000000000..315b914dc2 --- /dev/null +++ b/docs/framework/vanilla/guide/pagination.md @@ -0,0 +1,262 @@ +--- +title: Pagination (Vanilla JS) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vanilla JS examples: + +- [Pagination](../examples/pagination) + +Vanilla examples use the core table package directly with store reactivity bindings and imperative DOM rendering. + +### Vanilla JS Setup + +```ts +import { constructTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/table-core' +import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings' + +const features = tableFeatures({ rowPaginationFeature, coreReativityFeature: storeReactivityBindings() }) + +const table = constructTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +## Pagination (Vanilla JS) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain ***ALL*** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. + +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. + +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 pagination and then switch to server-side pagination in the future as your data grows. + +#### Should You Use Virtualization Instead? + +Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` to your features and the `paginatedRowModel` to your row models: + +```jsx +import { + constructTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/table-core' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = constructTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. + +```jsx +import { + constructTable, + tableFeatures, + rowPaginationFeature, +} from '@tanstack/table-core' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = constructTable({ + features, + rowModels: {}, // no paginatedRowModel needed for server-side pagination + columns, + data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +You can manage the `pagination` state just like any other state in the table instance. + +```jsx +import { + constructTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/table-core' + +const features = tableFeatures({ rowPaginationFeature }) + +let pagination = { + pageIndex: 0, // initial page index + pageSize: 10, // default page size +} + +const setPagination = (updater) => { + pagination = typeof updater === 'function' ? updater(pagination) : updater + table.setOptions((prev) => ({ ...prev, state: { ...prev.state, pagination } })) +} + +const table = constructTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + onPaginationChange: setPagination, + state: { + pagination, + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```jsx +const table = constructTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT pass the `pagination` state to both the `state` and `initialState` options. `state` will overwrite `initialState`. Only use one or the other. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` when page-altering state changes occur, such as when the `data` is updated, filters change, grouping changes, etc. This behavior is automatically disabled when `manualPagination` is true but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. + +```jsx +const table = constructTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + autoResetPageIndex: false, // turn off auto reset of pageIndex +}) +``` + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> **Note**: These pagination APIs are available when using `rowPaginationFeature`. + +```ts +const firstPageButton = document.createElement('button') +firstPageButton.textContent = '<<' +firstPageButton.disabled = !table.getCanPreviousPage() +firstPageButton.onclick = () => table.firstPage() + +const previousPageButton = document.createElement('button') +previousPageButton.textContent = '<' +previousPageButton.disabled = !table.getCanPreviousPage() +previousPageButton.onclick = () => table.previousPage() + +const nextPageButton = document.createElement('button') +nextPageButton.textContent = '>' +nextPageButton.disabled = !table.getCanNextPage() +nextPageButton.onclick = () => table.nextPage() + +const lastPageButton = document.createElement('button') +lastPageButton.textContent = '>>' +lastPageButton.disabled = !table.getCanNextPage() +lastPageButton.onclick = () => table.lastPage() + +const pageSizeSelect = document.createElement('select') +pageSizeSelect.value = String(table.store.state.pagination.pageSize) +pageSizeSelect.onchange = (event) => { + table.setPageSize(Number((event.target as HTMLSelectElement).value)) +} + +for (const pageSize of [10, 20, 30, 40, 50]) { + const option = document.createElement('option') + option.value = String(pageSize) + option.textContent = `Show ${pageSize}` + pageSizeSelect.appendChild(option) +} +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/vanilla/guide/sorting.md b/docs/framework/vanilla/guide/sorting.md new file mode 100644 index 0000000000..2d22ec6727 --- /dev/null +++ b/docs/framework/vanilla/guide/sorting.md @@ -0,0 +1,471 @@ +--- +title: Sorting (Vanilla JS) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vanilla JS examples: + +- [Sorting](../examples/sorting) + +Vanilla examples use the core table package directly with store reactivity bindings and imperative DOM rendering. + +### Vanilla JS Setup + +```ts +import { constructTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/table-core' +import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings' + +const features = tableFeatures({ rowSortingFeature, coreReativityFeature: storeReactivityBindings() }) + +const table = constructTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +## Sorting (Vanilla JS) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```tsx +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +You can access the sorting state directly from the table instance with `table.atoms.sorting.get()` or from the current `table.store.state` snapshot. + +```tsx +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.sorting.get()) // access the current sorting state +``` + +However, if you need to access the sorting state before the table is initialized, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state, you can control/manage the sorting state in your own state management with the `state.sorting` and `onSortingChange` table options. + +```tsx +let sorting: SortingState = [] // can set initial sorting state here +//... +// use sorting state to fetch data from your server or something... +//... +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + state: { + sorting, + }, + onSortingChange: (updater) => { + sorting = typeof updater === 'function' ? updater(sorting) : updater + table.setOptions((prev) => ({ ...prev, state: { ...prev.state, sorting } })) + }, +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```jsx +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. + +```jsx +let sorting: SortingState = [] +//... +const table = constructTable({ + features: tableFeatures({ rowSortingFeature }), // feature needed for sorting state/APIs + rowModels: {}, // no sortedRowModel needed for manual sorting + columns, + data, + manualSorting: true, // use pre-sorted row model instead of sorted row model + state: { + sorting, + }, + onSortingChange: (updater) => { + sorting = typeof updater === 'function' ? updater(sorting) : updater + table.setOptions((prev) => ({ ...prev, state: { ...prev.state, sorting } })) + }, +}) +``` + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` to your features and the `sortedRowModel` to your row models. Import `createSortedRowModel` and `sortFns` from TanStack Table: + +```jsx +import { + constructTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFns, +} from '@tanstack/table-core' + +const features = tableFeatures({ rowSortingFeature }) + +const table = constructTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions either as the `sortFn` column option, or as a global sorting function using the `sortFns` table option. + +#### Custom Sorting Functions + +When defining a custom sorting function in either the `sortFns` table option or as a `sortFn` column option, it should have the following signature: + +```tsx +//optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = (rowA: Row, rowB: Row, columnId: string) => { + return //-1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> Note: The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```jsx +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // use custom global sorting function + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + } +] +//... +const table = constructTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel({ + ...sortFns, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }), + }, + columns, + data, +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```jsx +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```jsx +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, //sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, //sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + sortDescFirst: true, //sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> **NOTE**: You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```jsx +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +In not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> NOTE: `'first'` and `'last'` options are available in v9. + +```jsx +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this: + +`'none' -> 'desc' -> 'asc' -> 'none' -> 'desc' -> 'asc' -> ...` + +If you disable sorting removal, the behavior will be like this: + +`'none' -> 'desc' -> 'asc' -> 'desc' -> 'asc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```jsx +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (always none -> asc -> desc -> asc) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```jsx +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```jsx +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```jsx +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```jsx +const table = constructTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/vue/guide/column-faceting.md b/docs/framework/vue/guide/column-faceting.md new file mode 100644 index 0000000000..22d511953d --- /dev/null +++ b/docs/framework/vue/guide/column-faceting.md @@ -0,0 +1,113 @@ +--- +title: Column Faceting (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Faceted Filters](../examples/filters-faceted) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/vue-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, +}) +``` + +## Column Faceting (Vue) Guide + +Column Faceting is a feature that allows you to generate lists of values for a given column from that column's data. For example, a list of unique values in a column can be generated from all rows in that column to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a column of numbers to be used as a range for a range slider filter component. + +### Column Faceting Row Models + +In order to use any of the column faceting features, add the `columnFacetingFeature` to your features and the appropriate faceted row models to `rowModels`. + +```ts +import { + useTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/vue-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = useTable({ + features, + rowModels: { + 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 + }, + columns, + 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]; +``` + +### Custom (Server-Side) Faceting + +If 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. You can use the `getFacetedUniqueValues` and `getFacetedMinMaxValues` table options to resolve the faceted values from the server-side. + +```ts +const facetingQuery = useQuery( + //... +) + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, + getFacetedUniqueValues: (table, columnId) => { + const uniqueValueMap = new Map() + //... + return uniqueValueMap + }, + getFacetedMinMaxValues: (table, columnId) => { + //... + return [min, max] + }, + //... +}) +``` + +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/vue/guide/column-filtering.md b/docs/framework/vue/guide/column-filtering.md new file mode 100644 index 0000000000..ae1b2473bb --- /dev/null +++ b/docs/framework/vue/guide/column-filtering.md @@ -0,0 +1,376 @@ +--- +title: Column Filtering (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Fuzzy Search](../examples/filters-fuzzy) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/vue-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` + +## Column Filtering (Vue) 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 `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +const table = useTable({ + features: tableFeatures({ columnFilteringFeature }), + rowModels: {}, // no filteredRowModel needed for manual server-side filtering + 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` to your features and the `filteredRowModel` to your row models. Import `createFilteredRowModel` and `filterFns` from TanStack Table: + +```ts +import { + useTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/vue-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + 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 + +You can access the column filter state from the table instance with `table.atoms.columnFilters.get()` or from the current `table.store.state` snapshot. + +```ts +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.columnFilters.get()) // access the current column filters state +``` + +However, if you need to access the column filter state before the table is initialized, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state, you can control/manage the column filter state in your own state management with the `state.columnFilters` and `onColumnFiltersChange` table options. + +```ts +const columnFilters = ref([]) // can set initial column filter state here +//... +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + state: { + get columnFilters() { + + return columnFilters.value + + }, + }, + onColumnFiltersChange: (updater) => { + columnFilters.value = updater instanceof Function ? updater(columnFilters.value) : updater + }, +}) +``` + +#### Initial Column Filter State + +If you do not need to control the column filter state in your own state management or scope, but you still want to set an initial column filter state, you can use the `initialState` table option instead of `state`. + +```ts +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 10 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `arrIncludes` - Item inclusion within an array +- `arrIncludesAll` - All items included in an array +- `arrIncludesSome` - Some items included in an array +- `equals` - Object/referential equality `Object.is`/`===` +- `weakEquals` - Weak object/referential equality `==` +- `inNumberRange` - Number range inclusion + +You can also define your own custom filter functions either as the `filterFn` column option, or as a global filter function using the `filterFns` table option. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +When defining a custom filter function in either the `filterFn` column option or the `filterFns` table option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = (row: Row, columnId: string, filterValue: any, addMeta: (meta: any) => void) => boolean +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // use custom global filter function + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + } +] +//... +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }), + }, + columns, + data, +}) +``` + +##### 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 = ( + row: Row, + columnId: string, + filterValue: number | string, //resolveFilterValue will transform this 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 = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + columns, + data, + enableColumnFilters: false, // disable column filtering for all columns +}) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```ts +const table = useTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + filterFromLeafRows: true, // filter and search through sub-rows +}) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```ts +const table = useTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +}) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `onChange` or `onBlur` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/vue/guide/column-ordering.md b/docs/framework/vue/guide/column-ordering.md new file mode 100644 index 0000000000..810d0e0118 --- /dev/null +++ b/docs/framework/vue/guide/column-ordering.md @@ -0,0 +1,156 @@ +--- +title: Column Ordering (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Column Ordering](../examples/column-ordering) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnOrderingFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Ordering (Vue) 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 left, center (unpinned), and right 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 table = useTable({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + 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. + +```ts +const columnOrder = ref(['columnId1', 'columnId2', 'columnId3']) +//... +const table = useTable({ + features: tableFeatures({ columnOrderingFeature }), + rowModels: {}, + //... + state: { + get columnOrder() { + + return columnOrder.value + + }, + //... + } + onColumnOrderChange: (updater) => { + columnOrder.value = updater instanceof Function ? updater(columnOrder.value) : updater + }, + //... +}); +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, you can set up the logic something like this: + +```ts +const columnOrder = ref(columns.map(c => c.id)); + +//depending on your dnd solution of choice, you may or may not need state like this +const movingColumnId = ref(null); +const targetColumnId = ref(null); + +//util function to splice and reorder the columnOrder array +const reorderColumn = ( + movingColumnId: Column, + targetColumnId: Column, +): string[] => { + const newColumnOrder = [...columnOrder.value]; + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + newColumnOrder.splice(newColumnOrder.indexOf(movingColumnId), 1)[0], + ); + columnOrder.value = newColumnOrder +}; + +const handleDragEnd = (e: DragEvent) => { + if (!movingColumnId.value || !targetColumnId.value) return + columnOrder.value = reorderColumn(movingColumnId.value, targetColumnId.value) +}; + +//use your dnd solution of choice +``` + +### 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('left') +column.getIndex('center') +column.getIndex('right') + +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 (Vue) + +There are undoubtedly many ways to implement drag and drop features along-side TanStack Table. Here are a few suggestions in order for you to not have a bad time: + +1. Do NOT try to use [`"react-dnd"`](https://react-dnd.github.io/react-dnd/docs/overview) _if you are using Vue or newer_. framework-specific DnD was an important library for its time, but it now does not get updated very often, and it has incompatibilities with Vue, especially in Vue development mode. It is still possible to get it to work, but there are newer alternatives that have better compatibility and are more actively maintained. framework-specific DnD's Provider may also interfere and conflict with any other DnD solutions you may want to try in your app. + +2. Use [`"@dnd-kit/core"`](https://dndkit.com/). DnD Kit is a modern, modular and lightweight drag and drop library that is highly compatible with the modern Vue ecosystem, and it works well with semantic `` markup. The official framework-specific DnD examples, Column DnD and Row DnD, use DnD Kit. + +3. Consider other DnD libraries like [`"react-beautiful-dnd"`](https://github.com/atlassian/react-beautiful-dnd), but be aware of their potentially large bundle sizes, maintenance status, and compatibility with `
` markup. + +4. Consider using native browser events and state management to implement lightweight drag and drop features. However, be aware that this approach may not be best for mobile users if you do not go the extra mile to implement proper touch events. [Material React Table V2](https://www.material-react-table.com/docs/examples/column-ordering) is an example of a library that implements TanStack Table with only browser drag and drop events such as `onDragStart`, `onDragEnd`, `onDragEnter` and no other dependencies. Browse its source code to see how it is done. diff --git a/docs/framework/vue/guide/column-pinning.md b/docs/framework/vue/guide/column-pinning.md new file mode 100644 index 0000000000..2a60cb176e --- /dev/null +++ b/docs/framework/vue/guide/column-pinning.md @@ -0,0 +1,169 @@ +--- +title: Column Pinning (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnPinningFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ columnPinningFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Pinning (Vue) 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 left or right. + +### 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 left, center (unpinned), and right 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.left` and `columnPinning.right` 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. + +```ts +import { useTable, tableFeatures, columnPinningFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ columnPinningFeature }) + +const columnPinning = ref({ + left: [], + right: [], +}) + +const table = useTable({ + features, + rowModels: {}, + //... + state: { + get columnPinning() { + + return columnPinning.value + + }, + //... + }, + onColumnPinningChange: (updater) => { + columnPinning.value = updater instanceof Function ? updater(columnPinning.value) : 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, + rowModels: {}, + //... + initialState: { + columnPinning: { + left: ['expand-column'], + right: ['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 left or right. 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 `left` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `right` 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({ + left: ['firstName'], + right: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```ts +table.getLeftLeafColumns() +table.getCenterLeafColumns() +table.getRightLeafColumns() + +table.getLeftVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getRightVisibleLeafColumns() + +table.getLeftHeaderGroups() +table.getCenterHeaderGroups() +table.getRightHeaderGroups() + +table.getLeftFooterGroups() +table.getCenterFooterGroups() +table.getRightFooterGroups() + +table.getLeftFlatHeaders() +table.getCenterFlatHeaders() +table.getRightFlatHeaders() + +table.getLeftLeafHeaders() +table.getCenterLeafHeaders() +table.getRightLeafHeaders() +``` + +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('left') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('right') + +table.getPinnedVisibleLeafColumns('left') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('right') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'left'` or `'right'` to check one pinned side. + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getLeftHeaderGroups`, `table.getCenterHeaderGroups`, `table.getRightHeaderGroups`, `row.getLeftVisibleCells`, `row.getCenterVisibleCells`, and `row.getRightVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/vue/guide/column-resizing.md b/docs/framework/vue/guide/column-resizing.md new file mode 100644 index 0000000000..a4dedc91c6 --- /dev/null +++ b/docs/framework/vue/guide/column-resizing.md @@ -0,0 +1,224 @@ +--- +title: Column Resizing (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnResizingFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ columnResizingFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Resizing (Vue) 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 `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/vue-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, + rowModels: {}, + 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 React 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 React 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() +``` + +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. + +```vue + +``` + +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. + +```vue +
+``` + +#### 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. + +```vue +
+``` + +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 +} +``` + +Use `onColumnResizingChange` with `state.columnResizing` if you need to manage this state externally. + +```ts +const columnResizing = ref({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, + state: { + get columnResizing() { + + return columnResizing.value + + }, + }, + onColumnResizingChange: (updater) => { + columnResizing.value = updater instanceof Function ? updater(columnResizing.value) : 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. The current generated v9 API spelling is `table.setcolumnResizing` with a lowercase `c` in `column`; use that exact name. + +```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 Vue, you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns. + +We have created a [performant column resizing example](../examples/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: + +1. Don't use `column.getSize()` on every header and every data cell. Instead, calculate all column widths once upfront, **memoized**! +2. Memoize your Table Body while resizing is in progress. +3. Use CSS variables to communicate column widths to your table cells. + +If you follow these steps, you should see significant performance improvements while resizing columns. + +If you are not using React, and are using the Svelte, Vue, or Solid adapters instead, you may not need to worry about this as much, but similar principles apply. diff --git a/docs/framework/vue/guide/column-sizing.md b/docs/framework/vue/guide/column-sizing.md new file mode 100644 index 0000000000..ac41c454ec --- /dev/null +++ b/docs/framework/vue/guide/column-sizing.md @@ -0,0 +1,122 @@ +--- +title: Column Sizing (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Column Sizing](../examples/column-sizing) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnSizingFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ columnSizingFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Sizing (Vue) 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 columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +const table = useTable({ + features: tableFeatures({ columnSizingFeature }), + rowModels: {}, + 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() // left offset in the current column flow +column.getStart('left') +column.getStart('center') +column.getStart('right') + +column.getAfter() // right offset in the current column flow +column.getAfter('left') +column.getAfter('center') +column.getAfter('right') + +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.getLeftTotalSize() +table.getCenterTotalSize() +table.getRightTotalSize() +``` + +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) +``` diff --git a/docs/framework/vue/guide/column-visibility.md b/docs/framework/vue/guide/column-visibility.md new file mode 100644 index 0000000000..e105768d42 --- /dev/null +++ b/docs/framework/vue/guide/column-visibility.md @@ -0,0 +1,144 @@ +--- +title: Column Visibility (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Column Visibility](../examples/column-visibility) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnVisibilityFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Column Visibility (Vue) 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. + +```ts +import { useTable, tableFeatures, columnVisibilityFeature } from '@tanstack/vue-table' + +const columnVisibility = ref({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +const table = useTable({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + state: { + get columnVisibility() { + + return columnVisibility.value + + }, + //... + }, + onColumnVisibilityChange: (updater) => { + columnVisibility.value = updater instanceof Function ? updater(columnVisibility.value) : 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 table = useTable({ + features: tableFeatures({ columnVisibilityFeature }), + rowModels: {}, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +}) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```ts +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessor: '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. + +```vue + +``` + +### 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`. + +```vue +
+ +
+ + + + + + + + + + +
+ {{ column.id }} +
+ +
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/vue/guide/custom-features.md b/docs/framework/vue/guide/custom-features.md new file mode 100644 index 0000000000..37f8e281e7 --- /dev/null +++ b/docs/framework/vue/guide/custom-features.md @@ -0,0 +1,295 @@ +--- +title: Custom Features (Vue) Guide +--- + +Vue refs can be passed directly where the adapter expects reactive table options. + +## Custom Features (Vue) Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers where TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable`, `useTable`, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrapper hooks around the `useTable` hook to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking—you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods do: + +#### Default Options and Initial State + +
+ +##### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +##### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is responsible for adding methods to the `table` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. + +
+ +##### assignColumnPrototype + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, the [Column Grouping](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts) feature adds cell instance API methods such as `getIsGrouped` and `getIsAggregated`. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full custom-plugin example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/vue-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs options, and state in your application. + +```vue + + + +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `ref`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/vue/guide/expanding.md b/docs/framework/vue/guide/expanding.md new file mode 100644 index 0000000000..16020653ac --- /dev/null +++ b/docs/framework/vue/guide/expanding.md @@ -0,0 +1,278 @@ +--- +title: Expanding (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Expanding](../examples/expanding) + +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, rowExpandingFeature, createExpandedRowModel } from '@tanstack/vue-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = useTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + columns, + data, +}) +``` + +## Expanding Feature (Vue) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` to your features and the `expandedRowModel` to your row models: + +```ts +import { + useTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/vue-table' + +const features = tableFeatures({ rowExpandingFeature }) + +const table = useTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are essentially child rows that inherit the same column structure as their parent rows. If your data object already includes these expanded rows data, you can utilize the `getSubRows` function to specify these child rows. However, if your data object does not contain the expanded rows data, they can be treated as custom expanded data, which is discussed in next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 } + ] + }, + { id: 3, + name: 'Doe', + age: 40, + children: [ + { id: 4, name: 'Alice', age: 10 } + ] + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +const table = useTable({ + features, + rowModels: { + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +}) +``` + +> **Note:** You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +```vue + + + +``` + +### Expanded rows state + +If you need to control the expanded state of the rows in your table, you can do so by using the expanded state and the `onExpandedChange` option. This allows you to manage the expanded state according to your requirements. + +```ts +const expanded = ref({}) + +const table = useTable({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + state: { + get expanded() { + + return expanded.value + + }, + }, + onExpandedChange: (updater) => { + expanded.value = updater instanceof Function ? updater(expanded.value) : updater + }, +}) +``` + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows with IDs present as keys in the record and have their value set to true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. For example, you can add a button UI within the columns definition. + +```ts +const columns = [ + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'age', + header: 'Age', + }, + { + header: 'Children', + cell: ({ row }) => row.getCanExpand(), + }, +] +``` + +```vue + +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```ts +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```ts +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, the filtering process starts from the parent rows and moves downwards. This means if a parent row is excluded by the filter, all its child rows will also be excluded. However, you can change this behavior by using the `filterFromLeafRows` option. When this option is enabled, the filtering process starts from the leaf (child) rows and moves upwards. This ensures that a parent row will be included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. Additionally, you can control how deep into the child hierarchy the filter process goes by using the `maxLeafRowFilterDepth` option. This option allows you to specify the maximum depth of child rows that the filter should consider. + +```ts +//... +const table = useTable({ + features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + expandedRowModel: createExpandedRowModel(), + }, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parents page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +const table = useTable({ + features, + rowModels: { expandedRowModel: createExpandedRowModel() }, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Pinning Guide](../../../guide/pinning.md) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const table = useTable({ + features: tableFeatures({ rowExpandingFeature }), + rowModels: {}, // no expandedRowModel needed for manual expanding + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/vue/guide/fuzzy-filtering.md b/docs/framework/vue/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..377b2ca74d --- /dev/null +++ b/docs/framework/vue/guide/fuzzy-filtering.md @@ -0,0 +1,153 @@ +--- +title: Fuzzy Filtering (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Fuzzy Search](../examples/filters-fuzzy) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/vue-table' + +const features = tableFeatures({ columnFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` + +## Fuzzy Filtering (Vue) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement a client side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> **Note:** You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked in order to work better with TanStack Table's row by row filtering approach. + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +Here's an example of a custom fuzzy filter function: + +```typescript +import { rankItem } from '@tanstack/match-sorter-utils'; +import { FilterFn } from '@tanstack/vue-table' + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the rankItem function from the @tanstack/match-sorter-utils library to rank the item. We then store the ranking information in the meta data of the row, and return whether the item passed the ranking criteria. + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, you can specify the fuzzy filter function in the globalFilterFn option of the table instance: + +```typescript +import { + useTable, + tableFeatures, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + filterFns, + sortFns, +} from '@tanstack/vue-table' + +const features = tableFeatures({ globalFilteringFeature, rowSortingFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel({ + ...filterFns, + fuzzy: fuzzyFilter, + }), + sortedRowModel: createSortedRowModel(sortFns), // needed if you want sorting with fuzzy rank + }, + columns, + data, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, pass your fuzzy filter function to `createFilteredRowModel` (merging it with the built-in `filterFns`). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```typescript +const column = [ + { + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +]; +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```typescript +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFns } from '@tanstack/vue-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId]?.itemRank!, + rowB.columnFiltersMeta[columnId]?.itemRank! + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then specify this sorting function in the sortFn option of the column definition: + +```typescript +{ + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + sortFn: 'fuzzySort', //using our custom fuzzy sort function +} +``` diff --git a/docs/framework/vue/guide/global-faceting.md b/docs/framework/vue/guide/global-faceting.md new file mode 100644 index 0000000000..906d5d1d71 --- /dev/null +++ b/docs/framework/vue/guide/global-faceting.md @@ -0,0 +1,109 @@ +--- +title: Global Faceting (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Faceted Filters](../examples/filters-faceted) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnFacetingFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues } from '@tanstack/vue-table' + +const features = tableFeatures({ columnFacetingFeature }) + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + }, + columns, + data, +}) +``` + +## Global Faceting (Vue) Guide + +Global Faceting allows you to generate lists of values for all columns from the table's data. For example, a list of unique values in a table can be generated from all rows in all columns to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a table of numbers to be used as a range for a range slider filter component. + +### Global Faceting Row Models + +In order to use any of the global faceting features, add the appropriate faceted row models to your `rowModels`: + +```ts +import { + useTable, + tableFeatures, + columnFacetingFeature, + createFacetedRowModel, + createFacetedMinMaxValues, + createFacetedUniqueValues, +} from '@tanstack/vue-table' + +const features = tableFeatures({ columnFacetingFeature }) +// If the table also uses global filtering, include globalFilteringFeature too. + +const table = useTable({ + features, + rowModels: { + facetedRowModel: createFacetedRowModel(), // required (other faceting methods depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + }, + // other options... +}) +``` + +### Use Global Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting table instance APIs to access the lists of values generated by the faceted row models. + +Use `table.getGlobalFacetedRowModel()` if you need to inspect the row model used to derive global facet 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 Global (Server-Side) Faceting + +If 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 `rowModels.facetedUniqueValues` and `rowModels.facetedMinMaxValues` factories. Global faceting requests use the internal `__global__` column ID. + +```ts +const globalFacets = await fetch('/api/faceting').then(res => res.json()) + +const table = useTable({ + features, + rowModels: { + facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() + return new Map(globalFacets.uniqueValues) + }, + facetedMinMaxValues: (_table, columnId) => () => { + if (columnId !== '__global__') return undefined + return globalFacets.minMaxValues + }, + }, + // other options... +}) +``` + +In this example, the custom factories return server-provided global facet values when TanStack Table asks for the `__global__` faceting context. Column-specific faceting can still use separate values by checking for each column ID. diff --git a/docs/framework/vue/guide/global-filtering.md b/docs/framework/vue/guide/global-filtering.md new file mode 100644 index 0000000000..b04ecb7feb --- /dev/null +++ b/docs/framework/vue/guide/global-filtering.md @@ -0,0 +1,248 @@ +--- +title: Global Filtering (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Column Filters](../examples/filters) +- [Fuzzy Search](../examples/filters-fuzzy) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, globalFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/vue-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + columns, + data, +}) +``` + +## Global Filtering (Vue) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +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 Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `rowModels`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +import { + useTable, + tableFeatures, + globalFilteringFeature, +} from '@tanstack/vue-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = useTable({ + features, + rowModels: {}, // no filteredRowModel needed for manual server-side global filtering + data, + columns, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` to your features and the `filteredRowModel` to your row models: + +```ts +import { + useTable, + tableFeatures, + globalFilteringFeature, + createFilteredRowModel, + filterFns, +} from '@tanstack/vue-table' + +const features = tableFeatures({ globalFilteringFeature }) + +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + // other options... +}) +``` + +### Global Filter Function + +The globalFilterFn option allows you to specify the filter function that will be used for global filtering. The filter function can be a string that references a built-in filter function, a string that references a custom filter function provided via the tableOptions.filterFns option, or a custom filter function. + +```ts +const table = useTable({ + features, + rowModels: { + filteredRowModel: createFilteredRowModel(filterFns), + }, + data, + columns, + globalFilterFn: 'text', // built-in filter function +}) +``` + +By default there are 10 built-in filter functions to choose from: + +- includesString - Case-insensitive string inclusion +- includesStringSensitive - Case-sensitive string inclusion +- equalsString - Case-insensitive string equality +- equalsStringSensitive - Case-sensitive string equality +- arrIncludes - Item inclusion within an array +- arrIncludesAll - All items included in an array +- arrIncludesSome - Some items included in an array +- equals - Object/referential equality Object.is/=== +- weakEquals - Weak object/referential equality == +- inNumberRange - Number range inclusion + +You can also define your own custom filter functions either as the globalFilterFn table option. + +### Global Filter State + +The global filter state is stored in the table's state atoms and can be read with `table.atoms.globalFilter.get()` or from the current `table.store.state.globalFilter` snapshot. If you want to persist the global filter state outside of the table, use the `state.globalFilter` and `onGlobalFilterChange` options together. + +```ts +const globalFilter = ref([]) + +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + state: { + get globalFilter() { + + return globalFilter.value + + }, + }, + onGlobalFilterChange: (updater) => { + globalFilter.value = updater instanceof Function ? updater(globalFilter.value) : updater + }, +}) +``` + +The global filtering state is defined as an object with the following shape: + +```ts +interface GlobalFilter { + globalFilter: any +} +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. + +```vue + +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the globalFilterFn option. + +> **Note:** It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering.md). + +```ts +const customFilterFn = (rows, columnId, filterValue) => { + // custom filter logic +} + +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table initialState option. + +However, you can also just specify the initial global filter state in the state.globalFilter option. + +```ts +const globalFilter = ref("search term") //recommended to initialize globalFilter state here + +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + initialState: { + globalFilter: 'search term', // if not managing globalFilter state, set initial state here + }, + state: { + get globalFilter() { + + return globalFilter.value + + }, // pass our managed globalFilter state to the table + }, +}) +``` + +> NOTE: Do not use both initialState.globalFilter and state.globalFilter at the same time, as the initialized state in the state.globalFilter will override the initialState.globalFilter. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` diff --git a/docs/framework/vue/guide/grouping.md b/docs/framework/vue/guide/grouping.md new file mode 100644 index 0000000000..83053b4ab2 --- /dev/null +++ b/docs/framework/vue/guide/grouping.md @@ -0,0 +1,232 @@ +--- +title: Grouping (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Grouping](../examples/grouping) + +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, columnGroupingFeature, createGroupedRowModel, aggregationFns } from '@tanstack/vue-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + columns, + data, +}) +``` + +## Grouping (Vue) Guide + +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 left, center (unpinned), and right pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +To use the grouping feature, add the `columnGroupingFeature` to your features and the `groupedRowModel` to your row models. The grouped row model is responsible for grouping the rows based on the grouping state. + +```ts +import { + useTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, + aggregationFns, +} from '@tanstack/vue-table' + +const features = tableFeatures({ columnGroupingFeature }) + +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + }, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```ts +const features = tableFeatures({ columnGroupingFeature, rowExpandingFeature }) + +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel(aggregationFns), + expandedRowModel: createExpandedRowModel(), + }, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```ts +table.setGrouping(['column1', 'column2']); +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```ts +table.resetGrouping(); +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```ts +const table = useTable({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows by columns using the aggregationFn option. This is a string that is the ID of the aggregation function. You can define the aggregation functions using the aggregationFns option. + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'sum', +}) +``` + +In the above example, the sum aggregation function will be used to aggregate the data in the grouped rows. +By default, numeric columns will use the sum aggregation function, and non-numeric columns will use the count aggregation function. You can override this behavior by specifying the aggregationFn option in the column definition. + +There are several built-in aggregation functions that you can use: + +- sum - Sums the values in the grouped rows. +- count - Counts the number of rows in the grouped rows. +- min - Finds the minimum value in the grouped rows. +- max - Finds the maximum value in the grouped rows. +- extent - Finds the extent (min and max) of the values in the grouped rows. +- mean - Finds the mean of the values in the grouped rows. +- median - Finds the median of the values in the grouped rows. +- unique - Returns an array of unique values in the grouped rows. +- uniqueCount - Counts the number of unique values in the grouped rows. + +#### Custom Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows using the aggregationFns option. This is a record where the keys are the IDs of the aggregation functions, and the values are the aggregation functions themselves. You can then reference these aggregation functions in a column's aggregationFn option. + +```ts +const table = useTable({ + features, + rowModels: { + groupedRowModel: createGroupedRowModel({ + ...aggregationFns, + myCustomAggregation: (columnId, leafRows, childRows) => { + // return the aggregated value + }, + }), + }, + // other options... +}) +``` + +In the above example, myCustomAggregation is a custom aggregation function that takes the column ID, the leaf rows, and the child rows, and returns the aggregated value. You can then use this aggregation function in a column's aggregationFn option: + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'myCustomAggregation', +}) +``` + +### Manual Grouping + +If you are doing server-side grouping and aggregation, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to manually group the rows before passing them to the table. + +```ts +const table = useTable({ + features: tableFeatures({ columnGroupingFeature }), + rowModels: {}, // no groupedRowModel needed for manual grouping + // other options... + manualGrouping: true, +}) +``` + +> **Note:** There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Grouping Change Handler + +If you want to manage the grouping state yourself, you can use the onGroupingChange option. This option is a function that is called when the grouping state changes. You can pass the managed state back to the table via the tableOptions.state.grouping option. + +```ts +const grouping = ref([]) + +const table = useTable({ + features, + rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, + // other options... + state: { + get grouping() { + + return grouping.value + + }, + }, + onGroupingChange: (updater) => { + grouping.value = updater instanceof Function ? updater(grouping.value) : updater + }, +}) +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```ts +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +column.getAutoAggregationFn() +column.getAggregationFn() +``` + +Rows expose grouping helpers for grouped row rendering: + +```ts +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose helpers for choosing between grouped, aggregated, placeholder, and normal cell rendering: + +```ts +cell.getIsGrouped() +cell.getIsAggregated() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```ts +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/vue/guide/pagination.md b/docs/framework/vue/guide/pagination.md new file mode 100644 index 0000000000..23752193a1 --- /dev/null +++ b/docs/framework/vue/guide/pagination.md @@ -0,0 +1,247 @@ +--- +title: Pagination (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Pagination](../examples/pagination) + +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/vue-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +## Pagination (Vue) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain ***ALL*** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. + +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows and still performs well, albeit with only handful of columns. + +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 pagination and then switch to server-side pagination in the future as your data grows. + +#### Should You Use Virtualization Instead? + +Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` to your features and the `paginatedRowModel` to your row models: + +```ts +import { + useTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/vue-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, +}) +``` + +> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. + +```ts +import { + useTable, + tableFeatures, + rowPaginationFeature, +} from '@tanstack/vue-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = useTable({ + features, + rowModels: {}, // no paginatedRowModel needed for server-side pagination + columns, + data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +You can manage the `pagination` state just like any other state in the table instance. + +```ts +import { + useTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/vue-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const pagination = ref({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + onPaginationChange: (updater) => { + pagination.value = updater instanceof Function ? updater(pagination.value) : updater + }, + state: { + get pagination() { + + return pagination.value + + }, + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```ts +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT pass the `pagination` state to both the `state` and `initialState` options. `state` will overwrite `initialState`. Only use one or the other. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` when page-altering state changes occur, such as when the `data` is updated, filters change, grouping changes, etc. This behavior is automatically disabled when `manualPagination` is true but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. + +```ts +const table = useTable({ + features, + rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + data, + autoResetPageIndex: false, // turn off auto reset of pageIndex +}) +``` + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> **Note**: These pagination APIs are available when using `rowPaginationFeature`. + +```vue + + + + + +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/vue/guide/row-pinning.md b/docs/framework/vue/guide/row-pinning.md new file mode 100644 index 0000000000..4cc5c28e71 --- /dev/null +++ b/docs/framework/vue/guide/row-pinning.md @@ -0,0 +1,211 @@ +--- +title: Row Pinning (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Row Pinning](../examples/row-pinning) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, rowPinningFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Row Pinning (Vue) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory, so `rowModels` can stay empty unless your table uses other row-model features. + +```ts +import { + rowPinningFeature, + tableFeatures, + useTable, +} from '@tanstack/vue-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```ts +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```ts +const table = useTable({ + features, + rowModels: {}, + columns, + data, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, use `state.rowPinning` with `onRowPinningChange`. + +```ts +const rowPinning = ref({ + top: [], + bottom: [], +}) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, + state: { + get rowPinning() { + + return rowPinning.value + + }, + }, + onRowPinningChange: (updater) => { + rowPinning.value = updater instanceof Function ? updater(rowPinning.value) : updater + }, +}) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```ts +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +}) + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```ts +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls: + +```vue +
+ + + +
+``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```ts +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly: + +```vue + + + + + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```ts +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```ts +const table = useTable({ + features, + rowModels: {}, + columns, + data, + enableRowPinning: row => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```ts +const table = useTable({ + features, + rowModels: {}, + columns, + data, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/vue/guide/row-selection.md b/docs/framework/vue/guide/row-selection.md new file mode 100644 index 0000000000..d72473c978 --- /dev/null +++ b/docs/framework/vue/guide/row-selection.md @@ -0,0 +1,199 @@ +--- +title: Row Selection (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Row Selection](../examples/row-selection) +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, rowSelectionFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) +``` + +## Row Selection (Vue) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you (though as seen down below, it may be more convenient to manage the row selection state in your own scope). You can access the internal row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the current row selection state +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +> Note: If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +Even though the table instance will already manage the row selection state for you, it is usually more convenient to manage the state yourself in order to have easy access to the selected row ids that you can use to make API calls or other actions. + +Use the `onRowSelectionChange` table option to hoist up the row selection state to your own scope. Then pass the row selection state back to the table instance using in the `state` table option. + +```ts +import { useTable, tableFeatures, rowSelectionFeature } from '@tanstack/vue-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const rowSelection = ref({}) + +const table = useTable({ + features, + rowModels: {}, + //... + onRowSelectionChange: (updater) => { + rowSelection.value = updater instanceof Function ? updater(rowSelection.value) : updater + }, + state: { + get rowSelection() { + + return rowSelection.value + + }, + }, +}) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +const table = useTable({ + features, + rowModels: {}, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +const table = useTable({ + //... + enableRowSelection: row => row.original.age > 18, //only enable row selection for adults +}) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +const table = useTable({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +}) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +const table = useTable({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +}) +``` + +### Render Row Selection UI + +TanStack table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These function automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +```vue + + + +``` + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. + +```vue + + + + + + + +``` diff --git a/docs/framework/vue/guide/sorting.md b/docs/framework/vue/guide/sorting.md new file mode 100644 index 0000000000..d9831ac0ed --- /dev/null +++ b/docs/framework/vue/guide/sorting.md @@ -0,0 +1,476 @@ +--- +title: Sorting (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Sorting](../examples/sorting) + +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +```ts +import { useTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/vue-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = useTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +## Sorting (Vue) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```ts +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +You can access the sorting state directly from the table instance with `table.atoms.sorting.get()` or from the current `table.store.state` snapshot. + +```ts +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... +}) + +console.log(table.atoms.sorting.get()) // access the current sorting state +``` + +However, if you need to access the sorting state before the table is initialized, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state, you can control/manage the sorting state in your own state management with the `state.sorting` and `onSortingChange` table options. + +```ts +const sorting = ref([]) // can set initial sorting state here +//... +// use sorting state to fetch data from your server or something... +//... +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + state: { + get sorting() { + + return sorting.value + + }, + }, + onSortingChange: (updater) => { + sorting.value = updater instanceof Function ? updater(sorting.value) : updater + }, +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```ts +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + //... + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. + +```ts +const sorting = ref([]) +//... +const table = useTable({ + features: tableFeatures({ rowSortingFeature }), // feature needed for sorting state/APIs + rowModels: {}, // no sortedRowModel needed for manual sorting + columns, + data, + manualSorting: true, // use pre-sorted row model instead of sorted row model + state: { + get sorting() { + + return sorting.value + + }, + }, + onSortingChange: (updater) => { + sorting.value = updater instanceof Function ? updater(sorting.value) : updater + }, +}) +``` + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` to your features and the `sortedRowModel` to your row models. Import `createSortedRowModel` and `sortFns` from TanStack Table: + +```ts +import { + useTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFns, +} from '@tanstack/vue-table' + +const features = tableFeatures({ rowSortingFeature }) + +const table = useTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + }, + columns, + data, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions either as the `sortFn` column option, or as a global sorting function using the `sortFns` table option. + +#### Custom Sorting Functions + +When defining a custom sorting function in either the `sortFns` table option or as a `sortFn` column option, it should have the following signature: + +```ts +//optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = (rowA: Row, rowB: Row, columnId: string) => { + return //-1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> Note: The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // use custom global sorting function + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + } +] +//... +const table = useTable({ + features, + rowModels: { + sortedRowModel: createSortedRowModel({ + ...sortFns, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }), + }, + columns, + data, +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```ts +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, //sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, //sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + sortDescFirst: true, //sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> **NOTE**: You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +In not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> NOTE: `'first'` and `'last'` options are available in v9. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this: + +`'none' -> 'desc' -> 'asc' -> 'none' -> 'desc' -> 'asc' -> ...` + +If you disable sorting removal, the behavior will be like this: + +`'none' -> 'desc' -> 'asc' -> 'desc' -> 'asc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```ts +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (always none -> asc -> desc -> asc) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```ts +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```ts +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```ts +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```ts +const table = useTable({ + features, + rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, + columns, + data, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/vue/guide/virtualization.md b/docs/framework/vue/guide/virtualization.md new file mode 100644 index 0000000000..12344fcf4c --- /dev/null +++ b/docs/framework/vue/guide/virtualization.md @@ -0,0 +1,250 @@ +--- +title: Virtualization (Vue) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Vue examples: + +- [Virtualized Columns](../examples/virtualized-columns) +- [Virtualized Rows](../examples/virtualized-rows) +- [Virtualized Infinite Scrolling](../examples/virtualized-infinite-scrolling) + +Vue refs can be passed directly where the adapter expects reactive table options. + +### Vue Setup + +Install and import the Vue virtualizer adapter from `@tanstack/vue-virtual`. TanStack Table still owns rows, columns, and table state; the virtualizer owns scroll indexes and measurements. +Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/react/examples/table). + +## Virtualization (Vue) Guide + +The TanStack Table packages do not come with any virtualization APIs or features built in. Virtualization is a rendering strategy, not a table feature. You can use TanStack Table with any virtualization library, but the official examples use TanStack Virtual. + +TanStack Table and TanStack Virtual solve different parts of the problem: + +- TanStack Table builds the row models, columns, headers, cells, sizing, sorting, filtering, and other table state. +- TanStack Virtual decides which item indexes should be rendered for the current scroll position. +- Your table renderer maps those virtual indexes back to rows, headers, and cells. + +### When To Use Virtualization + +Use virtualization when your table has a very large number of rows, columns, or both. Virtualization keeps the DOM small by only rendering the items that are visible in the scroll viewport plus a small overscan buffer. + +Virtualization is not a replacement for server-side pagination, filtering, or sorting. If the data is virtualized on the client, the data still needs to exist on the client. If your dataset is too large to load into the browser, use server-side data operations or infinite scrolling. + +For small tables, normal rendering is simpler and usually preferable. + +### Install TanStack Virtual + +Install the Vue virtualizer adapter: + +```sh +npm install @tanstack/vue-virtual +``` + +The Vue examples use `useVirtualizer` from `@tanstack/vue-virtual`. TanStack Table still owns rows, columns, headers, cells, sizing, sorting, filtering, and other table state; TanStack Virtual decides which item indexes should render for the current scroll position. +### The Basic Pattern + +Most virtualized table implementations follow the same pattern: + +1. Create a fixed-height scroll container. +2. Pass the scroll element to the virtualizer. +3. Use `table.getRowModel().rows` or `table.getVisibleLeafColumns()` as the source list. +4. Configure `count`, `estimateSize`, `overscan`, and optional `measureElement`. +5. Render only virtual items. +6. Use virtual offsets or spacer padding to preserve the full scroll geometry. + +Here is a compact row virtualization example: + +```ts +const rows = computed(() => table.getRowModel().rows) + +const rowVirtualizer = useVirtualizer( + computed(() => ({ + count: rows.value.length, + getScrollElement: () => tableContainerRef.value, + estimateSize: () => 33, + overscan: 5, + })), +) + +const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems()) +const totalSize = computed(() => rowVirtualizer.value.getTotalSize()) +``` + +```vue + + + + + + + +``` + +### Virtualized Rows + +The [virtualized rows examples](../examples/virtualized-rows) show how to render large row counts while keeping the DOM small. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The core idea is that sorting, filtering, grouping, and other row-model work still comes from TanStack Table. The virtualizer reads from the final table row model: + +```ts +const rows = table.getRowModel().rows +``` + +The row virtualizer is configured with `count: rows.length`, a row height estimate, the scroll container, and an overscan value. The `tbody` is given the full virtual height with `rowVirtualizer.getTotalSize()`, while each rendered row is absolutely positioned with `transform: translateY(...)`. + +The examples render cells from the current row with APIs like `row.getVisibleCells()` or `row.getAllCells()`, depending on whether the example needs visibility-aware cells or all cells. + +The official examples use large generated datasets, commonly tens or hundreds of thousands of rows. They also support dynamic row heights by using `measureElement` when possible. The examples skip dynamic row measurement in Firefox because Firefox can measure table border height differently. + +### Virtualized Columns + +The [virtualized columns examples](../examples/virtualized-columns) show how to render large row and column counts. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +Column virtualization uses the current visible column list: + +```ts +const visibleColumns = table.getVisibleLeafColumns() +``` + +The column virtualizer is configured for horizontal virtualization: + +```ts +const columnVirtualizer = useVirtualizer({ + count: visibleColumns.length, + estimateSize: index => visibleColumns[index].getSize(), + getScrollElement: () => tableContainerRef.current, + horizontal: true, + overscan: 3, +}) +``` + +Column virtualization uses a different rendering strategy than row virtualization. Instead of absolutely positioning columns, the examples add fake spacer cells to the left and right: + +```ts +const virtualColumns = columnVirtualizer.getVirtualItems() +const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 +const virtualPaddingRight = + columnVirtualizer.getTotalSize() - + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) +``` + +Those spacer cells preserve the horizontal scroll width while the renderer only mounts the virtual columns. This approach keeps row rendering table-like and allows dynamic row height measurement to keep working. + +### Virtualized Rows And Columns Together + +The official virtualized columns examples also virtualize rows. In those examples: + +- The row virtualizer controls vertical positioning and total body height. +- The column virtualizer controls horizontal header/cell rendering and left/right spacer cells. +- `virtualRow.index` maps to `rows[virtualRow.index]`. +- `virtualColumn.index` maps to `visibleCells[virtualColumn.index]`. + +Always use virtual indexes against the same current row and column lists returned by the table. If sorting, filtering, pagination, grouping, or column visibility changes, recompute the virtualized rows and columns from the current table state. + +### Virtualized Infinite Scrolling + +The [virtualized infinite scrolling examples](../examples/virtualized-infinite-scrolling) combine row virtualization with progressive data fetching. The examples are available for React, Solid, Svelte, Vue, Angular, and Lit. + +The common pattern is: + +1. Fetch a page of rows. +2. Flatten fetched pages into the table `data`. +3. Use row virtualization over the loaded rows. +4. Listen to scroll events on the table container. +5. Fetch the next page when the user scrolls near the bottom. + +The Vue infinite scrolling pattern can use TanStack Query or any other data-fetching layer. + +```ts +const { scrollHeight, scrollTop, clientHeight } = scrollElement + +if (scrollHeight - scrollTop - clientHeight < 500) { + fetchNextPage() +} +``` + +If sorting is handled by the server, use manual sorting so the fetched data reflects the whole backend dataset rather than only the currently loaded rows. When sorting changes and the fetched dataset is replaced, scroll back to the top with `rowVirtualizer.scrollToIndex(0)`. + +### Dynamic Row Heights + +Dynamic row heights are useful when content can wrap or expand. They are also more complex than fixed-height rows. + +Use `estimateSize` as the virtualizer's initial guess: + +```ts +estimateSize: () => 33 +``` + +Then use `measureElement` to refine the actual row height after rendering: + +```ts +function measureElement(el: Element | ComponentPublicInstance | null) { + if (!el || !(el instanceof Element)) return + rowVirtualizer.value.measureElement(el) +} +``` + +```vue + +``` + +Set `data-index` on each row so the virtualizer can associate measurements with the correct item. In non-React adapters, call `measureElement` through the adapter-appropriate ref, action, directive, or controller. + +Overscan helps avoid blank regions while measurements settle. If every row has a known fixed height, skip dynamic measurement and use the fixed height estimate instead. + +### Sticky Headers And Semantic Table Markup + +The examples still use semantic table tags, but they change table layout CSS to support virtual positioning and sticky headers. + +Dynamic row virtualization commonly requires: + +```css +table { + display: grid; +} + +thead { + display: grid; + position: sticky; + top: 0; +} + +tr { + display: flex; +} +``` + +Rows are absolutely positioned inside a relatively positioned `tbody`, and cells use flex sizing so they can match `column.getSize()` or `cell.column.getSize()`. This is intentional. Native table layout does not work well with dynamic-height virtual rows that are positioned independently. + +### Performance Tips + +- Keep virtualizers near the components that render the virtualized items. +- Avoid re-rendering the full table body on every scroll. +- Keep row, column, and data references stable where possible. +- Use `overscan` deliberately. More overscan reduces visible blanking, while less overscan reduces DOM nodes. +- Avoid expensive cell renderers in very large virtualized tables. +- Test production builds. Framework development builds can be slower than production builds; profile production bundles before optimizing. +- Prefer fixed row sizes when the UI allows it. +- For column virtualization, use `column.getSize()`, `header.getSize()`, and `cell.column.getSize()` consistently. diff --git a/docs/guide/column-defs.md b/docs/guide/column-defs.md index 48868cf771..629c2f207c 100644 --- a/docs/guide/column-defs.md +++ b/docs/guide/column-defs.md @@ -275,7 +275,7 @@ columnHelper.accessor('firstName', { ## Aggregated Cell Formatting -For more info on aggregated cells, see [grouping](./grouping). +For more info on aggregated cells, see [grouping](../framework/react/guide/grouping). ## Header & Footer Formatting diff --git a/docs/guide/data.md b/docs/guide/data.md index 5154f95b62..1cab5fc24a 100644 --- a/docs/guide/data.md +++ b/docs/guide/data.md @@ -179,7 +179,7 @@ type User = { } ``` -Where `subRows` is an optional array of `User` objects. This is discussed in more detail in the [Expanding Guide](./expanding). +Where `subRows` is an optional array of `User` objects. This is discussed in more detail in the [Expanding Guide](../framework/react/guide/expanding). ### Give Data a "Stable" Reference @@ -252,4 +252,4 @@ Believe it or not, TanStack Table was actually built to scale up to handle poten The default mindset of a developer building a data grid is to implement server-side pagination, sorting, and filtering for large datasets. This is still usually a good idea, but a lot of developers underestimate how much data can actually be handled in the client with modern browsers and the right optimizations. If your table will never have more than a few thousand rows, you can probably take advantage of the client-side features in TanStack Table instead of implementing them yourself on the server. Before committing to letting TanStack Table's client-side features handle your large dataset, you should test it with your actual data to see if it performs well enough for your needs, of course. -This is discussed in more detail in the [Pagination Guide](./pagination#should-you-use-client-side-pagination). +This is discussed in more detail in the [Pagination Guide](../framework/react/guide/pagination#should-you-use-client-side-pagination). diff --git a/docs/guide/features.md b/docs/guide/features.md index 1fbc2143ee..64ff0c75c5 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -6,19 +6,157 @@ TanStack Table comes with many features, each with their own associated options > **v9 note:** In v9, features are opt-in. You declare which features your table uses via the `features` option (using `tableFeatures()`). This enables tree-shaking—you only bundle the code for the features you need. See the [Table Instance Guide](./tables) and [Row Models Guide](./row-models) for setup. To include all features (v8-style), use `stockFeatures`. -- [Column Ordering](./column-ordering) -- [Column Pinning](./column-pinning) -- [Column Sizing](./column-sizing) -- [Column Resizing](./column-resizing) -- [Column Visibility](./column-visibility) -- [Expanding](./expanding) -- [Column Faceting](./column-faceting) -- [Column Filtering](./column-filtering) -- [Global Faceting](./global-faceting) -- [Global Filtering](./global-filtering) -- [Grouping](./grouping) -- [Pagination](./pagination) -- [Row Pinning](./row-pinning) -- [Row Selection](./row-selection) -- [Sorting](./sorting) -- [Virtualization](./virtualization) + + +# React + +- [Column Ordering](../framework/react/guide/column-ordering) +- [Column Pinning](../framework/react/guide/column-pinning) +- [Column Sizing](../framework/react/guide/column-sizing) +- [Column Resizing](../framework/react/guide/column-resizing) +- [Column Visibility](../framework/react/guide/column-visibility) +- [Expanding](../framework/react/guide/expanding) +- [Column Faceting](../framework/react/guide/column-faceting) +- [Column Filtering](../framework/react/guide/column-filtering) +- [Global Faceting](../framework/react/guide/global-faceting) +- [Global Filtering](../framework/react/guide/global-filtering) +- [Grouping](../framework/react/guide/grouping) +- [Pagination](../framework/react/guide/pagination) +- [Row Pinning](../framework/react/guide/row-pinning) +- [Row Selection](../framework/react/guide/row-selection) +- [Sorting](../framework/react/guide/sorting) +- [Virtualization](../framework/react/guide/virtualization) + +# Preact + +- [Column Ordering](../framework/preact/guide/column-ordering) +- [Column Pinning](../framework/preact/guide/column-pinning) +- [Column Sizing](../framework/preact/guide/column-sizing) +- [Column Resizing](../framework/preact/guide/column-resizing) +- [Column Visibility](../framework/preact/guide/column-visibility) +- [Expanding](../framework/preact/guide/expanding) +- [Column Faceting](../framework/preact/guide/column-faceting) +- [Column Filtering](../framework/preact/guide/column-filtering) +- [Global Faceting](../framework/preact/guide/global-faceting) +- [Global Filtering](../framework/preact/guide/global-filtering) +- [Grouping](../framework/preact/guide/grouping) +- [Pagination](../framework/preact/guide/pagination) +- [Row Pinning](../framework/preact/guide/row-pinning) +- [Row Selection](../framework/preact/guide/row-selection) +- [Sorting](../framework/preact/guide/sorting) +- [Virtualization](../framework/preact/guide/virtualization) + +# Solid + +- [Column Ordering](../framework/solid/guide/column-ordering) +- [Column Pinning](../framework/solid/guide/column-pinning) +- [Column Sizing](../framework/solid/guide/column-sizing) +- [Column Resizing](../framework/solid/guide/column-resizing) +- [Column Visibility](../framework/solid/guide/column-visibility) +- [Expanding](../framework/solid/guide/expanding) +- [Column Faceting](../framework/solid/guide/column-faceting) +- [Column Filtering](../framework/solid/guide/column-filtering) +- [Global Faceting](../framework/solid/guide/global-faceting) +- [Global Filtering](../framework/solid/guide/global-filtering) +- [Grouping](../framework/solid/guide/grouping) +- [Pagination](../framework/solid/guide/pagination) +- [Row Pinning](../framework/solid/guide/row-pinning) +- [Row Selection](../framework/solid/guide/row-selection) +- [Sorting](../framework/solid/guide/sorting) +- [Virtualization](../framework/solid/guide/virtualization) + +# Svelte + +- [Column Ordering](../framework/svelte/guide/column-ordering) +- [Column Pinning](../framework/svelte/guide/column-pinning) +- [Column Sizing](../framework/svelte/guide/column-sizing) +- [Column Resizing](../framework/svelte/guide/column-resizing) +- [Column Visibility](../framework/svelte/guide/column-visibility) +- [Expanding](../framework/svelte/guide/expanding) +- [Column Faceting](../framework/svelte/guide/column-faceting) +- [Column Filtering](../framework/svelte/guide/column-filtering) +- [Global Faceting](../framework/svelte/guide/global-faceting) +- [Global Filtering](../framework/svelte/guide/global-filtering) +- [Grouping](../framework/svelte/guide/grouping) +- [Pagination](../framework/svelte/guide/pagination) +- [Row Pinning](../framework/svelte/guide/row-pinning) +- [Row Selection](../framework/svelte/guide/row-selection) +- [Sorting](../framework/svelte/guide/sorting) +- [Virtualization](../framework/svelte/guide/virtualization) + +# Vue + +- [Column Ordering](../framework/vue/guide/column-ordering) +- [Column Pinning](../framework/vue/guide/column-pinning) +- [Column Sizing](../framework/vue/guide/column-sizing) +- [Column Resizing](../framework/vue/guide/column-resizing) +- [Column Visibility](../framework/vue/guide/column-visibility) +- [Expanding](../framework/vue/guide/expanding) +- [Column Faceting](../framework/vue/guide/column-faceting) +- [Column Filtering](../framework/vue/guide/column-filtering) +- [Global Faceting](../framework/vue/guide/global-faceting) +- [Global Filtering](../framework/vue/guide/global-filtering) +- [Grouping](../framework/vue/guide/grouping) +- [Pagination](../framework/vue/guide/pagination) +- [Row Pinning](../framework/vue/guide/row-pinning) +- [Row Selection](../framework/vue/guide/row-selection) +- [Sorting](../framework/vue/guide/sorting) +- [Virtualization](../framework/vue/guide/virtualization) + +# Angular + +- [Column Ordering](../framework/angular/guide/column-ordering) +- [Column Pinning](../framework/angular/guide/column-pinning) +- [Column Sizing](../framework/angular/guide/column-sizing) +- [Column Resizing](../framework/angular/guide/column-resizing) +- [Column Visibility](../framework/angular/guide/column-visibility) +- [Expanding](../framework/angular/guide/expanding) +- [Column Faceting](../framework/angular/guide/column-faceting) +- [Column Filtering](../framework/angular/guide/column-filtering) +- [Global Faceting](../framework/angular/guide/global-faceting) +- [Global Filtering](../framework/angular/guide/global-filtering) +- [Grouping](../framework/angular/guide/grouping) +- [Pagination](../framework/angular/guide/pagination) +- [Row Pinning](../framework/angular/guide/row-pinning) +- [Row Selection](../framework/angular/guide/row-selection) +- [Sorting](../framework/angular/guide/sorting) +- [Virtualization](../framework/angular/guide/virtualization) + +# Lit + +- [Column Ordering](../framework/lit/guide/column-ordering) +- [Column Pinning](../framework/lit/guide/column-pinning) +- [Column Sizing](../framework/lit/guide/column-sizing) +- [Column Resizing](../framework/lit/guide/column-resizing) +- [Column Visibility](../framework/lit/guide/column-visibility) +- [Expanding](../framework/lit/guide/expanding) +- [Column Faceting](../framework/lit/guide/column-faceting) +- [Column Filtering](../framework/lit/guide/column-filtering) +- [Global Faceting](../framework/lit/guide/global-faceting) +- [Global Filtering](../framework/lit/guide/global-filtering) +- [Grouping](../framework/lit/guide/grouping) +- [Pagination](../framework/lit/guide/pagination) +- [Row Pinning](../framework/lit/guide/row-pinning) +- [Row Selection](../framework/lit/guide/row-selection) +- [Sorting](../framework/lit/guide/sorting) +- [Virtualization](../framework/lit/guide/virtualization) + +# Vanilla + +- Column Ordering +- Column Pinning +- Column Sizing +- Column Resizing +- Column Visibility +- Expanding +- Column Faceting +- Column Filtering +- Global Faceting +- Global Filtering +- Grouping +- [Pagination](../framework/vanilla/guide/pagination) +- Row Pinning +- Row Selection +- [Sorting](../framework/vanilla/guide/sorting) +- Virtualization + diff --git a/docs/guide/filters.md b/docs/guide/filters.md index 8d10bda84e..6f4445229f 100644 --- a/docs/guide/filters.md +++ b/docs/guide/filters.md @@ -6,8 +6,61 @@ title: Filters Guide The filter guides are now split into multiple guides: -- [Column Filtering](./column-filtering) -- [Global Filtering](./global-filtering) -- [Fuzzy Filtering](./fuzzy-filtering) -- [Column Faceting](./column-faceting) -- [Global Faceting](./global-faceting) \ No newline at end of file + + +# React + +- [Column Filtering](../framework/react/guide/column-filtering) +- [Global Filtering](../framework/react/guide/global-filtering) +- [Fuzzy Filtering](../framework/react/guide/fuzzy-filtering) +- [Column Faceting](../framework/react/guide/column-faceting) +- [Global Faceting](../framework/react/guide/global-faceting) + +# Preact + +- [Column Filtering](../framework/preact/guide/column-filtering) +- [Global Filtering](../framework/preact/guide/global-filtering) +- [Fuzzy Filtering](../framework/preact/guide/fuzzy-filtering) +- [Column Faceting](../framework/preact/guide/column-faceting) +- [Global Faceting](../framework/preact/guide/global-faceting) + +# Solid + +- [Column Filtering](../framework/solid/guide/column-filtering) +- [Global Filtering](../framework/solid/guide/global-filtering) +- [Fuzzy Filtering](../framework/solid/guide/fuzzy-filtering) +- [Column Faceting](../framework/solid/guide/column-faceting) +- [Global Faceting](../framework/solid/guide/global-faceting) + +# Svelte + +- [Column Filtering](../framework/svelte/guide/column-filtering) +- [Global Filtering](../framework/svelte/guide/global-filtering) +- [Fuzzy Filtering](../framework/svelte/guide/fuzzy-filtering) +- [Column Faceting](../framework/svelte/guide/column-faceting) +- [Global Faceting](../framework/svelte/guide/global-faceting) + +# Vue + +- [Column Filtering](../framework/vue/guide/column-filtering) +- [Global Filtering](../framework/vue/guide/global-filtering) +- [Fuzzy Filtering](../framework/vue/guide/fuzzy-filtering) +- [Column Faceting](../framework/vue/guide/column-faceting) +- [Global Faceting](../framework/vue/guide/global-faceting) + +# Angular + +- [Column Filtering](../framework/angular/guide/column-filtering) +- [Global Filtering](../framework/angular/guide/global-filtering) +- [Fuzzy Filtering](../framework/angular/guide/fuzzy-filtering) +- [Column Faceting](../framework/angular/guide/column-faceting) +- [Global Faceting](../framework/angular/guide/global-faceting) + +# Lit + +- [Column Filtering](../framework/lit/guide/column-filtering) +- [Global Filtering](../framework/lit/guide/global-filtering) +- [Fuzzy Filtering](../framework/lit/guide/fuzzy-filtering) +- [Column Faceting](../framework/lit/guide/column-faceting) +- [Global Faceting](../framework/lit/guide/global-faceting) + diff --git a/docs/guide/global-faceting.md b/docs/guide/global-faceting.md deleted file mode 100644 index 38a4aab1a5..0000000000 --- a/docs/guide/global-faceting.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: Global Faceting Guide ---- - -## Examples - -Want to skip to the implementation? Check out these examples: - - - -# React - -- [Faceted Filters](../framework/react/examples/filters-faceted) - -# Preact - -- [Faceted Filters](../framework/preact/examples/filters-faceted) - -# Solid - -- [Faceted Filters](../framework/solid/examples/filters-faceted) - -# Svelte - -- [Faceted Filters](../framework/svelte/examples/filters-faceted) - -# Vue - -- [Faceted Filters](../framework/vue/examples/filters-faceted) - -# Lit - -- [Faceted Filters](../framework/lit/examples/filters-faceted) - - - -## Global Faceting Guide - -Global Faceting allows you to generate lists of values for all columns from the table's data. For example, a list of unique values in a table can be generated from all rows in all columns to be used as search suggestions in an autocomplete filter component. Or, a tuple of minimum and maximum values can be generated from a table of numbers to be used as a range for a range slider filter component. - -### Global Faceting Row Models - -In order to use any of the global faceting features, add the appropriate faceted row models to your `rowModels`: - -```ts -import { - useTable, - tableFeatures, - createFacetedRowModel, - createFacetedMinMaxValues, - createFacetedUniqueValues, -} from '@tanstack/react-table' - -const features = tableFeatures({}) // add globalFilteringFeature if using global filtering - -const table = useTable({ - features, - rowModels: { - facetedRowModel: createFacetedRowModel(), // required (other faceting methods depend on this) - facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values - facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values - }, - // other options... -}) -``` - -### Use Global Faceted Row Models - -Once you have included the appropriate row models in your table options, you will be able to use the faceting table 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(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 Global (Server-Side) Faceting - -If 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. You can use the getGlobalFacetedUniqueValues and getGlobalFacetedMinMaxValues table options to resolve the faceted values from the server-side. - -```ts -const facetingQuery = useQuery( - 'faceting', - async () => { - const response = await fetch('/api/faceting'); - return response.json(); - }, - { - onSuccess: (data) => { - table.getGlobalFacetedUniqueValues = () => data.uniqueValues; - table.getGlobalFacetedMinMaxValues = () => data.minMaxValues; - }, - } -); -``` - -In this example, we use the `useQuery` hook from `react-query` to fetch faceting data from the server. Once the data is fetched, we set the `getGlobalFacetedUniqueValues` and `getGlobalFacetedMinMaxValues` table options to return the faceted values from the server response. This will allow the table to use the server-side faceting data for generating autocomplete suggestions and range filters. diff --git a/docs/guide/headers.md b/docs/guide/headers.md index f8f5145823..7b1adaec56 100644 --- a/docs/guide/headers.md +++ b/docs/guide/headers.md @@ -65,7 +65,7 @@ Every header stores a reference to its parent [column](./columns) object and its ### More Header APIs -Headers have a few more useful APIs attached to them that are useful for interacting with the table state. Most of them relate to the column sizing and resizing features. See the [Column Sizing Guide](./column-sizing) and [Column Resizing Guide](./column-resizing) for more information. +Headers have a few more useful APIs attached to them that are useful for interacting with the table state. Most of them relate to the column sizing and resizing features. See the [Column Sizing Guide](../framework/react/guide/column-sizing) and [Column Resizing Guide](../framework/react/guide/column-resizing) for more information. ### Header Rendering diff --git a/docs/guide/pinning.md b/docs/guide/pinning.md index aa8c116291..dc5488658c 100644 --- a/docs/guide/pinning.md +++ b/docs/guide/pinning.md @@ -6,5 +6,40 @@ title: Pinning Guide Pinning is split into 2 different feature guides: -- [Column Pinning](./column-pinning) -- [Row Pinning](./row-pinning) \ No newline at end of file + + +# React + +- [Column Pinning](../framework/react/guide/column-pinning) +- [Row Pinning](../framework/react/guide/row-pinning) + +# Preact + +- [Column Pinning](../framework/preact/guide/column-pinning) +- [Row Pinning](../framework/preact/guide/row-pinning) + +# Solid + +- [Column Pinning](../framework/solid/guide/column-pinning) +- [Row Pinning](../framework/solid/guide/row-pinning) + +# Svelte + +- [Column Pinning](../framework/svelte/guide/column-pinning) +- [Row Pinning](../framework/svelte/guide/row-pinning) + +# Vue + +- [Column Pinning](../framework/vue/guide/column-pinning) +- [Row Pinning](../framework/vue/guide/row-pinning) + +# Angular + +- [Column Pinning](../framework/angular/guide/column-pinning) +- [Row Pinning](../framework/angular/guide/row-pinning) + +# Lit + +- [Column Pinning](../framework/lit/guide/column-pinning) +- [Row Pinning](../framework/lit/guide/row-pinning) + diff --git a/docs/guide/row-pinning.md b/docs/guide/row-pinning.md deleted file mode 100644 index b5e3fa3c6e..0000000000 --- a/docs/guide/row-pinning.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Row Pinning Guide ---- - -## Examples - -Want to skip to the implementation? Check out these examples: - - - -# React - -- [Row Pinning](../framework/react/examples/row-pinning) - -# Preact - -- [Row Pinning](../framework/preact/examples/row-pinning) - -# Solid - -- [Row Pinning](../framework/solid/examples/row-pinning) - -# Svelte - -- [Row Pinning](../framework/svelte/examples/row-pinning) - -# Vue - -- [Row Pinning](../framework/vue/examples/row-pinning) - -# Angular - -- [Row Pinning](../framework/angular/examples/row-pinning) - -# Lit - -- [Row Pinning](../framework/lit/examples/row-pinning) - - - -## Row Pinning Guide - -There are 2 table features that can reorder rows, which happen in the following order: - -1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. -2. [Sorting](./sorting) diff --git a/docs/guide/rows.md b/docs/guide/rows.md index 78e106e70a..9228fe654e 100644 --- a/docs/guide/rows.md +++ b/docs/guide/rows.md @@ -83,7 +83,7 @@ const firstName = row.original.firstName // { firstName: 'John', lastName: 'Doe' ### Sub Rows -If you are using either grouping or expanding features, your rows may contain sub-rows or parent row references. This is discussed in much more detail in the [Expanding Guide](./expanding), but here is a quick overview of useful properties and methods for working with sub-rows. +If you are using either grouping or expanding features, your rows may contain sub-rows or parent row references. This is discussed in much more detail in the [Expanding Guide](../framework/react/guide/expanding), but here is a quick overview of useful properties and methods for working with sub-rows. - `row.subRows`: An array of sub-rows for the row. - `row.depth`: The depth of the row (if nested or grouped) relative to the root row array. 0 for root level rows, 1 for child rows, 2 for grandchild rows, etc. diff --git a/docs/guide/virtualization.md b/docs/guide/virtualization.md deleted file mode 100644 index 381a859585..0000000000 --- a/docs/guide/virtualization.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Virtualization Guide ---- - -## Examples - -Want to skip to the implementation? Check out these examples: - - - -# React - -- [Virtualized Columns](../framework/react/examples/virtualized-columns) -- [Virtualized Rows](../framework/react/examples/virtualized-rows) -- [Virtualized Infinite Scrolling](../framework/react/examples/virtualized-infinite-scrolling) - -# Solid - -- [Virtualized Columns](../framework/solid/examples/virtualized-columns) -- [Virtualized Rows](../framework/solid/examples/virtualized-rows) -- [Virtualized Infinite Scrolling](../framework/solid/examples/virtualized-infinite-scrolling) - -# Svelte - -- [Virtualized Columns](../framework/svelte/examples/virtualized-columns) -- [Virtualized Rows](../framework/svelte/examples/virtualized-rows) -- [Virtualized Infinite Scrolling](../framework/svelte/examples/virtualized-infinite-scrolling) - -# Vue - -- [Virtualized Columns](../framework/vue/examples/virtualized-columns) -- [Virtualized Rows](../framework/vue/examples/virtualized-rows) -- [Virtualized Infinite Scrolling](../framework/vue/examples/virtualized-infinite-scrolling) - -# Angular - -- [Virtualized Columns](../framework/angular/examples/virtualized-columns) -- [Virtualized Rows](../framework/angular/examples/virtualized-rows) -- [Virtualized Infinite Scrolling](../framework/angular/examples/virtualized-infinite-scrolling) - -# Lit - -- [Virtualized Columns](../framework/lit/examples/virtualized-columns) -- [Virtualized Rows](../framework/lit/examples/virtualized-rows) -- [Virtualized Infinite Scrolling](../framework/lit/examples/virtualized-infinite-scrolling) - - - -Also see the [TanStack Virtual table example](https://tanstack.com/virtual/latest/docs/framework/react/examples/table). - -## Virtualization Guide - -The TanStack Table packages do not come with any virtualization APIs or features built-in, but TanStack Table can easily work with other virtualization libraries like [react-window](https://www.npmjs.com/package/react-window) or TanStack's own [TanStack Virtual](https://tanstack.com/virtual/v3). This guide will show some strategies for using TanStack Table with TanStack Virtual. diff --git a/docs/overview.md b/docs/overview.md index e3026346e9..498d63d482 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -39,25 +39,149 @@ The table core uses the following abstractions, commonly exposed by adapters: TanStack Table will help you build just about any type of table you can imagine. It has built-in state and APIs for the following features: -- [Column Faceting](./guide/column-faceting) - List unique lists of column values or min/max values for a column -- [Column Filtering](./guide/column-filtering) - Filter rows based on search values for a column -- [Column Grouping](./guide/grouping) - Group columns together, run aggregations, and more -- [Column Ordering](./guide/column-ordering) - Dynamically change the order of columns -- [Column Pinning](./guide/column-pinning) - Pin (Freeze) columns to the left or right of the table -- [Column Sizing](./guide/column-sizing) - Dynamically change the size of columns (column resizing handles) -- [Column Visibility](./guide/column-visibility) - Hide/show columns -- [Global Faceting](./guide/global-faceting) - List unique lists of column values or min/max values for the entire table -- [Global Filtering](./guide/global-filtering) - Filter rows based on search values for the entire table -- [Row Expanding](./guide/expanding) - Expand/collapse rows (sub-rows) -- [Row Pagination](./guide/pagination) - Paginate rows -- [Row Pinning](./guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table -- [Row Selection](./guide/row-selection) - Select/deselect rows (checkboxes) -- [Row Sorting](./guide/sorting) - Sort rows by column values + + +# React + +- [Column Faceting](./framework/react/guide/column-faceting) - List unique lists of column values or min/max values for a column +- [Column Filtering](./framework/react/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/react/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/react/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/react/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Sizing](./framework/react/guide/column-sizing) - Dynamically change the size of columns (column resizing handles) +- [Column Visibility](./framework/react/guide/column-visibility) - Hide/show columns +- [Global Faceting](./framework/react/guide/global-faceting) - List unique lists of column values or min/max values for the entire table +- [Global Filtering](./framework/react/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/react/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/react/guide/pagination) - Paginate rows +- [Row Pinning](./framework/react/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/react/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/react/guide/sorting) - Sort rows by column values + +# Preact + +- [Column Faceting](./framework/preact/guide/column-faceting) - List unique lists of column values or min/max values for a column +- [Column Filtering](./framework/preact/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/preact/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/preact/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/preact/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Sizing](./framework/preact/guide/column-sizing) - Dynamically change the size of columns (column resizing handles) +- [Column Visibility](./framework/preact/guide/column-visibility) - Hide/show columns +- [Global Faceting](./framework/preact/guide/global-faceting) - List unique lists of column values or min/max values for the entire table +- [Global Filtering](./framework/preact/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/preact/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/preact/guide/pagination) - Paginate rows +- [Row Pinning](./framework/preact/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/preact/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/preact/guide/sorting) - Sort rows by column values + +# Solid + +- [Column Faceting](./framework/solid/guide/column-faceting) - List unique lists of column values or min/max values for a column +- [Column Filtering](./framework/solid/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/solid/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/solid/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/solid/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Sizing](./framework/solid/guide/column-sizing) - Dynamically change the size of columns (column resizing handles) +- [Column Visibility](./framework/solid/guide/column-visibility) - Hide/show columns +- [Global Faceting](./framework/solid/guide/global-faceting) - List unique lists of column values or min/max values for the entire table +- [Global Filtering](./framework/solid/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/solid/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/solid/guide/pagination) - Paginate rows +- [Row Pinning](./framework/solid/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/solid/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/solid/guide/sorting) - Sort rows by column values + +# Svelte + +- [Column Faceting](./framework/svelte/guide/column-faceting) - List unique lists of column values or min/max values for a column +- [Column Filtering](./framework/svelte/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/svelte/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/svelte/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/svelte/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Sizing](./framework/svelte/guide/column-sizing) - Dynamically change the size of columns (column resizing handles) +- [Column Visibility](./framework/svelte/guide/column-visibility) - Hide/show columns +- [Global Faceting](./framework/svelte/guide/global-faceting) - List unique lists of column values or min/max values for the entire table +- [Global Filtering](./framework/svelte/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/svelte/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/svelte/guide/pagination) - Paginate rows +- [Row Pinning](./framework/svelte/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/svelte/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/svelte/guide/sorting) - Sort rows by column values + +# Vue + +- [Column Faceting](./framework/vue/guide/column-faceting) - List unique lists of column values or min/max values for a column +- [Column Filtering](./framework/vue/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/vue/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/vue/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/vue/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Sizing](./framework/vue/guide/column-sizing) - Dynamically change the size of columns (column resizing handles) +- [Column Visibility](./framework/vue/guide/column-visibility) - Hide/show columns +- [Global Faceting](./framework/vue/guide/global-faceting) - List unique lists of column values or min/max values for the entire table +- [Global Filtering](./framework/vue/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/vue/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/vue/guide/pagination) - Paginate rows +- [Row Pinning](./framework/vue/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/vue/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/vue/guide/sorting) - Sort rows by column values + +# Angular + +- [Column Faceting](./framework/angular/guide/column-faceting) - List unique lists of column values or min/max values for a column +- [Column Filtering](./framework/angular/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/angular/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/angular/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/angular/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Sizing](./framework/angular/guide/column-sizing) - Dynamically change the size of columns (column resizing handles) +- [Column Visibility](./framework/angular/guide/column-visibility) - Hide/show columns +- [Global Faceting](./framework/angular/guide/global-faceting) - List unique lists of column values or min/max values for the entire table +- [Global Filtering](./framework/angular/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/angular/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/angular/guide/pagination) - Paginate rows +- [Row Pinning](./framework/angular/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/angular/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/angular/guide/sorting) - Sort rows by column values + +# Lit + +- [Column Faceting](./framework/lit/guide/column-faceting) - List unique lists of column values or min/max values for a column +- [Column Filtering](./framework/lit/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/lit/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/lit/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/lit/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Sizing](./framework/lit/guide/column-sizing) - Dynamically change the size of columns (column resizing handles) +- [Column Visibility](./framework/lit/guide/column-visibility) - Hide/show columns +- [Global Faceting](./framework/lit/guide/global-faceting) - List unique lists of column values or min/max values for the entire table +- [Global Filtering](./framework/lit/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/lit/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/lit/guide/pagination) - Paginate rows +- [Row Pinning](./framework/lit/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/lit/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/lit/guide/sorting) - Sort rows by column values + +# Vanilla + +- Column Faceting - List unique lists of column values or min/max values for a column +- Column Filtering - Filter rows based on search values for a column +- Column Grouping - Group columns together, run aggregations, and more +- Column Ordering - Dynamically change the order of columns +- Column Pinning - Pin (Freeze) columns to the left or right of the table +- Column Sizing - Dynamically change the size of columns (column resizing handles) +- Column Visibility - Hide/show columns +- Global Faceting - List unique lists of column values or min/max values for the entire table +- Global Filtering - Filter rows based on search values for the entire table +- Row Expanding - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/vanilla/guide/pagination) - Paginate rows +- Row Pinning - Pin (Freeze) rows to the top or bottom of the table +- Row Selection - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/vanilla/guide/sorting) - Sort rows by column values + These are just some of the capabilities that you can build with TanStack Table. There are many more features that are possible with TanStack Table that you can add along-side the built-in features. -[Virtualization](./guide/virtualization) is an example of a feature that is not built-in to TanStack Table, but can be achieved by using another library (like [TanStack Virtual](https://tanstack.com/virtual/v3)) and adding it along-side your other table rendering logic. +[Virtualization](./framework/react/guide/virtualization) is an example of a feature that is not built-in to TanStack Table, but can be achieved by using another library (like [TanStack Virtual](https://tanstack.com/virtual/v3)) and adding it along-side your other table rendering logic. -TanStack Table also supports [Custom Features](./guide/custom-features) (plugins) that you can use to modify the table instance to add your own custom logic to the table in a more integrated way. +TanStack Table also supports [Custom Features](./framework/react/guide/custom-features) (plugins) that you can use to modify the table instance to add your own custom logic to the table in a more integrated way. And of course, you can just write your own state and hooks to add whatever other features you want for your table. The features from the TanStack Table core are just a solid foundation to build on, with a large focus on performance and DX. From ab4208b94f8871131a330f160d0b986a4b98d66f Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Tue, 9 Jun 2026 12:51:30 -0500 Subject: [PATCH 2/2] docs: add more migration guides --- docs/config.json | 15 +- docs/framework/angular/guide/migrating.md | 12 - docs/framework/angular/guide/pagination.md | 2 - docs/framework/lit/guide/migrating.md | 639 +++++++++++++++++++ docs/framework/lit/guide/pagination.md | 2 - docs/framework/preact/guide/migrating.md | 631 +++++++++++++++++++ docs/framework/preact/guide/pagination.md | 2 - docs/framework/react/guide/migrating.md | 47 +- docs/framework/react/guide/pagination.md | 2 - docs/framework/solid/guide/migrating.md | 599 ++++++++++++++++++ docs/framework/solid/guide/pagination.md | 2 - docs/framework/svelte/guide/migrating.md | 675 +++++++++++++++++++++ docs/framework/svelte/guide/pagination.md | 2 - docs/framework/vanilla/guide/pagination.md | 2 - docs/framework/vue/guide/migrating.md | 618 +++++++++++++++++++ docs/framework/vue/guide/pagination.md | 2 - 16 files changed, 3199 insertions(+), 53 deletions(-) create mode 100644 docs/framework/lit/guide/migrating.md create mode 100644 docs/framework/preact/guide/migrating.md create mode 100644 docs/framework/solid/guide/migrating.md create mode 100644 docs/framework/svelte/guide/migrating.md create mode 100644 docs/framework/vue/guide/migrating.md diff --git a/docs/config.json b/docs/config.json index d9965d0e3f..a32fdf8812 100644 --- a/docs/config.json +++ b/docs/config.json @@ -27,7 +27,8 @@ { "label": "lit", "children": [ - { "label": "Lit Table Adapter", "to": "framework/lit/lit-table" } + { "label": "Lit Table Adapter", "to": "framework/lit/lit-table" }, + { "label": "Migrating to V9", "to": "framework/lit/guide/migrating" } ] }, { @@ -41,25 +42,29 @@ { "label": "preact", "children": [ - { "label": "Preact Table Adapter", "to": "framework/preact/preact-table" } + { "label": "Preact Table Adapter", "to": "framework/preact/preact-table" }, + { "label": "Migrating to V9", "to": "framework/preact/guide/migrating" } ] }, { "label": "solid", "children": [ - { "label": "Solid Table Adapter", "to": "framework/solid/solid-table" } + { "label": "Solid Table Adapter", "to": "framework/solid/solid-table" }, + { "label": "Migrating to V9", "to": "framework/solid/guide/migrating" } ] }, { "label": "svelte", "children": [ - { "label": "Svelte Table Adapter", "to": "framework/svelte/svelte-table" } + { "label": "Svelte Table Adapter", "to": "framework/svelte/svelte-table" }, + { "label": "Migrating to V9", "to": "framework/svelte/guide/migrating" } ] }, { "label": "vue", "children": [ - { "label": "Vue Table Adapter", "to": "framework/vue/vue-table" } + { "label": "Vue Table Adapter", "to": "framework/vue/vue-table" }, + { "label": "Migrating to V9", "to": "framework/vue/guide/migrating" } ] }, { diff --git a/docs/framework/angular/guide/migrating.md b/docs/framework/angular/guide/migrating.md index 9ddbc684f6..b3eabfda9f 100644 --- a/docs/framework/angular/guide/migrating.md +++ b/docs/framework/angular/guide/migrating.md @@ -30,18 +30,6 @@ While v9 is a significant upgrade, **you don't have to adopt everything at once* The main change is **how you define a table** with the Angular adapter — specifically the new `features` and `rowModels` options. ---- - -## Quick Legacy Migration - -Angular does **not** ship a legacy API. - -If you're migrating an Angular project from TanStack Table v8 to v9, you will migrate directly to the v9 Angular adapter APIs (`injectTable`, `features`, and `rowModels`). - ---- - -The rest of this guide focuses on migrating to the full v9 API and taking advantage of its features. - ## Core Breaking Changes ### Entrypoint Change diff --git a/docs/framework/angular/guide/pagination.md b/docs/framework/angular/guide/pagination.md index ddaf846dac..8f31474d36 100644 --- a/docs/framework/angular/guide/pagination.md +++ b/docs/framework/angular/guide/pagination.md @@ -80,8 +80,6 @@ readonly table = injectTable(() => ({ }) ``` -> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. - ### Manual Server-Side Pagination If you decide that you need to use server-side pagination, here is how you can implement it. diff --git a/docs/framework/lit/guide/migrating.md b/docs/framework/lit/guide/migrating.md new file mode 100644 index 0000000000..4fcdc4ec51 --- /dev/null +++ b/docs/framework/lit/guide/migrating.md @@ -0,0 +1,639 @@ +--- +title: Migrating to TanStack Table v9 (Lit) +--- + +## What's New in TanStack Table v9 + +TanStack Table v9 is a major release with explicit feature registration, row model registration, and a new atom-backed state model. The Lit adapter wraps those APIs in a `ReactiveController`. + +> The Lit adapter may change during the v9 beta cycle. This guide documents the current local v9 API and avoids speculating about future beta changes. + +### 1. Tree-shaking + +- **Features are tree-shakeable**: register only the table features you use. +- **Row models are explicit**: move root `get*RowModel` options into the `rowModels` object. +- **Function registries moved to factories**: row model factories receive `sortFns`, `filterFns`, and `aggregationFns` directly. + +### 2. State Management + +- **Uses TanStack Store**: state is backed by TanStack Store atoms. +- **Uses a `TableController`**: the controller wires table store changes to Lit host updates. +- **Per-slice state**: registered features expose their state through `table.atoms`. +- **Default full-state selection**: `tableController.table(options)` exposes the full registered state on `table.state`; pass a selector only when you want to narrow it. + +### 3. Composability + +- **`tableOptions()`**: compose reusable option fragments. +- **`createTableHook()`**: define shared Lit table factories with pre-bound features, row models, defaults, and render helpers. + +### The Good News: Most Table Logic Is Still Familiar + +- Column definitions keep the same basic `accessorKey`, `accessorFn`, `header`, `cell`, and `footer` shapes. +- Feature APIs like `table.nextPage()`, `column.toggleSorting()`, and `row.toggleSelected()` remain the preferred way to update state. +- Lit templates still render header groups, rows, and cells from the table instance. + +The main migration is replacing the v8 controller-with-options-thunk with a v9 controller plus `.table(options, selector?)`, then moving feature and row-model setup into the v9 shape. + +--- + +## Core Breaking Changes + +### Controller Construction Change + +```ts +// v8 +private tableController = new TableController(this, () => ({ + columns, + data: this.data, +})) + +protected render() { + const table = this.tableController.table + return html`...` +} + +// v9 +private tableController = new TableController(this) + +protected render() { + const table = this.tableController.table({ + features, + rowModels, + columns, + data: this.data, + }) + + return html`...` +} +``` + +The v9 controller takes the host only. Pass options to `.table(...)` during render. + +### New Required Options: `features` and `rowModels` + +```ts +// v8 +import { + TableController, + getCoreRowModel, +} from '@tanstack/lit-table' + +private tableController = new TableController(this, () => ({ + columns, + data: this.data, + getCoreRowModel: getCoreRowModel(), +})) + +// v9 +import { + TableController, + tableFeatures, +} from '@tanstack/lit-table' + +const features = tableFeatures({}) + +private tableController = new TableController(this) + +protected render() { + const table = this.tableController.table({ + features, + rowModels: {}, // core row model is automatic + columns, + data: this.data, + }) +} +``` + +--- + +## The `features` Option + +Features control which APIs, options, and state slices exist on the table. + +### Importing Individual Features + +```ts +import { + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + tableFeatures, +} from '@tanstack/lit-table' + +const features = tableFeatures({ + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, +}) +``` + +If a feature is not registered, its APIs and state slice are not available. + +### Using `stockFeatures` for v8-like Behavior + +`stockFeatures` is useful for early migration before you audit feature usage. + +```ts +import { stockFeatures } from '@tanstack/lit-table' + +const table = this.tableController.table({ + features: stockFeatures, + rowModels, + columns, + data: this.data, +}) +``` + +Use it as a temporary migration shortcut. Explicit feature registration is the production target. + +### Available Features + +| Feature | Import Name | +|---|---| +| Column Filtering | `columnFilteringFeature` | +| Global Filtering | `globalFilteringFeature` | +| Row Sorting | `rowSortingFeature` | +| Row Pagination | `rowPaginationFeature` | +| Row Selection | `rowSelectionFeature` | +| Row Expanding | `rowExpandingFeature` | +| Row Pinning | `rowPinningFeature` | +| Column Pinning | `columnPinningFeature` | +| Column Visibility | `columnVisibilityFeature` | +| Column Ordering | `columnOrderingFeature` | +| Column Sizing | `columnSizingFeature` | +| Column Resizing | `columnResizingFeature` | +| Column Grouping | `columnGroupingFeature` | +| Column Faceting | `columnFacetingFeature` | +| Global Faceting | `globalFacetingFeature` | + +--- + +## The `rowModels` Option + +Row models now live under `rowModels`. + +### Migration Mapping + +| v8 Option | v9 `rowModels` Key | v9 Factory Function | +|---|---|---| +| `getCoreRowModel()` | (automatic) | Not needed | +| `getFilteredRowModel()` | `filteredRowModel` | `createFilteredRowModel(filterFns)` | +| `getSortedRowModel()` | `sortedRowModel` | `createSortedRowModel(sortFns)` | +| `getPaginationRowModel()` | `paginatedRowModel` | `createPaginatedRowModel()` | +| `getExpandedRowModel()` | `expandedRowModel` | `createExpandedRowModel()` | +| `getGroupedRowModel()` | `groupedRowModel` | `createGroupedRowModel(aggregationFns)` | +| `getFacetedRowModel()` | `facetedRowModel` | `createFacetedRowModel()` | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues` | `createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues` | `createFacetedUniqueValues()` | + +### Full Migration Example + +```ts +// v8 +import { + TableController, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + filterFns, + sortingFns, +} from '@tanstack/lit-table' + +private tableController = new TableController(this, () => ({ + columns, + data: this.data, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + filterFns, + sortingFns, +})) +``` + +```ts +// v9 +import { + TableController, + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFns, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/lit-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, +}) + +const rowModels = { + filteredRowModel: createFilteredRowModel(filterFns), + sortedRowModel: createSortedRowModel(sortFns), + paginatedRowModel: createPaginatedRowModel(), +} + +private tableController = new TableController(this) + +protected render() { + const table = this.tableController.table({ + features, + rowModels, + columns, + data: this.data, + }) + + return html`...` +} +``` + +--- + +## State Management Changes + +Lit v9 table state is atom-backed and controller-driven. The controller requests host updates when table store or option store changes. + +| Surface | Use | +|---|---| +| `table.state` | Full registered table state by default, or selected state from the second argument to `tableController.table(...)`. | +| `table.store.state` | Current full table state snapshot. | +| `table.atoms..get()` | Narrow current-value read for one state slice. | +| `table.Subscribe` | Template helper for selecting table state while rendering. | +| `table.baseAtoms.` | Internal writable atoms. Prefer feature APIs or external atoms. | + +### Accessing State + +```ts +// v8 +const sorting = table.getState().sorting + +// v9: full snapshot +const sorting = table.store.state.sorting + +// v9: narrow atom read +const sorting = table.atoms.sorting.get() +``` + +By default, `table.state` contains the full registered table state. + +```ts +const table = this.tableController.table({ + features, + rowModels, + columns, + data: this.data, +}) + +const { pagination, sorting } = table.state +``` + +Pass a second-argument selector when you want `table.state` to contain only the values that should be selected for render code. + +```ts +const table = this.tableController.table( + { + features, + rowModels, + columns, + data: this.data, + }, + (state) => ({ + pagination: state.pagination, + }), +) + +table.state.pagination +``` + +Passing `(state) => state` is equivalent to the default selector and is no longer necessary. + +### Selecting State with `table.Subscribe` + +```ts +${table.Subscribe({ + selector: (state) => ({ + pagination: state.pagination, + }), + children: ({ pagination }) => html` + Page ${pagination.pageIndex + 1} + `, +})} +``` + +`table.Subscribe` can also accept a `source`, but the current Lit adapter invalidates the host through the table store subscription. Treat source mode as render-time selection convenience. + +### Controlled State + +Use Lit `@state()` fields with per-slice callbacks. + +```ts +import { state } from 'lit/decorators.js' +import type { PaginationState, SortingState } from '@tanstack/lit-table' + +@state() +private sorting: SortingState = [] + +@state() +private pagination: PaginationState = { + pageIndex: 0, + pageSize: 10, +} + +protected render() { + const table = this.tableController.table({ + features, + rowModels, + columns, + data: this.data, + state: { + sorting: this.sorting, + pagination: this.pagination, + }, + onSortingChange: (updater) => { + this.sorting = + updater instanceof Function ? updater(this.sorting) : updater + }, + onPaginationChange: (updater) => { + this.pagination = + updater instanceof Function ? updater(this.pagination) : updater + }, + }) + + return html`...` +} +``` + +The v8-style top-level `onStateChange` callback is gone. Use per-slice callbacks or external atoms. + +### External Atoms + +Use external atoms when the app should own and share state slices outside the table. + +```ts +import { createAtom } from '@tanstack/store' +import type { PaginationState, SortingState } from '@tanstack/lit-table' + +const sortingAtom = createAtom([]) +const paginationAtom = createAtom({ + pageIndex: 0, + pageSize: 10, +}) + +protected render() { + const table = this.tableController.table({ + features, + rowModels, + columns, + data: this.data, + atoms: { + sorting: sortingAtom, + pagination: paginationAtom, + }, + }) + + return html`Page ${paginationAtom.get().pageIndex + 1}` +} +``` + +Do not provide both `atoms.pagination` and `state.pagination`; the atom owns that slice. + +--- + +## Column Helper Changes + +Column helpers and column types now include `TFeatures` first. + +```ts +// v8 +const columnHelper = createColumnHelper() +const columns: ColumnDef[] = [ + columnHelper.accessor('age', { + header: 'Age', + sortingFn: 'alphanumeric', + }), +] + +// v9 +const columnHelper = createColumnHelper() +const columns: Array> = columnHelper.columns([ + columnHelper.accessor('age', { + header: 'Age', + sortFn: 'alphanumeric', + }), +]) +``` + +Use `columnHelper.columns([...])` for better inference across nested columns. + +--- + +## Rendering Changes + +Replace v8 `flexRender(def, context)` calls with `FlexRender({ cell | header | footer })`. + +```ts +// v8 +html`${flexRender(cell.column.columnDef.cell, cell.getContext())}` + +// v9 +html`${FlexRender({ cell })}` +html`${FlexRender({ header })}` +html`${FlexRender({ footer: header })}` +``` + +The table instance also exposes `table.FlexRender`: + +```ts +html`${table.FlexRender({ cell })}` +``` + +--- + +## The `tableOptions()` Utility + +`tableOptions()` helps compose shared table option fragments. + +```ts +import { tableOptions } from '@tanstack/lit-table' + +const baseOptions = tableOptions({ + features, + rowModels, + defaultColumn: { + minSize: 40, + }, +}) + +const table = this.tableController.table({ + ...baseOptions, + columns, + data: this.data, +}) +``` + +--- + +## `createTableHook`: Composable Table Patterns + +`createTableHook` creates shared Lit table helpers with features, row models, and render helpers already bound. + +```ts +import { createTableHook } from '@tanstack/lit-table' + +export const { useAppTable, createAppColumnHelper } = createTableHook({ + features, + rowModels, +}) + +const columnHelper = createAppColumnHelper() + +@customElement('people-table') +class PeopleTable extends LitElement { + private appTable = useAppTable(this, { + columns, + data: this.data, + }) + + protected render() { + const table = this.appTable.table() + return html`...` + } +} +``` + +See the [Composable Tables Guide](./composable-tables.md) for full patterns. + +--- + +## Other Breaking Changes + +### Column Pinning Option Split + +Table-level `enablePinning` split into: + +```ts +enableColumnPinning: true +enableRowPinning: true +``` + +### Column Sizing vs. Column Resizing Split + +Column resizing now has its own feature and state slice. + +```ts +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) +``` + +`columnSizingInfo` became `columnResizing`, and `onColumnSizingInfoChange` became `onColumnResizingChange`. + +### Sorting API Renames + +| v8 | v9 | +|---|---| +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` | `SortFn` | + +### Removed Internal API Prefixes + +Underscore-prefixed APIs that are now public should be called without `_`, such as `row.getAllCellsByColumnId()`. + +--- + +## TypeScript Changes Summary + +### Type Generics + +Use `TFeatures` as the first generic: + +```ts +ColumnDef +Column +Row +Table +``` + +### Using `typeof features` + +```ts +const features = tableFeatures({ + rowSortingFeature, + rowPaginationFeature, +}) + +const columnHelper = createColumnHelper() +``` + +### Using `StockFeatures` + +```ts +import type { StockFeatures } from '@tanstack/lit-table' + +type PersonColumn = ColumnDef +``` + +### `ColumnMeta` Generic Change + +```ts +declare module '@tanstack/lit-table' { + interface ColumnMeta { + align?: 'left' | 'right' + } +} +``` + +### `RowData` Type Restriction + +Prefer explicit object row types: + +```ts +type Person = { + firstName: string + lastName: string + age: number +} +``` + +--- + +## Migration Checklist + +- [ ] Replace controller construction with `new TableController(this)`. +- [ ] Move table options from the controller constructor into `tableController.table(...)`. +- [ ] Add `features: tableFeatures({ ... })`. +- [ ] Move root `get*RowModel` options into `rowModels`. +- [ ] Remove `getCoreRowModel`; the core row model is automatic. +- [ ] Pass `sortFns`, `filterFns`, and `aggregationFns` to row model factories. +- [ ] Rename `sortingFn` to `sortFn`. +- [ ] Add `typeof features` to column helpers and types. +- [ ] Replace `table.getState()` reads with `table.state`, `table.store.state`, or `table.atoms..get()`. +- [ ] Replace top-level `onStateChange` with per-slice callbacks or external atoms. +- [ ] Replace `flexRender(...)` calls with `FlexRender({ cell | header | footer })`. +- [ ] Audit `stockFeatures` before production. + +--- + +## Examples + +- [Basic TableController](../examples/basic-table-controller) +- [Basic App Table](../examples/basic-app-table) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) +- [Sorting](../examples/sorting) +- [Pagination](../examples/pagination) +- [Composable Tables](../examples/composable-tables) diff --git a/docs/framework/lit/guide/pagination.md b/docs/framework/lit/guide/pagination.md index b5dd834d5f..13ae62d3e8 100644 --- a/docs/framework/lit/guide/pagination.md +++ b/docs/framework/lit/guide/pagination.md @@ -89,8 +89,6 @@ const table = this.tableController.table({ }) ``` -> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. - ### Manual Server-Side Pagination If you decide that you need to use server-side pagination, here is how you can implement it. diff --git a/docs/framework/preact/guide/migrating.md b/docs/framework/preact/guide/migrating.md new file mode 100644 index 0000000000..a774293339 --- /dev/null +++ b/docs/framework/preact/guide/migrating.md @@ -0,0 +1,631 @@ +--- +title: Migrating to TanStack Table v9 (Preact) +--- + +## What's New in TanStack Table v9 + +TanStack Table v9 is a major release with a smaller, more explicit table setup. The core table logic is familiar, but the table instance now declares exactly which features, row models, and state subscriptions it needs. + +### 1. Tree-shaking + +- **Features are tree-shakeable**: features are registered explicitly. If a table only needs sorting and pagination, it does not need to ship filtering, grouping, or row selection code. +- **Row models are registered explicitly**: row model factories now live under `rowModels` instead of root `get*RowModel` options. +- **Function registries moved to factories**: row model factories like `createSortedRowModel(sortFns)`, `createFilteredRowModel(filterFns)`, and `createGroupedRowModel(aggregationFns)` receive the function registry they need. This lets unused built-in functions tree-shake. + +### 2. State Management + +- **Uses TanStack Store**: table state is now backed by TanStack Store atoms. +- **Per-slice state atoms**: state slices like `sorting`, `pagination`, and `rowSelection` are separate atoms instead of one monolithic state object. +- **Default full-state subscription, optional narrower selectors**: By default, `useTable` selects all registered table state, so `table.state` contains the full state and the component re-renders when any registered state changes. Pass a narrower selector, use `table.Subscribe`, or use atom selectors when only a smaller part of the UI should re-render. +- **External atoms**: apps can own individual state slices by passing writable atoms through the `atoms` option. + +### 3. Composability + +- **`tableOptions()`**: build type-safe partial table option objects that can be shared and composed. +- **`createTableHook()`**: create app-level Preact table factories with shared features, row models, defaults, and component conventions. + +### The Good News: Most Upgrades Are Opt-in + +- You can start with `stockFeatures` while migrating, then replace it with explicit feature registration. +- `useTable` defaults to v8-style full state subscriptions. Pass a narrower selector only when you want to optimize re-renders. +- Table markup is largely unchanged. Rows, headers, cells, and feature APIs still drive rendering. + +The main migration is changing from the React adapter used through `preact/compat` to the native Preact adapter: `useReactTable` becomes `useTable`, and `get*RowModel` options become `features` plus `rowModels`. + +## Preact v8 Context + +TanStack Table v8 did not have an officially released Preact adapter. If you used TanStack Table in a Preact app on v8, you were most likely using `@tanstack/react-table` through `preact/compat`. + +This guide is for migrating that setup to the native v9 `@tanstack/preact-table` adapter. After this migration, TanStack Table's Preact packages should not be the reason your table code requires `preact/compat`; any remaining compat aliases should come from the rest of your app or other dependencies. + +--- + +## Core Breaking Changes + +### Hook Rename + +```tsx +// v8 / before: Preact app using the React adapter through preact/compat +import { useReactTable } from '@tanstack/react-table' + +const table = useReactTable(options) + +// v9: native Preact adapter +import { useTable } from '@tanstack/preact-table' + +const table = useTable(options) +``` + +### New Required Options: `features` and `rowModels` + +In v9, a table must declare its feature set and row models. + +```tsx +// v8 / before: React adapter through preact/compat +import { getCoreRowModel, useReactTable } from '@tanstack/react-table' + +const table = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), +}) + +// v9 +import { tableFeatures, useTable } from '@tanstack/preact-table' + +const features = tableFeatures({}) + +const table = useTable({ + features, + rowModels: {}, // core row model is automatic + columns, + data, +}) +``` + +Keep `features` and reusable `rowModels` objects outside the component when possible so references stay stable. + +--- + +## The `features` Option + +Features control which APIs, options, and state slices exist on the table instance. In the v8 React adapter, features were bundled together. In v9, importing and registering only what you use is the default. + +### Importing Individual Features + +```tsx +import { + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + tableFeatures, +} from '@tanstack/preact-table' + +const features = tableFeatures({ + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, +}) +``` + +### Using `stockFeatures` for v8-like Behavior + +`stockFeatures` includes the common feature set and can be useful for smoke tests or early migration. It gives up the main bundle-size benefit of v9, so audit it before shipping. + +```tsx +import { stockFeatures, useTable } from '@tanstack/preact-table' + +const table = useTable({ + features: stockFeatures, + rowModels, + columns, + data, +}) +``` + +### Available Features + +| Feature | Import Name | +|---|---| +| Column Filtering | `columnFilteringFeature` | +| Global Filtering | `globalFilteringFeature` | +| Row Sorting | `rowSortingFeature` | +| Row Pagination | `rowPaginationFeature` | +| Row Selection | `rowSelectionFeature` | +| Row Expanding | `rowExpandingFeature` | +| Row Pinning | `rowPinningFeature` | +| Column Pinning | `columnPinningFeature` | +| Column Visibility | `columnVisibilityFeature` | +| Column Ordering | `columnOrderingFeature` | +| Column Sizing | `columnSizingFeature` | +| Column Resizing | `columnResizingFeature` | +| Column Grouping | `columnGroupingFeature` | +| Column Faceting | `columnFacetingFeature` | +| Global Faceting | `globalFacetingFeature` | + +--- + +## The `rowModels` Option + +Row models process data for features like filtering, sorting, grouping, expanding, faceting, and pagination. In v9, they are configured under `rowModels`. + +### Migration Mapping + +| v8 Option | v9 `rowModels` Key | v9 Factory Function | +|---|---|---| +| `getCoreRowModel()` | (automatic) | Not needed | +| `getFilteredRowModel()` | `filteredRowModel` | `createFilteredRowModel(filterFns)` | +| `getSortedRowModel()` | `sortedRowModel` | `createSortedRowModel(sortFns)` | +| `getPaginationRowModel()` | `paginatedRowModel` | `createPaginatedRowModel()` | +| `getExpandedRowModel()` | `expandedRowModel` | `createExpandedRowModel()` | +| `getGroupedRowModel()` | `groupedRowModel` | `createGroupedRowModel(aggregationFns)` | +| `getFacetedRowModel()` | `facetedRowModel` | `createFacetedRowModel()` | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues` | `createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues` | `createFacetedUniqueValues()` | + +### Full Migration Example + +```tsx +// v8 / before: React adapter through preact/compat +import { + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + sortingFns, + filterFns, + useReactTable, +} from '@tanstack/react-table' + +const table = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + sortingFns, + filterFns, +}) + +// v9 +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFns, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, + useTable, +} from '@tanstack/preact-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, +}) + +const rowModels = { + filteredRowModel: createFilteredRowModel(filterFns), + sortedRowModel: createSortedRowModel(sortFns), + paginatedRowModel: createPaginatedRowModel(), +} + +const table = useTable({ + features, + rowModels, + columns, + data, +}) +``` + +--- + +## State Management Changes + +In v8 React-adapter examples, most code read all state through `table.getState()`. In v9, Preact can read a full snapshot, selected state, or a single atom. + +| Surface | Use | +|---|---| +| `table.state` | The selected state from `useTable`; by default, this is the full registered table state. | +| `table.store.state` | A full framework-agnostic table state snapshot. | +| `table.atoms..get()` | A narrow current-value read for one state slice. | +| `table.Subscribe` | A render boundary for selected table state or a specific atom/store source. | +| `table.baseAtoms.` | Internal writable atoms. Prefer feature APIs instead of writing these directly. | + +### Accessing State + +```tsx +// v8 +const sorting = table.getState().sorting +const pagination = table.getState().pagination + +// v9: full snapshot +const sorting = table.store.state.sorting +const pagination = table.store.state.pagination + +// v9: narrow atom read +const sorting = table.atoms.sorting.get() +``` + +By default, `table.state` is reactive and contains the full registered table state: + +```tsx +const table = useTable({ + features, + rowModels, + columns, + data, +}) + +const { pagination, sorting } = table.state +``` + +Pass a custom selector when you want `table.state` to contain only the reactive state values that should cause this component to re-render. + +```tsx +const table = useTable( + { + features, + rowModels, + columns, + data, + }, + (state) => ({ + pagination: state.pagination, + sorting: state.sorting, + }), +) + +table.state.pagination +``` + +Passing `(state) => state` is equivalent to the default selector and is no longer necessary. + +For large tables, opt the parent out and subscribe lower in the tree: + +```tsx +const table = useTable(options, () => null) +``` + +### Optimized Rendering with `table.Subscribe` + +```tsx +function PaginationFooter({ table }) { + return ( + ({ + pagination: state.pagination, + })} + > + {({ pagination }) => ( + Page {pagination.pageIndex + 1} + )} + + ) +} +``` + +`table.Subscribe` can also subscribe directly to one atom: + +```tsx + + {(rowSelection) => {Object.keys(rowSelection).length} selected} + +``` + +### Controlled State + +The `state` plus `on[State]Change` pattern still works for migration. Keep it per-slice. + +```tsx +import { useState } from 'preact/hooks' +import type { PaginationState, SortingState } from '@tanstack/preact-table' + +const [sorting, setSorting] = useState([]) +const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, +}) + +const table = useTable({ + features, + rowModels, + columns, + data, + state: { + sorting, + pagination, + }, + onSortingChange: setSorting, + onPaginationChange: setPagination, +}) +``` + +The v8-style `onStateChange` callback is no longer part of the v9 `useTable` state model. + +### External Atoms + +Use external atoms when the app should own a table state slice and share it outside the table. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/preact-store' +import type { PaginationState, SortingState } from '@tanstack/preact-table' + +function MyTable({ columns, data }) { + const sortingAtom = useCreateAtom([]) + const paginationAtom = useCreateAtom({ + pageIndex: 0, + pageSize: 10, + }) + + const sorting = useSelector(sortingAtom) + const pagination = useSelector(paginationAtom) + + const table = useTable({ + features, + rowModels, + columns, + data, + atoms: { + sorting: sortingAtom, + pagination: paginationAtom, + }, + }) + + return Page {pagination.pageIndex + 1} +} +``` + +When `atoms.pagination` is provided, table writes like `table.setPageIndex(2)` write to that atom. Do not also pass `state.pagination`; atoms take precedence. + +--- + +## Column Helper Changes + +`TFeatures` is now the first generic for column helpers and table types. + +```tsx +// v8 +const columnHelper = createColumnHelper() +const columns: ColumnDef[] = [ + columnHelper.accessor('age', { + header: 'Age', + sortingFn: 'alphanumeric', + }), +] + +// v9 +const columnHelper = createColumnHelper() +const columns: Array> = columnHelper.columns([ + columnHelper.accessor('age', { + header: 'Age', + sortFn: 'alphanumeric', + }), +]) +``` + +Use `columnHelper.columns([...])` to preserve better inference for nested and grouped column definitions. + +--- + +## Rendering Changes + +The React-adapter `flexRender(def, context)` function still exists for advanced cases, but v9 prefers the table-aware `FlexRender` component. + +```tsx +// v8 +{flexRender(cell.column.columnDef.cell, cell.getContext())} + +// v9 + +``` + +You can also import the standalone component: + +```tsx +import { FlexRender } from '@tanstack/preact-table' + + + + +``` + +--- + +## The `tableOptions()` Utility + +`tableOptions()` is a type helper for reusable table option fragments. + +```tsx +import { tableOptions } from '@tanstack/preact-table' + +const baseOptions = tableOptions({ + features, + rowModels, + defaultColumn: { + minSize: 40, + }, +}) + +const table = useTable({ + ...baseOptions, + columns, + data, +}) +``` + +Use it when several tables share feature registration, row models, defaults, or manual server-side settings. + +--- + +## `createTableHook`: Composable Table Patterns + +`createTableHook` creates app-specific Preact table helpers with features, row models, and component conventions already bound. + +```tsx +import { createTableHook } from '@tanstack/preact-table' + +const { useAppTable, createAppColumnHelper } = createTableHook({ + features, + rowModels, +}) + +const columnHelper = createAppColumnHelper() + +function PeopleTable({ data }) { + const table = useAppTable({ + columns, + data, + }) +} +``` + +See the [Composable Tables Guide](./composable-tables.md) for full patterns. + +--- + +## Other Breaking Changes + +### Column Pinning Option Split + +At the table level, `enablePinning` split into column and row options: + +```tsx +const table = useTable({ + enableColumnPinning: true, + enableRowPinning: true, +}) +``` + +Per-column `enablePinning` remains a column option. + +### Column Sizing vs. Column Resizing Split + +Column resizing now has its own feature and state slice. + +```tsx +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) +``` + +`columnSizingInfo` is now `columnResizing`, and `onColumnSizingInfoChange` is now `onColumnResizingChange`. + +### Sorting API Renames + +| v8 | v9 | +|---|---| +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` | `SortFn` | +| `SortingFns` | `SortFns` | + +### Removed Internal API Prefixes + +Several underscore-prefixed APIs are now public without the underscore. For example, `row._getAllCellsByColumnId()` becomes `row.getAllCellsByColumnId()`. + +--- + +## TypeScript Changes Summary + +### Type Generics + +`TFeatures` is now the first generic on core table types. + +```tsx +ColumnDef +Column +Row +Cell +Table +``` + +### Using `typeof features` + +Use the concrete `features` object for type inference: + +```tsx +const features = tableFeatures({ + rowSortingFeature, + rowPaginationFeature, +}) + +const columnHelper = createColumnHelper() +``` + +### Using `StockFeatures` + +If a helper must support `stockFeatures`, use `StockFeatures`: + +```tsx +import type { StockFeatures } from '@tanstack/preact-table' + +type PersonColumn = ColumnDef +``` + +### `ColumnMeta` Generic Change + +Module augmentation now includes `TFeatures`: + +```tsx +declare module '@tanstack/preact-table' { + interface ColumnMeta { + align?: 'left' | 'right' + } +} +``` + +### `RowData` Type Restriction + +`RowData` is now constrained to record-like objects or arrays. Prefer object row types such as: + +```tsx +type Person = { + firstName: string + lastName: string + age: number +} +``` + +--- + +## Migration Checklist + +- [ ] Replace `@tanstack/react-table` imports used through `preact/compat` with `@tanstack/preact-table`. +- [ ] Replace `useReactTable` with `useTable`. +- [ ] Add `features: tableFeatures({ ... })`. +- [ ] Replace root `get*RowModel` options with `rowModels`. +- [ ] Drop `getCoreRowModel`; the core row model is automatic. +- [ ] Move `sortingFns`, `filterFns`, and `aggregationFns` into row model factories. +- [ ] Rename `sortingFn` to `sortFn` and `sortingFns` to `sortFns`. +- [ ] Update column helpers and types to include `typeof features`. +- [ ] Replace broad `table.getState()` reads with `table.state`, `table.store.state`, or `table.atoms..get()`. +- [ ] Replace `onStateChange` with per-slice `on[State]Change` or external atoms. +- [ ] Replace direct `flexRender(...)` calls with `` or ``. +- [ ] Remove `preact/compat` aliases that were only needed for TanStack Table. +- [ ] Audit `stockFeatures` usage before production. +- [ ] Run type checks and click through sorting, filtering, pagination, and selection flows. + +--- + +## Examples + +- [Basic useTable](../examples/basic-use-table) +- [Basic Subscribe](../examples/basic-subscribe) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) +- [With TanStack Query](../examples/with-tanstack-query) +- [Sorting](../examples/sorting) +- [Pagination](../examples/pagination) +- [Composable Tables](../examples/composable-tables) diff --git a/docs/framework/preact/guide/pagination.md b/docs/framework/preact/guide/pagination.md index d4914b796b..997206db68 100644 --- a/docs/framework/preact/guide/pagination.md +++ b/docs/framework/preact/guide/pagination.md @@ -75,8 +75,6 @@ const table = useTable({ }) ``` -> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. - ### Manual Server-Side Pagination If you decide that you need to use server-side pagination, here is how you can implement it. diff --git a/docs/framework/react/guide/migrating.md b/docs/framework/react/guide/migrating.md index a5f1fba2bd..f48382ef6f 100644 --- a/docs/framework/react/guide/migrating.md +++ b/docs/framework/react/guide/migrating.md @@ -15,7 +15,7 @@ TanStack Table v9 is a major release that introduces significant architectural i - **Uses TanStack Store**: The internal state system has been rebuilt on [TanStack Store](https://tanstack.com/store), providing a reactive, framework-agnostic foundation. This works similarly to TanStack Form's state model. - **Three-layer atom architecture**: Each state slice (sorting, pagination, rowSelection, etc.) lives in its own [atom](https://tanstack.com/store/latest/docs/reference/atom) rather than a single monolithic state object. Internally, the library writes to per-slice `baseAtoms`; reads go through derived `table.atoms` and the flat `table.store`. This enables fine-grained reactivity — components can subscribe to just the slices they care about. -- **Opt-in subscriptions instead of memo hacks**: Use `table.Subscribe` or pass a selector to `useTable` to subscribe to specific slices of state. Only re-render when the state you care about changes—no more `React.memo` or manual memoization. Pass `state => state` if you want v8-style behavior where any state change triggers a re-render. +- **Default full-state subscription, optional narrower selectors**: By default, `useTable` selects all registered table state, so `table.state` contains the full state and the component re-renders when any registered state changes. Pass a narrower selector or use `table.Subscribe` when only part of the UI should re-render. - **Bring your own atoms (optional)**: For advanced use cases, you can own individual state slices by passing your own writable atoms via the new `atoms` option. This is great for sharing a slice across components or integrating with other atom-based tools. Precedence: `options.atoms[key]` > `options.state[key]` > internal `baseAtoms[key]`. ### 3. Composability @@ -27,7 +27,7 @@ TanStack Table v9 is a major release that introduces significant architectural i While v9 is a significant upgrade, **you don't have to adopt everything at once**: -- **Don't want to optimize renders?** Pass `state => state` as the selector to `useTable` and rendering works like v8. +- **Don't want to optimize renders yet?** Do nothing special. The default selector selects all registered state, so rendering works like v8. - **Don't want to think about tree-shaking?** Import `stockFeatures` to include all features, just like v8. - **Table markup is largely unchanged.** How you render ``, ``, ``, ` + +// v9 + +``` + +The table instance also exposes the same component: + +```tsx + + + +``` + +--- + +## The `tableOptions()` Utility + +`tableOptions()` lets you compose reusable option fragments with type inference. + +```tsx +import { tableOptions } from '@tanstack/solid-table' + +const baseOptions = tableOptions({ + features, + rowModels, + defaultColumn: { + minSize: 40, + }, +}) + +const table = createTable({ + ...baseOptions, + columns, + get data() { + return data() + }, +}) +``` + +--- + +## `createTableHook`: Composable Table Patterns + +`createTableHook` creates shared Solid table helpers. + +```tsx +import { createTableHook } from '@tanstack/solid-table' + +const { createAppTable, createAppColumnHelper } = createTableHook({ + features, + rowModels, +}) + +const columnHelper = createAppColumnHelper() + +const table = createAppTable({ + columns, + get data() { + return data() + }, +}) +``` + +See the [Composable Tables Guide](./composable-tables.md) for complete patterns. + +--- + +## Other Breaking Changes + +### Column Pinning Option Split + +Table-level `enablePinning` split into: + +```tsx +enableColumnPinning: true +enableRowPinning: true +``` + +### Column Sizing vs. Column Resizing Split + +Column resizing is now a separate feature and state slice. + +```tsx +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) +``` + +`columnSizingInfo` became `columnResizing`, and `onColumnSizingInfoChange` became `onColumnResizingChange`. + +### Sorting API Renames + +| v8 | v9 | +|---|---| +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` | `SortFn` | + +### Removed Internal API Prefixes + +Underscore-prefixed APIs that are now public should be called without `_`, such as `row.getAllCellsByColumnId()`. + +--- + +## TypeScript Changes Summary + +### Type Generics + +Use `TFeatures` as the first generic: + +```tsx +ColumnDef +Column +Row +Table +``` + +### Using `typeof features` + +```tsx +const features = tableFeatures({ + rowSortingFeature, + rowPaginationFeature, +}) + +const columnHelper = createColumnHelper() +``` + +### Using `StockFeatures` + +```tsx +import type { StockFeatures } from '@tanstack/solid-table' + +type PersonColumn = ColumnDef +``` + +### `ColumnMeta` Generic Change + +```tsx +declare module '@tanstack/solid-table' { + interface ColumnMeta { + align?: 'left' | 'right' + } +} +``` + +### `RowData` Type Restriction + +Prefer explicit object row types: + +```tsx +type Person = { + firstName: string + lastName: string + age: number +} +``` + +--- + +## Migration Checklist + +- [ ] Replace `createSolidTable` with `createTable`. +- [ ] Add `features: tableFeatures({ ... })`. +- [ ] Move every `get*RowModel` option into `rowModels`. +- [ ] Remove `getCoreRowModel`; the core row model is automatic. +- [ ] Pass `sortFns`, `filterFns`, and `aggregationFns` to row model factories. +- [ ] Rename `sortingFn` to `sortFn`. +- [ ] Add `typeof features` to column helpers and table types. +- [ ] Use getters for reactive `data` and controlled `state` slices. +- [ ] Replace `table.getState()` reads with `table.atoms..get()` or `table.store.get()`. +- [ ] Replace top-level `onStateChange` with per-slice handlers or external atoms. +- [ ] Replace `flexRender(...)` calls with ``. +- [ ] Audit `stockFeatures` before production. + +--- + +## Examples + +- [Basic createTable](../examples/basic-use-table) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) +- [With TanStack Query](../examples/with-tanstack-query) +- [Sorting](../examples/sorting) +- [Pagination](../examples/pagination) +- [Composable Tables](../examples/composable-tables) diff --git a/docs/framework/solid/guide/pagination.md b/docs/framework/solid/guide/pagination.md index 86eda63e60..44e381a0e3 100644 --- a/docs/framework/solid/guide/pagination.md +++ b/docs/framework/solid/guide/pagination.md @@ -79,8 +79,6 @@ const table = createTable({ }) ``` -> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. - ### Manual Server-Side Pagination If you decide that you need to use server-side pagination, here is how you can implement it. diff --git a/docs/framework/svelte/guide/migrating.md b/docs/framework/svelte/guide/migrating.md new file mode 100644 index 0000000000..dbd07deccb --- /dev/null +++ b/docs/framework/svelte/guide/migrating.md @@ -0,0 +1,675 @@ +--- +title: Migrating to TanStack Table v9 (Svelte) +--- + +## What's New in TanStack Table v9 + +TanStack Table v9 is a major release with explicit feature registration, row model registration, and a new atom-backed state model. The Svelte adapter was also rewritten around Svelte 5 runes. + +### 1. Tree-shaking + +- **Features are tree-shakeable**: register only the table features you use. +- **Row models are explicit**: move root `get*RowModel` options into the `rowModels` object. +- **Function registries moved to factories**: pass `sortFns`, `filterFns`, and `aggregationFns` into row model factories instead of root table options. + +### 2. State Management + +- **Uses TanStack Store**: table state is backed by TanStack Store atoms. +- **Uses Svelte 5 reactivity**: the adapter uses Svelte 5 runes and Svelte-aware atom bindings. +- **Per-slice state**: each registered feature creates its own state slice in `table.atoms`. +- **Default full-state selection, optional narrower selectors**: `table.state` contains the full registered state by default; use a custom selector, `subscribeTable`, or `useSelector` from `@tanstack/svelte-store` when you need a narrower reactive surface. + +### 3. Composability + +- **`tableOptions()`**: compose reusable option fragments. +- **`createTableHook()`**: define shared Svelte table factories with pre-bound features, row models, defaults, and registered components. + +### The Good News: Most Table Logic Is Still Familiar + +- Column definitions keep the same basic `accessorKey`, `accessorFn`, `header`, `cell`, and `footer` shapes. +- Feature APIs like `table.nextPage()`, `column.toggleSorting()`, and `row.toggleSelected()` remain the preferred way to change state. +- Markup still renders header groups, rows, and cells from the table instance. + +The main changes are the Svelte 5 requirement, the new `createTable` entrypoint, explicit `features` and `rowModels`, and the move from v8 writable-store patterns to v9 runes and atoms. + +--- + +## Svelte 5 Requirement + +The v9 Svelte adapter only supports **Svelte 5+**. It is built on Svelte 5 runes such as `$state`, `$derived.by`, and `$effect.pre`. + +If your app is still on Svelte 3 or Svelte 4, choose one of these paths: + +- Stay on `@tanstack/svelte-table@8`. +- Migrate the app to Svelte 5 first, then migrate TanStack Table. + +There is no Svelte 3/4 compatibility shim for the v9 Svelte adapter. + +--- + +## Core Breaking Changes + +### Entrypoint Rename + +```ts +// v8 +import { createSvelteTable } from '@tanstack/svelte-table' + +const table = createSvelteTable(options) + +// v9 +import { createTable } from '@tanstack/svelte-table' + +const table = createTable(options) +``` + +### New Required Options: `features` and `rowModels` + +```ts +// v8 +import { + createSvelteTable, + getCoreRowModel, +} from '@tanstack/svelte-table' + +const table = createSvelteTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), +}) + +// v9 +import { createTable, tableFeatures } from '@tanstack/svelte-table' + +const features = tableFeatures({}) + +const table = createTable({ + features, + rowModels: {}, // core row model is automatic + columns, + get data() { + return data + }, +}) +``` + +In Svelte 5, pass reactive values like `data` through getters so table options read the current rune value. + +--- + +## The `features` Option + +Features control which APIs, options, and state slices exist on the table. + +### Importing Individual Features + +```ts +import { + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + tableFeatures, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, +}) +``` + +If a feature is not registered, its APIs and state slice are not available. + +### Using `stockFeatures` for v8-like Behavior + +`stockFeatures` is useful for early migration when you have not audited feature usage yet. + +```ts +import { createTable, stockFeatures } from '@tanstack/svelte-table' + +const table = createTable({ + features: stockFeatures, + rowModels, + columns, + get data() { + return data + }, +}) +``` + +Use it as a temporary migration shortcut. Explicit feature registration is the production target. + +### Available Features + +| Feature | Import Name | +|---|---| +| Column Filtering | `columnFilteringFeature` | +| Global Filtering | `globalFilteringFeature` | +| Row Sorting | `rowSortingFeature` | +| Row Pagination | `rowPaginationFeature` | +| Row Selection | `rowSelectionFeature` | +| Row Expanding | `rowExpandingFeature` | +| Row Pinning | `rowPinningFeature` | +| Column Pinning | `columnPinningFeature` | +| Column Visibility | `columnVisibilityFeature` | +| Column Ordering | `columnOrderingFeature` | +| Column Sizing | `columnSizingFeature` | +| Column Resizing | `columnResizingFeature` | +| Column Grouping | `columnGroupingFeature` | +| Column Faceting | `columnFacetingFeature` | +| Global Faceting | `globalFacetingFeature` | + +--- + +## The `rowModels` Option + +Row models now live under `rowModels`. + +### Migration Mapping + +| v8 Option | v9 `rowModels` Key | v9 Factory Function | +|---|---|---| +| `getCoreRowModel()` | (automatic) | Not needed | +| `getFilteredRowModel()` | `filteredRowModel` | `createFilteredRowModel(filterFns)` | +| `getSortedRowModel()` | `sortedRowModel` | `createSortedRowModel(sortFns)` | +| `getPaginationRowModel()` | `paginatedRowModel` | `createPaginatedRowModel()` | +| `getExpandedRowModel()` | `expandedRowModel` | `createExpandedRowModel()` | +| `getGroupedRowModel()` | `groupedRowModel` | `createGroupedRowModel(aggregationFns)` | +| `getFacetedRowModel()` | `facetedRowModel` | `createFacetedRowModel()` | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues` | `createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues` | `createFacetedUniqueValues()` | + +### Full Migration Example + +```svelte + +``` + +```svelte + +``` + +--- + +## State Management Changes + +Svelte v9 table state is atom-backed and rune-aware. Do not port v8 writable-store table option patterns directly except as "before" code. + +| Surface | Use | +|---|---| +| `table.state` | Full registered table state by default, or selected state from the second argument to `createTable`. | +| `table.store.state` | Current full table state snapshot. | +| `table.atoms..get()` | Narrow current-value read for one state slice. | +| `subscribeTable(source, selector?)` | Fine-grained Svelte subscription helper that exposes `.current`. | +| `table.baseAtoms.` | Internal writable atoms. Prefer feature APIs or external atoms. | + +### Accessing State + +```ts +// v8 +const sorting = table.getState().sorting + +// v9: full snapshot +const sorting = table.store.state.sorting + +// v9: narrow atom read +const sorting = table.atoms.sorting.get() +``` + +By default, `table.state` contains the full registered table state. + +```ts +const table = createTable({ + features, + rowModels, + columns, + get data() { + return data + }, +}) + +const { pagination, sorting } = table.state +``` + +Pass a second-argument selector when you want `table.state` to contain only the values that should cause reactive updates. + +```ts +const table = createTable( + { + features, + rowModels, + columns, + get data() { + return data + }, + }, + (state) => ({ + pagination: state.pagination, + }), +) + +table.state.pagination +``` + +Passing `(state) => state` is equivalent to the default selector and is no longer necessary. + +### Fine-grained Updates with `subscribeTable` + +```ts +import { subscribeTable } from '@tanstack/svelte-table' + +const pagination = subscribeTable(table.atoms.pagination) +const pageIndex = subscribeTable( + table.atoms.pagination, + (pagination) => pagination.pageIndex, +) +``` + +```svelte + + Page {pagination.current.pageIndex + 1} of {table.getPageCount()} + +``` + +### Controlled State + +Use `createTableState` for Svelte-owned state slices that need to accept TanStack Table updater functions. + +```svelte + +``` + +The v8-style `onStateChange` callback is gone. Use per-slice `on[State]Change` callbacks or external atoms. + +### External Atoms + +Use external atoms when the app should own and share state slices outside the table. + +```svelte + + +Page {pagination.current.pageIndex + 1} +``` + +Do not provide both `atoms.pagination` and `state.pagination`; the atom owns that slice. + +--- + +## Column Helper Changes + +Column helpers and column types now include `TFeatures` first. + +```ts +// v8 +const columnHelper = createColumnHelper() +const columns: ColumnDef[] = [ + columnHelper.accessor('age', { + header: 'Age', + sortingFn: 'alphanumeric', + }), +] + +// v9 +const columnHelper = createColumnHelper() +const columns: Array> = columnHelper.columns([ + columnHelper.accessor('age', { + header: 'Age', + sortFn: 'alphanumeric', + }), +]) +``` + +Use `columnHelper.columns([...])` for better inference across nested columns. + +--- + +## Rendering Changes + +Replace v8 `flexRender` calls with the Svelte `FlexRender` component. + +```svelte + + + + + + +``` + +For Svelte components in column definitions, use `renderComponent`. + +```ts +import { renderComponent } from '@tanstack/svelte-table' +import StatusCell from './StatusCell.svelte' + +const columns = columnHelper.columns([ + columnHelper.accessor('status', { + cell: ({ row }) => renderComponent(StatusCell, { row }), + }), +]) +``` + +For Svelte snippets, use `renderSnippet`. + +```svelte + + +{#snippet nameCell(row)} + {row.original.firstName} +{/snippet} +``` + +--- + +## The `tableOptions()` Utility + +`tableOptions()` helps compose shared table option fragments. + +```ts +import { tableOptions } from '@tanstack/svelte-table' + +const baseOptions = tableOptions({ + features, + rowModels, + defaultColumn: { + minSize: 40, + }, +}) + +const table = createTable({ + ...baseOptions, + columns, + get data() { + return data + }, +}) +``` + +--- + +## `createTableHook`: Composable Table Patterns + +`createTableHook` creates shared Svelte table helpers with features, row models, and registered components already bound. + +```ts +import { createTableHook } from '@tanstack/svelte-table' + +export const { createAppTable, createAppColumnHelper } = createTableHook({ + features, + rowModels, +}) + +const columnHelper = createAppColumnHelper() + +const table = createAppTable({ + columns, + get data() { + return data + }, +}) +``` + +See the [Composable Tables Guide](./composable-tables.md) for full patterns. + +--- + +## Other Breaking Changes + +### Column Pinning Option Split + +Table-level `enablePinning` split into: + +```ts +enableColumnPinning: true +enableRowPinning: true +``` + +### Column Sizing vs. Column Resizing Split + +Column resizing now has its own feature and state slice. + +```ts +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) +``` + +`columnSizingInfo` became `columnResizing`, and `onColumnSizingInfoChange` became `onColumnResizingChange`. + +### Sorting API Renames + +| v8 | v9 | +|---|---| +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` | `SortFn` | + +### Removed Internal API Prefixes + +Underscore-prefixed APIs that are now public should be called without `_`, such as `row.getAllCellsByColumnId()`. + +--- + +## TypeScript Changes Summary + +### Type Generics + +Use `TFeatures` as the first generic: + +```ts +ColumnDef +Column +Row +Table +``` + +### Using `typeof features` + +```ts +const features = tableFeatures({ + rowSortingFeature, + rowPaginationFeature, +}) + +const columnHelper = createColumnHelper() +``` + +### Using `StockFeatures` + +```ts +import type { StockFeatures } from '@tanstack/svelte-table' + +type PersonColumn = ColumnDef +``` + +### `ColumnMeta` Generic Change + +```ts +declare module '@tanstack/svelte-table' { + interface ColumnMeta { + align?: 'left' | 'right' + } +} +``` + +### `RowData` Type Restriction + +Prefer explicit object row types: + +```ts +type Person = { + firstName: string + lastName: string + age: number +} +``` + +--- + +## Migration Checklist + +- [ ] Upgrade the app to Svelte 5. +- [ ] Replace `createSvelteTable` with `createTable`. +- [ ] Replace Svelte 3/4 writable-store table patterns with runes and getters. +- [ ] Add `features: tableFeatures({ ... })`. +- [ ] Move root `get*RowModel` options into `rowModels`. +- [ ] Remove `getCoreRowModel`; the core row model is automatic. +- [ ] Pass `sortFns`, `filterFns`, and `aggregationFns` to row model factories. +- [ ] Rename `sortingFn` to `sortFn`. +- [ ] Add `typeof features` to column helpers and types. +- [ ] Pass reactive `data` and controlled `state` slices through getters. +- [ ] Replace `table.getState()` reads with `table.state`, `table.store.state`, `table.atoms..get()`, or `subscribeTable`. +- [ ] Replace `onStateChange` with per-slice callbacks or external atoms. +- [ ] Replace `flexRender(...)` and `` table rendering with ``. +- [ ] Use `renderComponent` or `renderSnippet` for Svelte component/snippet cells. +- [ ] Audit `stockFeatures` before production. + +--- + +## Examples + +- [Basic createTable](../examples/basic-create-table) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) +- [With TanStack Query](../examples/with-tanstack-query) +- [Sorting](../examples/sorting) +- [Pagination](../examples/pagination) +- [Composable Tables](../examples/composable-tables) diff --git a/docs/framework/svelte/guide/pagination.md b/docs/framework/svelte/guide/pagination.md index 0dcc215fe1..503d28269b 100644 --- a/docs/framework/svelte/guide/pagination.md +++ b/docs/framework/svelte/guide/pagination.md @@ -79,8 +79,6 @@ const table = createTable({ }) ``` -> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. - ### Manual Server-Side Pagination If you decide that you need to use server-side pagination, here is how you can implement it. diff --git a/docs/framework/vanilla/guide/pagination.md b/docs/framework/vanilla/guide/pagination.md index 315b914dc2..6d9d0ba99c 100644 --- a/docs/framework/vanilla/guide/pagination.md +++ b/docs/framework/vanilla/guide/pagination.md @@ -78,8 +78,6 @@ const table = constructTable({ }) ``` -> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. - ### Manual Server-Side Pagination If you decide that you need to use server-side pagination, here is how you can implement it. diff --git a/docs/framework/vue/guide/migrating.md b/docs/framework/vue/guide/migrating.md new file mode 100644 index 0000000000..d8c943418d --- /dev/null +++ b/docs/framework/vue/guide/migrating.md @@ -0,0 +1,618 @@ +--- +title: Migrating to TanStack Table v9 (Vue) +--- + +## What's New in TanStack Table v9 + +TanStack Table v9 is a major release with explicit feature registration, row model registration, and a new atom-backed state model. The Vue adapter keeps table rendering headless while adding Vue-aware reactivity for table atoms and reactive options. + +### 1. Tree-shaking + +- **Features are tree-shakeable**: register only the table features you use. +- **Row models are explicit**: move root `get*RowModel` options into `rowModels`. +- **Function registries moved to factories**: row model factories receive `sortFns`, `filterFns`, and `aggregationFns` directly. + +### 2. State Management + +- **Uses TanStack Store**: table state is backed by TanStack Store atoms. +- **Uses Vue reactivity**: table atoms are backed by Vue refs and computed values. +- **Per-slice state**: registered features expose their state through `table.atoms`. +- **Vue option syncing**: `useTable` unwraps refs and computed values in options like `data` and syncs the table when they change. + +### 3. Composability + +- **`tableOptions()`**: compose reusable option fragments. +- **`createTableHook()`**: define shared Vue table factories with pre-bound features, row models, defaults, and components. + +### The Good News: Most Table Logic Is Still Familiar + +- Column definitions keep the same basic `accessorKey`, `accessorFn`, `header`, `cell`, and `footer` shapes. +- Feature APIs like `table.nextPage()`, `column.toggleSorting()`, and `row.toggleSelected()` remain the preferred way to update state. +- Templates still render header groups, rows, and cells from the table instance. + +The main migration is replacing `useVueTable` with `useTable`, then moving feature and row-model setup into the v9 shape. + +--- + +## Core Breaking Changes + +### Hook Rename + +```ts +// v8 +import { useVueTable } from '@tanstack/vue-table' + +const table = useVueTable(options) + +// v9 +import { useTable } from '@tanstack/vue-table' + +const table = useTable(options) +``` + +### New Required Options: `features` and `rowModels` + +```ts +// v8 +import { + getCoreRowModel, + useVueTable, +} from '@tanstack/vue-table' + +const table = useVueTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), +}) + +// v9 +import { tableFeatures, useTable } from '@tanstack/vue-table' + +const features = tableFeatures({}) + +const table = useTable({ + features, + rowModels: {}, // core row model is automatic + columns, + data, +}) +``` + +`data` can be a raw array, a `ref`, a `computed`, or a getter. The adapter unwraps reactive option values and keeps the table synced. + +--- + +## The `features` Option + +Features control which APIs, options, and state slices exist on the table. + +### Importing Individual Features + +```ts +import { + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + tableFeatures, +} from '@tanstack/vue-table' + +const features = tableFeatures({ + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, +}) +``` + +If a feature is not registered, its APIs and state slice are not available. + +### Using `stockFeatures` for v8-like Behavior + +`stockFeatures` is useful for early migration before you audit feature usage. + +```ts +import { stockFeatures, useTable } from '@tanstack/vue-table' + +const table = useTable({ + features: stockFeatures, + rowModels, + columns, + data, +}) +``` + +Use it as a temporary migration shortcut. Explicit feature registration is the production target. + +### Available Features + +| Feature | Import Name | +|---|---| +| Column Filtering | `columnFilteringFeature` | +| Global Filtering | `globalFilteringFeature` | +| Row Sorting | `rowSortingFeature` | +| Row Pagination | `rowPaginationFeature` | +| Row Selection | `rowSelectionFeature` | +| Row Expanding | `rowExpandingFeature` | +| Row Pinning | `rowPinningFeature` | +| Column Pinning | `columnPinningFeature` | +| Column Visibility | `columnVisibilityFeature` | +| Column Ordering | `columnOrderingFeature` | +| Column Sizing | `columnSizingFeature` | +| Column Resizing | `columnResizingFeature` | +| Column Grouping | `columnGroupingFeature` | +| Column Faceting | `columnFacetingFeature` | +| Global Faceting | `globalFacetingFeature` | + +--- + +## The `rowModels` Option + +Row models now live under `rowModels`. + +### Migration Mapping + +| v8 Option | v9 `rowModels` Key | v9 Factory Function | +|---|---|---| +| `getCoreRowModel()` | (automatic) | Not needed | +| `getFilteredRowModel()` | `filteredRowModel` | `createFilteredRowModel(filterFns)` | +| `getSortedRowModel()` | `sortedRowModel` | `createSortedRowModel(sortFns)` | +| `getPaginationRowModel()` | `paginatedRowModel` | `createPaginatedRowModel()` | +| `getExpandedRowModel()` | `expandedRowModel` | `createExpandedRowModel()` | +| `getGroupedRowModel()` | `groupedRowModel` | `createGroupedRowModel(aggregationFns)` | +| `getFacetedRowModel()` | `facetedRowModel` | `createFacetedRowModel()` | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues` | `createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues` | `createFacetedUniqueValues()` | + +### Full Migration Example + +```ts +// v8 +import { + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + filterFns, + sortingFns, + useVueTable, +} from '@tanstack/vue-table' + +const table = useVueTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + filterFns, + sortingFns, +}) + +// v9 +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFns, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, + useTable, +} from '@tanstack/vue-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, +}) + +const rowModels = { + filteredRowModel: createFilteredRowModel(filterFns), + sortedRowModel: createSortedRowModel(sortFns), + paginatedRowModel: createPaginatedRowModel(), +} + +const table = useTable({ + features, + rowModels, + columns, + data, +}) +``` + +--- + +## State Management Changes + +Vue v9 table state is atom-backed and Vue-aware. Prefer Vue `computed` values around narrow atom reads over broad whole-state reads. + +| Surface | Use | +|---|---| +| `table.atoms..get()` | Narrow reactive reads inside Vue tracking scopes. | +| `table.store.get()` | Current full state snapshot. Use mostly for debug output or intentionally broad dependencies. | +| `table.Subscribe` | A render-function or JSX boundary whose child reads the atoms it needs. | +| `table.baseAtoms.` | Internal writable atoms. Prefer feature APIs or external atoms. | + +### Accessing State + +```ts +// v8 +const sorting = table.getState().sorting + +// v9: narrow atom read +const sorting = table.atoms.sorting.get() + +// v9: full snapshot +const tableState = table.store.get() +``` + +Use Vue primitives to derive reactive values: + +```ts +import { computed } from 'vue' + +const pagination = computed(() => table.atoms.pagination.get()) +const pageIndex = computed(() => pagination.value.pageIndex) +const tableStateJson = computed(() => + JSON.stringify(table.store.get(), null, 2), +) +``` + +### Reactive Options + +`data` can be a `ref` or `computed`; the adapter unwraps and syncs it. + +```ts +import { ref } from 'vue' + +const data = ref(makeData(100)) + +const table = useTable({ + features, + rowModels: {}, + columns, + data, +}) + +data.value = makeData(200) +``` + +Getter-based options also work: + +```ts +const table = useTable({ + features, + rowModels, + columns, + get data() { + return data.value + }, +}) +``` + +### Fine-grained Updates with `table.Subscribe` + +Use `table.Subscribe` in render functions or JSX when a specific subtree should track selected atoms. + +```tsx + + {(atoms) => { + const pagination = atoms.pagination.get() + + return ( + Page {pagination.pageIndex + 1} + ) + }} + +``` + +### Controlled State + +When Vue refs own a state slice, expose the current value with getters and update the ref in the matching callback. + +```ts +import { ref } from 'vue' +import type { + PaginationState, + SortingState, + Updater, +} from '@tanstack/vue-table' + +function resolveUpdater(updater: Updater, previous: T): T { + return typeof updater === 'function' + ? (updater as (old: T) => T)(previous) + : updater +} + +const sorting = ref([]) +const pagination = ref({ + pageIndex: 0, + pageSize: 10, +}) + +const table = useTable({ + features, + rowModels, + columns, + get data() { + return data.value + }, + state: { + get sorting() { + return sorting.value + }, + get pagination() { + return pagination.value + }, + }, + onSortingChange: (updater) => { + sorting.value = resolveUpdater(updater, sorting.value) + }, + onPaginationChange: (updater) => { + pagination.value = resolveUpdater(updater, pagination.value) + }, +}) +``` + +The v8-style top-level `onStateChange` callback is gone. Use per-slice callbacks or external atoms. + +### External Atoms + +Use external atoms when the app should own and share state slices outside the table. + +```ts +import { createAtom, useSelector } from '@tanstack/vue-store' +import type { PaginationState, SortingState } from '@tanstack/vue-table' + +const sortingAtom = createAtom([]) +const paginationAtom = createAtom({ + pageIndex: 0, + pageSize: 10, +}) + +const pagination = useSelector(paginationAtom) + +const table = useTable({ + features, + rowModels, + columns, + get data() { + return data.value + }, + atoms: { + sorting: sortingAtom, + pagination: paginationAtom, + }, +}) + +pagination.value.pageIndex +``` + +Do not provide both `atoms.pagination` and `state.pagination`; the atom owns that slice. + +--- + +## Column Helper Changes + +Column helpers and column types now include `TFeatures` first. + +```ts +// v8 +const columnHelper = createColumnHelper() +const columns: ColumnDef[] = [ + columnHelper.accessor('age', { + header: 'Age', + sortingFn: 'alphanumeric', + }), +] + +// v9 +const columnHelper = createColumnHelper() +const columns: Array> = columnHelper.columns([ + columnHelper.accessor('age', { + header: 'Age', + sortFn: 'alphanumeric', + }), +]) +``` + +Use `columnHelper.columns([...])` for better inference across nested columns. + +--- + +## Rendering Changes + +The v9 `FlexRender` component supports shorthand props for cells, headers, and footers. + +```vue + + + + + + + +``` + +The older `:render` and `:props` shape still compiles, but the shorthand props are the preferred migration target. + +--- + +## The `tableOptions()` Utility + +`tableOptions()` helps compose shared table option fragments. + +```ts +import { tableOptions } from '@tanstack/vue-table' + +const baseOptions = tableOptions({ + features, + rowModels, + defaultColumn: { + minSize: 40, + }, +}) + +const table = useTable({ + ...baseOptions, + columns, + data, +}) +``` + +--- + +## `createTableHook`: Composable Table Patterns + +`createTableHook` creates shared Vue table helpers with features, row models, and registered components already bound. + +```ts +import { createTableHook } from '@tanstack/vue-table' + +const { useAppTable, createAppColumnHelper } = createTableHook({ + features, + rowModels, +}) + +const columnHelper = createAppColumnHelper() + +const table = useAppTable({ + columns, + data, +}) +``` + +See the [Composable Tables Guide](./composable-tables.md) for full patterns. + +--- + +## Other Breaking Changes + +### Column Pinning Option Split + +Table-level `enablePinning` split into: + +```ts +enableColumnPinning: true +enableRowPinning: true +``` + +### Column Sizing vs. Column Resizing Split + +Column resizing now has its own feature and state slice. + +```ts +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) +``` + +`columnSizingInfo` became `columnResizing`, and `onColumnSizingInfoChange` became `onColumnResizingChange`. + +### Sorting API Renames + +| v8 | v9 | +|---|---| +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` | `SortFn` | + +### Removed Internal API Prefixes + +Underscore-prefixed APIs that are now public should be called without `_`, such as `row.getAllCellsByColumnId()`. + +--- + +## TypeScript Changes Summary + +### Type Generics + +Use `TFeatures` as the first generic: + +```ts +ColumnDef +Column +Row +Table +``` + +### Using `typeof features` + +```ts +const features = tableFeatures({ + rowSortingFeature, + rowPaginationFeature, +}) + +const columnHelper = createColumnHelper() +``` + +### Using `StockFeatures` + +```ts +import type { StockFeatures } from '@tanstack/vue-table' + +type PersonColumn = ColumnDef +``` + +### `ColumnMeta` Generic Change + +```ts +declare module '@tanstack/vue-table' { + interface ColumnMeta { + align?: 'left' | 'right' + } +} +``` + +### `RowData` Type Restriction + +Prefer explicit object row types: + +```ts +type Person = { + firstName: string + lastName: string + age: number +} +``` + +--- + +## Migration Checklist + +- [ ] Replace `useVueTable` with `useTable`. +- [ ] Add `features: tableFeatures({ ... })`. +- [ ] Move root `get*RowModel` options into `rowModels`. +- [ ] Remove `getCoreRowModel`; the core row model is automatic. +- [ ] Pass `sortFns`, `filterFns`, and `aggregationFns` to row model factories. +- [ ] Rename `sortingFn` to `sortFn`. +- [ ] Add `typeof features` to column helpers and types. +- [ ] Replace `table.getState()` reads with `table.atoms..get()` or `table.store.get()`. +- [ ] Use Vue getters for controlled `state` slices. +- [ ] Replace top-level `onStateChange` with per-slice callbacks or external atoms. +- [ ] Prefer ``, `:header`, and `:footer` shorthand rendering. +- [ ] Audit `stockFeatures` before production. + +--- + +## Examples + +- [Basic useTable](../examples/basic-use-table) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) +- [With TanStack Query](../examples/with-tanstack-query) +- [Sorting](../examples/sorting) +- [Pagination](../examples/pagination) +- [Composable Tables](../examples/composable-tables) diff --git a/docs/framework/vue/guide/pagination.md b/docs/framework/vue/guide/pagination.md index 23752193a1..ca748f1264 100644 --- a/docs/framework/vue/guide/pagination.md +++ b/docs/framework/vue/guide/pagination.md @@ -77,8 +77,6 @@ const table = useTable({ }) ``` -> **Migrating from v8?** See [useLegacyTable](../../react/guide/use-legacy-table) for incremental migration. - ### Manual Server-Side Pagination If you decide that you need to use server-side pagination, here is how you can implement it.
`, etc. remains the same. @@ -277,7 +277,7 @@ v9's state system is built on [TanStack Store](https://tanstack.com/store) and e | Surface | Type | When to use | |---------|------|-------------| -| `table.state` | `TSelected` (the shape you return from your `useTable` selector) | The most ergonomic read surface inside a component rendered by `useTable`. | +| `table.state` | `TSelected` (full registered table state by default, or the shape returned from your custom `useTable` selector) | The most ergonomic read surface inside a component rendered by `useTable`. | | `table.store` | `ReadonlyStore` | A flat, framework-agnostic store of the entire table state. Use `table.store.state` for one-off reads, or pair with `useSelector` / `table.Subscribe` for fine-grained subscriptions. | | `table.atoms.` | `ReadonlyAtom` | A per-slice readonly atom. Subscribe to a single slice (e.g. `table.atoms.sorting`) when you want the narrowest possible re-render surface. | @@ -301,13 +301,22 @@ const { sorting, pagination } = table.getState() const fullState = table.store.state const { sorting, pagination } = table.store.state -// v9 - via table.state (selected state from your selector) -const table = useTable(options, (state) => ({ +// v9 - via table.state (full selected state by default) +const table = useTable({ + features, + rowModels: { /* ... */ }, + columns, + data, +}) +const { sorting, pagination } = table.state + +// v9 - via table.state with a custom selector +const selectedTable = useTable(options, (state) => ({ sorting: state.sorting, pagination: state.pagination, })) -// Now table.state only contains sorting and pagination -const { sorting, pagination } = table.state +// Now selectedTable.state only contains sorting and pagination +const { sorting, pagination } = selectedTable.state // v9 - via a single slice atom (framework-agnostic, ideal for fine-grained subscriptions) const sorting = table.atoms.sorting.get() @@ -345,26 +354,24 @@ function MyTable() { } ``` -### Opt-Out: v8-Style Full State Subscription +### Default: v8-Style Full State Subscription -If you want v8-style behavior where the component re-renders on any state change, pass `state => state` as the selector: +The default selector already gives v8-style behavior where the component re-renders on any registered table state change: ```tsx -// Re-renders on ANY state change (like v8) -const table = useTable( - { - features, - rowModels: { /* ... */ }, - columns, - data, - }, - (state) => state, // Subscribe to entire state -) +const table = useTable({ + features, + rowModels: { /* ... */ }, + columns, + data, +}) -// table.state now contains the full state +// table.state contains the full registered state const { sorting, pagination, columnFilters } = table.state ``` +Passing `(state) => state` is equivalent to the default and is no longer necessary. Pass a custom selector when you want `table.state` to contain only specific slices, or pass `() => null` and use `table.Subscribe` lower in the tree when the parent should not re-render for table state changes. + ### Controlled State Controlled state patterns work similarly to v8: diff --git a/docs/framework/react/guide/pagination.md b/docs/framework/react/guide/pagination.md index cf4dceb0bb..286acb7fcb 100644 --- a/docs/framework/react/guide/pagination.md +++ b/docs/framework/react/guide/pagination.md @@ -75,8 +75,6 @@ const table = useTable({ }) ``` -> **Migrating from v8?** See [useLegacyTable](./use-legacy-table) for incremental migration. - ### Manual Server-Side Pagination If you decide that you need to use server-side pagination, here is how you can implement it. diff --git a/docs/framework/solid/guide/migrating.md b/docs/framework/solid/guide/migrating.md new file mode 100644 index 0000000000..d3139115be --- /dev/null +++ b/docs/framework/solid/guide/migrating.md @@ -0,0 +1,599 @@ +--- +title: Migrating to TanStack Table v9 (Solid) +--- + +## What's New in TanStack Table v9 + +TanStack Table v9 is a major release that makes table setup more explicit and more tree-shakeable. The Solid adapter keeps the same headless rendering model, but table creation, row model registration, state reads, and rendering helpers have changed. + +### 1. Tree-shaking + +- **Features are tree-shakeable**: register only the features a table uses. +- **Row models are explicit**: client-side row processing moved from root `get*RowModel` options to the `rowModels` object. +- **Function registries moved to factories**: pass `sortFns`, `filterFns`, and `aggregationFns` to the row model factories that need them. + +### 2. State Management + +- **Uses TanStack Store**: state is backed by TanStack Store atoms with Solid-aware reactivity. +- **State is per-slice**: slices like `sorting`, `pagination`, and `rowSelection` are exposed through `table.atoms`. +- **Solid-native reads**: atom reads participate in Solid tracking when called inside JSX, `createMemo`, `createEffect`, or `table.Subscribe`. +- **External atoms**: apps can own individual slices with atoms from `@tanstack/solid-store`. + +### 3. Composability + +- **`tableOptions()`**: compose reusable table option fragments. +- **`createTableHook()`**: define app-specific table factories with shared features, row models, defaults, and components. + +### The Good News: Most Upgrades Are Opt-in + +- You can begin with `stockFeatures`, then audit down to explicit features. +- Most markup using `` loops, `table.getHeaderGroups()`, and `table.getRowModel().rows` stays familiar. +- Feature APIs like `table.nextPage()`, `column.toggleSorting()`, and `row.toggleSelected()` remain the preferred way to change state. + +The main migration is replacing `createSolidTable` with `createTable`, then moving feature and row-model setup into the v9 shape. + +--- + +## Core Breaking Changes + +### Entrypoint Rename + +```tsx +// v8 +import { createSolidTable } from '@tanstack/solid-table' + +const table = createSolidTable(options) + +// v9 +import { createTable } from '@tanstack/solid-table' + +const table = createTable(options) +``` + +### New Required Options: `features` and `rowModels` + +```tsx +// v8 +import { + createSolidTable, + getCoreRowModel, +} from '@tanstack/solid-table' + +const table = createSolidTable({ + columns, + get data() { + return data() + }, + getCoreRowModel: getCoreRowModel(), +}) + +// v9 +import { createTable, tableFeatures } from '@tanstack/solid-table' + +const features = tableFeatures({}) + +const table = createTable({ + features, + rowModels: {}, // core row model is automatic + columns, + get data() { + return data() + }, +}) +``` + +Keep `features`, `rowModels`, and column definitions outside reactive component work when they are static. + +--- + +## The `features` Option + +Features control which APIs, options, and state slices exist on the table. + +### Importing Individual Features + +```tsx +import { + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + tableFeatures, +} from '@tanstack/solid-table' + +const features = tableFeatures({ + columnFilteringFeature, + columnVisibilityFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, +}) +``` + +If a feature is not registered, its APIs and state slice are not available. For example, `table.atoms.rowSelection` requires `rowSelectionFeature`. + +### Using `stockFeatures` for v8-like Behavior + +`stockFeatures` is useful when you want a quick v8-like migration path before auditing features. + +```tsx +import { createTable, stockFeatures } from '@tanstack/solid-table' + +const table = createTable({ + features: stockFeatures, + rowModels, + columns, + get data() { + return data() + }, +}) +``` + +Use it as a migration shortcut, not as the preferred production end state. + +### Available Features + +| Feature | Import Name | +|---|---| +| Column Filtering | `columnFilteringFeature` | +| Global Filtering | `globalFilteringFeature` | +| Row Sorting | `rowSortingFeature` | +| Row Pagination | `rowPaginationFeature` | +| Row Selection | `rowSelectionFeature` | +| Row Expanding | `rowExpandingFeature` | +| Row Pinning | `rowPinningFeature` | +| Column Pinning | `columnPinningFeature` | +| Column Visibility | `columnVisibilityFeature` | +| Column Ordering | `columnOrderingFeature` | +| Column Sizing | `columnSizingFeature` | +| Column Resizing | `columnResizingFeature` | +| Column Grouping | `columnGroupingFeature` | +| Column Faceting | `columnFacetingFeature` | +| Global Faceting | `globalFacetingFeature` | + +--- + +## The `rowModels` Option + +Row models now live under `rowModels`. + +### Migration Mapping + +| v8 Option | v9 `rowModels` Key | v9 Factory Function | +|---|---|---| +| `getCoreRowModel()` | (automatic) | Not needed | +| `getFilteredRowModel()` | `filteredRowModel` | `createFilteredRowModel(filterFns)` | +| `getSortedRowModel()` | `sortedRowModel` | `createSortedRowModel(sortFns)` | +| `getPaginationRowModel()` | `paginatedRowModel` | `createPaginatedRowModel()` | +| `getExpandedRowModel()` | `expandedRowModel` | `createExpandedRowModel()` | +| `getGroupedRowModel()` | `groupedRowModel` | `createGroupedRowModel(aggregationFns)` | +| `getFacetedRowModel()` | `facetedRowModel` | `createFacetedRowModel()` | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues` | `createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues` | `createFacetedUniqueValues()` | + +### Full Migration Example + +```tsx +// v8 +import { + createSolidTable, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + filterFns, + sortingFns, +} from '@tanstack/solid-table' + +const table = createSolidTable({ + columns, + get data() { + return data() + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + filterFns, + sortingFns, +}) + +// v9 +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTable, + filterFns, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/solid-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, +}) + +const rowModels = { + filteredRowModel: createFilteredRowModel(filterFns), + sortedRowModel: createSortedRowModel(sortFns), + paginatedRowModel: createPaginatedRowModel(), +} + +const table = createTable({ + features, + rowModels, + columns, + get data() { + return data() + }, +}) +``` + +--- + +## State Management Changes + +Solid v9 uses table atoms backed by Solid primitives. Prefer narrow atom reads or Solid memos over broad whole-state reads. + +| Surface | Use | +|---|---| +| `table.atoms..get()` | Narrow reactive reads inside Solid tracking scopes. | +| `table.store.get()` | Current full state snapshot. Use mostly for debug output or intentionally broad dependencies. | +| `table.Subscribe` | A Solid render boundary whose child reads the atoms it needs. | +| `table.baseAtoms.` | Internal writable atoms. Prefer feature APIs or external atoms. | + +### Accessing State + +```tsx +// v8 +const sorting = table.getState().sorting + +// v9: narrow atom read +const sorting = table.atoms.sorting.get() + +// v9: full snapshot +const tableState = table.store.get() +``` + +Use Solid primitives to derive reactive values: + +```tsx +import { createMemo } from 'solid-js' + +const pagination = createMemo(() => table.atoms.pagination.get()) +const pageIndex = createMemo(() => pagination().pageIndex) +const tableStateJson = createMemo(() => + JSON.stringify(table.store.get(), null, 2), +) +``` + +Atom reads can also be used directly in JSX: + +```tsx + + Page {table.atoms.pagination.get().pageIndex + 1} of {table.getPageCount()} + +``` + +### Fine-grained Updates with `table.Subscribe` + +`table.Subscribe` creates a Solid tracking boundary. Read the atoms needed by that block inside the child function. + +```tsx + + {(atoms) => { + const pagination = atoms.pagination.get() + + return ( + Page {pagination.pageIndex + 1} + ) + }} + +``` + +### Controlled State + +Use getters in `state` so Solid tracks the current signal values. + +```tsx +import { createSignal } from 'solid-js' +import type { PaginationState, SortingState } from '@tanstack/solid-table' + +const [sorting, setSorting] = createSignal([]) +const [pagination, setPagination] = createSignal({ + pageIndex: 0, + pageSize: 10, +}) + +const table = createTable({ + features, + rowModels, + columns, + get data() { + return data() + }, + state: { + get sorting() { + return sorting() + }, + get pagination() { + return pagination() + }, + }, + onSortingChange: setSorting, + onPaginationChange: setPagination, +}) +``` + +The v8-style top-level `onStateChange` callback is gone. Use per-slice `on[State]Change` handlers or external atoms. + +### External Atoms + +External atoms are useful when the app should own a table state slice outside one component. + +```tsx +import { createAtom, useSelector } from '@tanstack/solid-store' +import type { PaginationState, SortingState } from '@tanstack/solid-table' + +const sortingAtom = createAtom([]) +const paginationAtom = createAtom({ + pageIndex: 0, + pageSize: 10, +}) + +function MyTable() { + const pagination = useSelector(paginationAtom) + + const table = createTable({ + features, + rowModels, + columns, + get data() { + return data() + }, + atoms: { + sorting: sortingAtom, + pagination: paginationAtom, + }, + }) + + return Page {pagination().pageIndex + 1} +} +``` + +Do not provide both `atoms.pagination` and `state.pagination`; the atom owns that slice. + +--- + +## Column Helper Changes + +Column helpers and column types now include `TFeatures` first. + +```tsx +// v8 +const columnHelper = createColumnHelper() +const columns: ColumnDef[] = [ + columnHelper.accessor('age', { + header: 'Age', + sortingFn: 'alphanumeric', + }), +] + +// v9 +const columnHelper = createColumnHelper() +const columns: Array> = columnHelper.columns([ + columnHelper.accessor('age', { + header: 'Age', + sortFn: 'alphanumeric', + }), +]) +``` + +Use `columnHelper.columns([...])` to keep `TValue` inference across nested column definitions. + +--- + +## Rendering Changes + +Replace `flexRender(def, context)` calls with `FlexRender`. + +```tsx +// v8 +{flexRender(header.column.columnDef.header, header.getContext())} + +